diff --git a/src/main/java/net/datafaker/providers/base/Barcode.java b/src/main/java/net/datafaker/providers/base/Barcode.java index 3fe488920..e75b040f7 100644 --- a/src/main/java/net/datafaker/providers/base/Barcode.java +++ b/src/main/java/net/datafaker/providers/base/Barcode.java @@ -45,7 +45,7 @@ private static int roundToHighestMultiplyOfTen(int number) { private long ean(int length) { long firstPart = switch (length) { - case 8, 12, 13, 14 -> this.faker.number().randomNumber(length - 1, true); + case 8, 12, 13, 14 -> this.faker.number().randomNumber(length - 1); default -> 0; }; int odd = 0; diff --git a/src/main/java/net/datafaker/providers/base/DateAndTime.java b/src/main/java/net/datafaker/providers/base/DateAndTime.java index e2e961232..ec122cc1b 100644 --- a/src/main/java/net/datafaker/providers/base/DateAndTime.java +++ b/src/main/java/net/datafaker/providers/base/DateAndTime.java @@ -310,7 +310,7 @@ public String birthday(int minAge, int maxAge, String pattern) { /** * Generates a random Duration lower than max. * - * @param max the maximum value + * @param max the maximum value (exclusive in most cases) * @param unit the temporal unit (day or shorter than a day) * @return a random Duration lower than {@code max}. * @throws IllegalArgumentException if the {@code unit} is invalid. @@ -322,8 +322,8 @@ public Duration duration(long max, ChronoUnit unit) { /** * Generates a random Duration between min and max. * - * @param min the minimum value - * @param max the maximum value + * @param min the minimum value (inclusive) + * @param max the maximum value (exclusive in most cases) * @param unit the temporal unit (day or shorter than a day) * @return a random Duration between {@code min} inclusive and {@code max} exclusive if {@code max} greater {@code min}. * @throws IllegalArgumentException if the {@code unit} is invalid. diff --git a/src/main/java/net/datafaker/providers/base/Number.java b/src/main/java/net/datafaker/providers/base/Number.java index 7a130978a..8dfeac9e6 100644 --- a/src/main/java/net/datafaker/providers/base/Number.java +++ b/src/main/java/net/datafaker/providers/base/Number.java @@ -1,5 +1,7 @@ package net.datafaker.providers.base; +import net.datafaker.service.Range; + import java.math.BigDecimal; import java.math.RoundingMode; @@ -60,8 +62,8 @@ public int numberBetween(final int min, final int max) { } /** - * @param min the lower bound (include min) - * @param max the upper bound (not include max) + * @param min the lower bound (inclusive) + * @param max the upper bound (exclusive in most cases) * @return a random number on faker.number() between min and max * if min = max, return min */ @@ -71,26 +73,31 @@ public long numberBetween(long min, long max) { final long realMax = Math.max(min, max); final long amplitude = realMax - realMin; if (amplitude >= 0) { - return faker.random().nextLong(amplitude) + realMin; + return faker.random().nextLong(Range.inclusiveExclusive(realMin, realMax)); } return decimalBetween(realMin, realMax).longValue(); } /** * @param numberOfDigits the number of digits the generated value should have - * @param strict whether or not the generated value should have exactly numberOfDigits + * @param strict NOT USED + * @deprecated use {@link #randomNumber(int)} instead */ + @Deprecated public long randomNumber(int numberOfDigits, boolean strict) { + return randomNumber(numberOfDigits); + } + + /** + * @param numberOfDigits the number of digits the generated value should have + */ + public long randomNumber(int numberOfDigits) { if (numberOfDigits <= 0) { - return faker.random().nextInt(1); + throw new IllegalArgumentException("Number of digits must be positive"); } long min = pow(10, numberOfDigits - 1); - if (strict) { - long max = min * 10; - return faker.random().nextLong(max - min) + min; - } - - return faker.random().nextLong(min * 10); + long max = min * 10; + return faker.random().nextLong(min, max); } private long pow(long value, int d) { @@ -108,8 +115,7 @@ private long pow(long value, int d) { * Returns a random number */ public long randomNumber() { - int numberOfDigits = faker.random().nextInt(1, 10); - return randomNumber(numberOfDigits, false); + return faker.random().nextLong(); } public double randomDouble(int maxNumberOfDecimals, int min, int max) { diff --git a/src/main/java/net/datafaker/service/RandomService.java b/src/main/java/net/datafaker/service/RandomService.java index e85685e07..fbc8a6a22 100644 --- a/src/main/java/net/datafaker/service/RandomService.java +++ b/src/main/java/net/datafaker/service/RandomService.java @@ -41,6 +41,10 @@ public Integer nextInt(int minInclusive, int maxInclusive) { return random.nextInt(minInclusive, maxInclusive + 1); } + public int nextInt(Range range) { + return (int) nextLong(range.cast(Integer::longValue)); + } + @SuppressWarnings("unused") public float nextFloat() { return random.nextFloat(); @@ -50,18 +54,28 @@ public long nextLong() { return random.nextLong(); } - public long nextLong(long maxInclusive) { - if (maxInclusive <= 0) { - throw new IllegalArgumentException("bound must be positive: " + maxInclusive); + public long nextLong(long maxExclusive) { + if (maxExclusive <= 0) { + throw new IllegalArgumentException("bound must be positive: " + maxExclusive); } + return nextLong(0, maxExclusive); + } - long bits, val; - do { - long randomLong = random.nextLong(); - bits = (randomLong << 1) >>> 1; - val = bits % maxInclusive; - } while (bits - val + (maxInclusive - 1) < 0L); - return val; + public long nextLong(Range range) { + return switch (range.from().end()) { + case EXCLUSIVE -> switch (range.to().end()) { + case EXCLUSIVE -> random.nextLong(plusOne(range.from().value()), range.to().value()); + case INCLUSIVE -> random.nextLong(plusOne(range.from().value()), plusOne(range.to().value())); + }; + case INCLUSIVE -> switch (range.to().end()) { + case EXCLUSIVE -> random.nextLong(range.from().value(), range.to().value()); + case INCLUSIVE -> random.nextLong(range.from().value(), plusOne(range.to().value())); + }; + }; + } + + private static long plusOne(long value) { + return value == Long.MAX_VALUE ? value : value + 1; } /** @@ -70,7 +84,7 @@ public long nextLong(long maxInclusive) { * Otherwise, {@code max} is exclusive. * * @param min lower bound (inclusive) - * @param max upper bound (exclusive in most case) + * @param max upper bound (exclusive in most cases) * @return a random long value between {@code min} and {@code max} */ public long nextLong(long min, long max) { @@ -83,6 +97,11 @@ public double nextDouble() { return random.nextDouble(); } + /** + * @param min (inclusive) + * @param max (inclusive) + * @return a random double value between {@code min} and {@code max} (both inclusive) + */ public double nextDouble(double min, double max) { return min + (nextDouble() * (max - min)); } diff --git a/src/main/java/net/datafaker/service/Range.java b/src/main/java/net/datafaker/service/Range.java new file mode 100644 index 000000000..fd12c8a5a --- /dev/null +++ b/src/main/java/net/datafaker/service/Range.java @@ -0,0 +1,68 @@ +package net.datafaker.service; + +import java.util.function.Function; + +public record Range>(Bound from, Bound to) { + public enum End {INCLUSIVE, EXCLUSIVE} + public record Bound(T value, End end) {} + + /** + * A range that contains all values + * 1. greater than or equal to {@code from}, and + * 2. less than or equal to {@code to}. + */ + public static > Range inclusive(T from, T to) { + if (from.compareTo(to) > 0) { + throw new IllegalArgumentException("Lower bound (%s) > upper bound (%s)".formatted(from, to)); + } + return new Range<>(new Bound<>(from, End.INCLUSIVE), new Bound<>(to, End.INCLUSIVE)); + } + + /** + * A range that contains all values + * 1. greater than or equal to {@code from}, and + * 2. less than {@code to}. + */ + public static > Range inclusiveExclusive(T from, T to) { + if (from.compareTo(to) >= 0) { + throw new IllegalArgumentException("Lower bound (%s) >= upper bound (%s)".formatted(from, to)); + } + return new Range<>(new Bound<>(from, End.INCLUSIVE), new Bound<>(to, End.EXCLUSIVE)); + } + + /** + * A range that contains all values + * 1. strictly greater than {@code from}, and + * 2. strictly less than {@code to}. + */ + public static > Range exclusive(T from, T to) { + if (to.longValue() == Long.MIN_VALUE) { + throw new IllegalArgumentException("Lower bound (%s) >= upper bound (%s)".formatted(from, to)); + } + long upperLimit = to.longValue() - 1; + if (from.longValue() >= upperLimit) { + throw new IllegalArgumentException("Lower bound (%s) >= upper bound-1 (%s)".formatted(from, upperLimit)); + } + return new Range<>(new Bound<>(from, End.EXCLUSIVE), new Bound<>(to, End.EXCLUSIVE)); + } + + /** + * A range that contains all values + * 1. strictly greater than {@code from}, and + * 2. less than or equal to {@code to}. + */ + public static > Range exclusiveInclusive(T from, T to) { + if (from.compareTo(to) >= 0) { + throw new IllegalArgumentException("Lower bound (%s) >= upper bound (%s)".formatted(from, to)); + } + return new Range<>(new Bound<>(from, End.EXCLUSIVE), new Bound<>(to, End.INCLUSIVE)); + } + + @SuppressWarnings("unchecked") + public > Range cast(Function caster) { + return new Range<>( + new Bound<>(caster.apply(from.value), from.end), + new Bound<>(caster.apply(to.value), to.end) + ); + } +} diff --git a/src/test/java/net/datafaker/providers/base/NumberTest.java b/src/test/java/net/datafaker/providers/base/NumberTest.java index 163046272..080a0c0da 100644 --- a/src/test/java/net/datafaker/providers/base/NumberTest.java +++ b/src/test/java/net/datafaker/providers/base/NumberTest.java @@ -15,6 +15,7 @@ import java.util.function.Supplier; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; class NumberTest extends BaseFakerTest { @@ -62,19 +63,17 @@ void testRandomNumber() { void testRandomNumberWithSingleDigitStrict() { final Number number = faker.number(); for (int i = 0; i < 100; ++i) { - long value = number.randomNumber(1, true); + long value = number.randomNumber(1); assertThat(value).isLessThan(10L) .isGreaterThanOrEqualTo(0L); } } @Test - void testRandomNumberWithZeroDigitsStrict() { + void randomNumberWithZeroDigits() { final Number number = faker.number(); - for (int i = 0; i < 100; ++i) { - long value = number.randomNumber(0, true); - assertThat(value).isZero(); - } + assertThatThrownBy(() -> number.randomNumber(0)) + .isInstanceOf(IllegalArgumentException.class); } @Test @@ -82,7 +81,7 @@ void testRandomNumberWithGivenDigitsStrict() { final Number number = faker.number(); for (int i = 1; i < 9; ++i) { for (int x = 0; x < 100; ++x) { - long value = number.randomNumber(i, true); + long value = number.randomNumber(i); String stringValue = String.valueOf(value); assertThat(stringValue).hasSize(i); } diff --git a/src/test/java/net/datafaker/providers/base/TimeAndDateTest.java b/src/test/java/net/datafaker/providers/base/TimeAndDateTest.java index 41aa6b669..839b23bcf 100644 --- a/src/test/java/net/datafaker/providers/base/TimeAndDateTest.java +++ b/src/test/java/net/datafaker/providers/base/TimeAndDateTest.java @@ -218,7 +218,11 @@ private static Stream generateDurationsWithMinMax() { Arguments.of(65, 98, ChronoUnit.SECONDS), Arguments.of(76, 100, ChronoUnit.MILLIS), Arguments.of(879, 1030, ChronoUnit.MICROS), - Arguments.of(879, 1030, ChronoUnit.NANOS) + Arguments.of(879, 1030, ChronoUnit.NANOS), + Arguments.of(0, Long.MAX_VALUE, ChronoUnit.NANOS), + Arguments.of(Long.MIN_VALUE, 0, ChronoUnit.NANOS), + Arguments.of(Long.MAX_VALUE - 1, Long.MAX_VALUE, ChronoUnit.NANOS), + Arguments.of(Long.MIN_VALUE, Long.MAX_VALUE, ChronoUnit.NANOS) ); } diff --git a/src/test/java/net/datafaker/providers/base/UniqueTest.java b/src/test/java/net/datafaker/providers/base/UniqueTest.java index bf5e99cab..6f453d618 100644 --- a/src/test/java/net/datafaker/providers/base/UniqueTest.java +++ b/src/test/java/net/datafaker/providers/base/UniqueTest.java @@ -31,13 +31,13 @@ void fetchFromYaml_shouldReturnValuesInRandomOrderUsingRandomService() { faker = new BaseFaker(new Locale("test"), randomService); - Set results = new HashSet<>(); - - results.add(faker.unique().fetchFromYaml(key)); - results.add(faker.unique().fetchFromYaml(key)); - results.add(faker.unique().fetchFromYaml(key)); - results.add(faker.unique().fetchFromYaml(key)); - results.add(faker.unique().fetchFromYaml(key)); + Set results = Set.of( + faker.unique().fetchFromYaml(key), + faker.unique().fetchFromYaml(key), + faker.unique().fetchFromYaml(key), + faker.unique().fetchFromYaml(key), + faker.unique().fetchFromYaml(key) + ); assertThat(results) .hasSize(5) diff --git a/src/test/java/net/datafaker/service/RandomNumbersTest.java b/src/test/java/net/datafaker/service/RandomNumbersTest.java index 3d832a93b..60f8ad209 100644 --- a/src/test/java/net/datafaker/service/RandomNumbersTest.java +++ b/src/test/java/net/datafaker/service/RandomNumbersTest.java @@ -14,12 +14,34 @@ class RandomNumbersTest { @Test void nextInt_minInclusive_maxExclusive() { + assertThat(all(() -> randomService.nextInt(3))).containsExactly(0, 1, 2); + assertThat(all(() -> randomService.nextInt(2, 4))).containsExactly(2, 3, 4); // legacy assertThat(all(() -> randomService.nextLong(3))).containsExactly(0L, 1L, 2L); assertThat(all(() -> randomService.nextLong(2, 4))).containsExactly(2L, 3L); - assertThat(all(() -> randomService.nextInt(3))).containsExactly(0, 1, 2); + } + + @Test + void range_inclusive() { + assertThat(all(() -> randomService.nextInt(Range.inclusive(2, 4)))).containsExactly(2, 3, 4); + assertThat(all(() -> randomService.nextLong(Range.inclusive(2L, 4L)))).containsExactly(2L, 3L, 4L); + } + + @Test + void range_inclusive_exclusive() { + assertThat(all(() -> randomService.nextInt(Range.inclusiveExclusive(2, 5)))).containsExactly(2, 3, 4); + assertThat(all(() -> randomService.nextLong(Range.inclusiveExclusive(2L, 5L)))).containsExactly(2L, 3L, 4L); + } - // inclusive: see https://github.com/datafaker-net/datafaker/issues/1395 - assertThat(all(() -> randomService.nextInt(2, 4))).containsExactly(2, 3, 4); + @Test + void range_exclusive_inclusive() { + assertThat(all(() -> randomService.nextInt(Range.exclusiveInclusive(2, 5)))).containsExactly(3, 4, 5); + assertThat(all(() -> randomService.nextLong(Range.exclusiveInclusive(2L, 5L)))).containsExactly(3L, 4L, 5L); + } + + @Test + void range_exclusive() { + assertThat(all(() -> randomService.nextLong(Range.exclusive(2L, 5L)))).containsExactly(3L, 4L); + assertThat(all(() -> randomService.nextInt(Range.exclusive(2, 5)))).containsExactly(3, 4); } @RepeatedTest(100) @@ -35,5 +57,4 @@ private > SortedSet all(Supplier lambda) } return result; } - } diff --git a/src/test/java/net/datafaker/service/RandomServiceTest.java b/src/test/java/net/datafaker/service/RandomServiceTest.java index 1362a3477..b3f12d155 100644 --- a/src/test/java/net/datafaker/service/RandomServiceTest.java +++ b/src/test/java/net/datafaker/service/RandomServiceTest.java @@ -33,7 +33,7 @@ void testLongWithinBoundary(RandomService randomService) { assertThat(randomService.nextLong(1)).isZero(); for (int i = 1; i < 10; i++) { - assertThat(randomService.nextLong(2)).isLessThan(2L); + assertThat(randomService.nextLong(2)).isGreaterThanOrEqualTo(0).isLessThan(2L); } } @@ -87,7 +87,7 @@ void predictableRandomRange() { assertThat(f1).isEqualTo(0.41291267F); assertThat(l1).isEqualTo(1092083446069765248L); - assertThat(l2).isEqualTo(1L); + assertThat(l2).isEqualTo(0L); assertThat(l3).isEqualTo(538L); assertThat(b).isFalse(); diff --git a/src/test/java/net/datafaker/service/RangeTest.java b/src/test/java/net/datafaker/service/RangeTest.java new file mode 100644 index 000000000..165b3d36a --- /dev/null +++ b/src/test/java/net/datafaker/service/RangeTest.java @@ -0,0 +1,46 @@ +package net.datafaker.service; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatCode; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class RangeTest { + @Test + void inclusive_minShouldBeLessThanMax() { + assertThatThrownBy(() -> Range.inclusive(45, 44)).isInstanceOf(IllegalArgumentException.class); + assertThatCode(() -> Range.inclusive(45, 45)).doesNotThrowAnyException(); + } + + @Test + void exclusive_minShouldBeLessThanMaxMinusOne() { + assertThatThrownBy(() -> Range.exclusive(44, 45)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Lower bound (44) >= upper bound-1 (44)"); + assertThatThrownBy(() -> Range.exclusive(Integer.MIN_VALUE, Integer.MIN_VALUE)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Lower bound (-2147483648) >= upper bound-1 (-2147483649)"); + assertThatThrownBy(() -> Range.exclusive(Long.MAX_VALUE, Long.MAX_VALUE)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Lower bound (9223372036854775807) >= upper bound-1 (9223372036854775806)"); + assertThatThrownBy(() -> Range.exclusive(Long.MIN_VALUE, Long.MIN_VALUE)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Lower bound (-9223372036854775808) >= upper bound (-9223372036854775808)"); + assertThatThrownBy(() -> Range.exclusive(Long.MAX_VALUE, Long.MIN_VALUE)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Lower bound (9223372036854775807) >= upper bound (-9223372036854775808)"); + assertThatCode(() -> Range.exclusive(44, 46)).doesNotThrowAnyException(); + } + + @Test + void inclusiveExclusive_minShouldBeLessThanMax() { + assertThatThrownBy(() -> Range.inclusiveExclusive(44, 44)).isInstanceOf(IllegalArgumentException.class); + assertThatCode(() -> Range.inclusiveExclusive(44, 45)).doesNotThrowAnyException(); + } + + @Test + void exclusiveInclusive_minShouldBeLessThanMax() { + assertThatThrownBy(() -> Range.exclusiveInclusive(44, 44)).isInstanceOf(IllegalArgumentException.class); + assertThatCode(() -> Range.exclusiveInclusive(44, 45)).doesNotThrowAnyException(); + } +}