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();
+ }
+}