Skip to content

Commit

Permalink
Feature: new methods nextInt(Range) and nextLong(Range) (#1416)
Browse files Browse the repository at this point in the history
* #1395 add methods `nextInt(Range)` and `nextLong(Range)`

to define explicitly if the min/max should be inclusive or exclusive

* deprecate method `Number.randomNumber(..., boolean strict)`

there is no point to call `randomNumber(strict = false)`. It's the same as just `faker.random().nextLong()`.
  • Loading branch information
asolntsev authored Oct 31, 2024
1 parent d5f4ffb commit 6f00a78
Show file tree
Hide file tree
Showing 11 changed files with 212 additions and 49 deletions.
2 changes: 1 addition & 1 deletion src/main/java/net/datafaker/providers/base/Barcode.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
6 changes: 3 additions & 3 deletions src/main/java/net/datafaker/providers/base/DateAndTime.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
Expand Down
32 changes: 19 additions & 13 deletions src/main/java/net/datafaker/providers/base/Number.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package net.datafaker.providers.base;

import net.datafaker.service.Range;

import java.math.BigDecimal;
import java.math.RoundingMode;

Expand Down Expand Up @@ -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
*/
Expand All @@ -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 <code>numberOfDigits</code>
* @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) {
Expand All @@ -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) {
Expand Down
41 changes: 30 additions & 11 deletions src/main/java/net/datafaker/service/RandomService.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ public Integer nextInt(int minInclusive, int maxInclusive) {
return random.nextInt(minInclusive, maxInclusive + 1);
}

public int nextInt(Range<Integer> range) {
return (int) nextLong(range.cast(Integer::longValue));
}

@SuppressWarnings("unused")
public float nextFloat() {
return random.nextFloat();
Expand All @@ -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<Long> 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;
}

/**
Expand All @@ -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) {
Expand All @@ -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));
}
Expand Down
68 changes: 68 additions & 0 deletions src/main/java/net/datafaker/service/Range.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package net.datafaker.service;

import java.util.function.Function;

public record Range<T extends Comparable<T>>(Bound<T> from, Bound<T> to) {
public enum End {INCLUSIVE, EXCLUSIVE}
public record Bound<T>(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 <T extends Comparable<T>> Range<T> 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 <T extends Comparable<T>> Range<T> 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 <T extends Number & Comparable<T>> Range<T> 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 <T extends Comparable<T>> Range<T> 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 <V extends Comparable<V>> Range<V> cast(Function<T, V> caster) {
return new Range<>(
new Bound<>(caster.apply(from.value), from.end),
new Bound<>(caster.apply(to.value), to.end)
);
}
}
13 changes: 6 additions & 7 deletions src/test/java/net/datafaker/providers/base/NumberTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<BaseFaker> {

Expand Down Expand Up @@ -62,27 +63,25 @@ 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
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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,11 @@ private static Stream<Arguments> 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)
);
}

Expand Down
14 changes: 7 additions & 7 deletions src/test/java/net/datafaker/providers/base/UniqueTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ void fetchFromYaml_shouldReturnValuesInRandomOrderUsingRandomService() {

faker = new BaseFaker(new Locale("test"), randomService);

Set<String> 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<String> 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)
Expand Down
29 changes: 25 additions & 4 deletions src/test/java/net/datafaker/service/RandomNumbersTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -35,5 +57,4 @@ private <T extends Number & Comparable<T>> SortedSet<T> all(Supplier<T> lambda)
}
return result;
}

}
4 changes: 2 additions & 2 deletions src/test/java/net/datafaker/service/RandomServiceTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down Expand Up @@ -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();
Expand Down
Loading

0 comments on commit 6f00a78

Please sign in to comment.