This is an automated email from the ASF dual-hosted git repository. aherbert pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-rng.git
commit fffcf4c1664aa7a4db66686db29312346d5a7ace Author: Alex Herbert <aherb...@apache.org> AuthorDate: Thu Jul 15 21:24:26 2021 +0100 RNG-140: Add LongSampler interface and UniformLongSampler This is replacement for RandomUtils.nextLong(long lo, long hi) from Commons Math. --- .../rng/sampling/distribution/LongSampler.java | 31 ++ .../distribution/SharedStateLongSampler.java | 30 ++ .../sampling/distribution/UniformLongSampler.java | 334 +++++++++++++++++++++ .../apache/commons/rng/sampling/RandomAssert.java | 15 + .../distribution/UniformLongSamplerTest.java | 324 ++++++++++++++++++++ 5 files changed, 734 insertions(+) diff --git a/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/distribution/LongSampler.java b/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/distribution/LongSampler.java new file mode 100644 index 0000000..a3e2fac --- /dev/null +++ b/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/distribution/LongSampler.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.rng.sampling.distribution; + +/** + * Sampler that generates values of type {@code long}. + * + * @since 1.4 + */ +public interface LongSampler { + /** + * Creates a sample. + * + * @return a sample. + */ + long sample(); +} diff --git a/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/distribution/SharedStateLongSampler.java b/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/distribution/SharedStateLongSampler.java new file mode 100644 index 0000000..9e052c2 --- /dev/null +++ b/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/distribution/SharedStateLongSampler.java @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.rng.sampling.distribution; + +import org.apache.commons.rng.sampling.SharedStateSampler; + +/** + * Sampler that generates values of type {@code long} and can create new instances to sample + * from the same state with a given source of randomness. + * + * @since 1.4 + */ +public interface SharedStateLongSampler + extends LongSampler, SharedStateSampler<SharedStateLongSampler> { + // Composite interface +} diff --git a/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/distribution/UniformLongSampler.java b/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/distribution/UniformLongSampler.java new file mode 100644 index 0000000..2eb05a1 --- /dev/null +++ b/commons-rng-sampling/src/main/java/org/apache/commons/rng/sampling/distribution/UniformLongSampler.java @@ -0,0 +1,334 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.apache.commons.rng.sampling.distribution; + +import org.apache.commons.rng.UniformRandomProvider; + +/** + * Discrete uniform distribution sampler generating values of type {@code long}. + * + * <p>Sampling uses {@link UniformRandomProvider#nextLong}.</p> + * + * <p>When the range is a power of two the number of calls is 1 per sample. + * Otherwise a rejection algorithm is used to ensure uniformity. In the worst + * case scenario where the range spans half the range of a {@code long} + * (2<sup>63</sup> + 1) the expected number of calls is 2 per sample.</p> + * + * @since 1.4 + */ +public abstract class UniformLongSampler implements SharedStateLongSampler { + /** Underlying source of randomness. */ + protected final UniformRandomProvider rng; + + /** + * Discrete uniform distribution sampler when the sample value is fixed. + */ + private static class FixedUniformLongSampler extends UniformLongSampler { + /** The value. */ + private final long value; + + /** + * @param value The value. + */ + FixedUniformLongSampler(long value) { + // No requirement for the RNG + super(null); + this.value = value; + } + + @Override + public long sample() { + return value; + } + + @Override + public String toString() { + // No RNG to include in the string + return "Uniform deviate [X=" + value + "]"; + } + + @Override + public UniformLongSampler withUniformRandomProvider(UniformRandomProvider rng) { + // No requirement for the RNG + return this; + } + } + + /** + * Discrete uniform distribution sampler when the range is a power of 2 and greater than 1. + * This sampler assumes the lower bound of the range is 0. + * + * <p>Note: This cannot be used when the range is 1 (2^0) as the shift would be 64-bits + * which is ignored by the shift operator.</p> + */ + private static class PowerOf2RangeUniformLongSampler extends UniformLongSampler { + /** Bit shift to apply to the long sample. */ + private final int shift; + + /** + * @param rng Generator of uniformly distributed random numbers. + * @param range Maximum range of the sample (exclusive). + * Must be a power of 2 greater than 2^0. + */ + PowerOf2RangeUniformLongSampler(UniformRandomProvider rng, + long range) { + super(rng); + this.shift = Long.numberOfLeadingZeros(range) + 1; + } + + /** + * @param rng Generator of uniformly distributed random numbers. + * @param source Source to copy. + */ + PowerOf2RangeUniformLongSampler(UniformRandomProvider rng, + PowerOf2RangeUniformLongSampler source) { + super(rng); + this.shift = source.shift; + } + + @Override + public long sample() { + // Use a bit shift to favour the most significant bits. + return rng.nextLong() >>> shift; + } + + @Override + public UniformLongSampler withUniformRandomProvider(UniformRandomProvider rng) { + return new PowerOf2RangeUniformLongSampler(rng, this); + } + } + + /** + * Discrete uniform distribution sampler when the range is small + * enough to fit in a positive long. + * This sampler assumes the lower bound of the range is 0. + */ + private static class SmallRangeUniformLongSampler extends UniformLongSampler { + /** Maximum range of the sample (exclusive). */ + private final long n; + + /** + * @param rng Generator of uniformly distributed random numbers. + * @param range Maximum range of the sample (exclusive). + */ + SmallRangeUniformLongSampler(UniformRandomProvider rng, + long range) { + super(rng); + this.n = range; + } + + @Override + public long sample() { + // Rejection algorithm copied from o.a.c.rng.core.BaseProvider + // to avoid the (n <= 0) conditional. + // See the JDK javadoc for java.util.Random.nextInt(int) for + // a description of the algorithm. + long bits; + long val; + do { + bits = rng.nextLong() >>> 1; + val = bits % n; + } while (bits - val + (n - 1) < 0); + return val; + } + + @Override + public UniformLongSampler withUniformRandomProvider(UniformRandomProvider rng) { + return new SmallRangeUniformLongSampler(rng, n); + } + } + + /** + * Discrete uniform distribution sampler when the range between lower and upper is too large + * to fit in a positive long. + */ + private static class LargeRangeUniformLongSampler extends UniformLongSampler { + /** Lower bound. */ + private final long lower; + /** Upper bound. */ + private final long upper; + + /** + * @param rng Generator of uniformly distributed random numbers. + * @param lower Lower bound (inclusive) of the distribution. + * @param upper Upper bound (inclusive) of the distribution. + */ + LargeRangeUniformLongSampler(UniformRandomProvider rng, + long lower, + long upper) { + super(rng); + this.lower = lower; + this.upper = upper; + } + + @Override + public long sample() { + // Use a simple rejection method. + // This is used when (upper-lower) >= Long.MAX_VALUE. + // This will loop on average 2 times in the worst case scenario + // when (upper-lower) == Long.MAX_VALUE. + while (true) { + final long r = rng.nextLong(); + if (r >= lower && + r <= upper) { + return r; + } + } + } + + @Override + public UniformLongSampler withUniformRandomProvider(UniformRandomProvider rng) { + return new LargeRangeUniformLongSampler(rng, lower, upper); + } + } + + /** + * Adds an offset to an underlying discrete sampler. + */ + private static class OffsetUniformLongSampler extends UniformLongSampler { + /** The offset. */ + private final long offset; + /** The long sampler. */ + private final UniformLongSampler sampler; + + /** + * @param offset The offset for the sample. + * @param sampler The discrete sampler. + */ + OffsetUniformLongSampler(long offset, + UniformLongSampler sampler) { + // No requirement for the RNG + super(null); + this.offset = offset; + this.sampler = sampler; + } + + @Override + public long sample() { + return offset + sampler.sample(); + } + + @Override + public String toString() { + return sampler.toString(); + } + + @Override + public UniformLongSampler withUniformRandomProvider(UniformRandomProvider rng) { + return new OffsetUniformLongSampler(offset, sampler.withUniformRandomProvider(rng)); + } + } + + /** + * @param rng Generator of uniformly distributed random numbers. + */ + private UniformLongSampler(UniformRandomProvider rng) { + this.rng = rng; + } + + /** {@inheritDoc} */ + @Override + public String toString() { + return "Uniform deviate [" + rng.toString() + "]"; + } + + /** {@inheritDoc} */ + // Redeclare the signature to return a UniformLongSampler not a SharedStateLongSampler + @Override + public abstract UniformLongSampler withUniformRandomProvider(UniformRandomProvider rng); + + /** + * Creates a new discrete uniform distribution sampler. + * + * @param rng Generator of uniformly distributed random numbers. + * @param lower Lower bound (inclusive) of the distribution. + * @param upper Upper bound (inclusive) of the distribution. + * @return the sampler + * @throws IllegalArgumentException if {@code lower > upper}. + */ + public static UniformLongSampler of(UniformRandomProvider rng, + long lower, + long upper) { + if (lower > upper) { + throw new IllegalArgumentException(lower + " > " + upper); + } + + // Choose the algorithm depending on the range + + // Edge case for no range. + // This must be done first as the methods to handle lower == 0 + // do not handle upper == 0. + if (upper == lower) { + return new FixedUniformLongSampler(lower); + } + + // Algorithms to ignore the lower bound if it is zero. + if (lower == 0) { + return createZeroBoundedSampler(rng, upper); + } + + final long range = (upper - lower) + 1; + // Check power of 2 first to handle range == 2^63. + if (isPowerOf2(range)) { + return new OffsetUniformLongSampler(lower, + new PowerOf2RangeUniformLongSampler(rng, range)); + } + if (range <= 0) { + // The range is too wide to fit in a positive long (larger + // than 2^63); use a simple rejection method. + // Note: if range == 0 then the input is [Long.MIN_VALUE, Long.MAX_VALUE]. + // No specialisation exists for this and it is handled as a large range. + return new LargeRangeUniformLongSampler(rng, lower, upper); + } + // Use a sample from the range added to the lower bound. + return new OffsetUniformLongSampler(lower, + new SmallRangeUniformLongSampler(rng, range)); + } + + /** + * Create a new sampler for the range {@code 0} inclusive to {@code upper} inclusive. + * + * <p>This can handle any positive {@code upper}. + * + * @param rng Generator of uniformly distributed random numbers. + * @param upper Upper bound (inclusive) of the distribution. Must be positive. + * @return the sampler + */ + private static UniformLongSampler createZeroBoundedSampler(UniformRandomProvider rng, + long upper) { + // Note: Handle any range up to 2^63 (which is negative as a signed + // 64-bit long but handled as a power of 2) + final long range = upper + 1; + return isPowerOf2(range) ? + new PowerOf2RangeUniformLongSampler(rng, range) : + new SmallRangeUniformLongSampler(rng, range); + } + + /** + * Checks if the value is a power of 2. + * + * <p>This returns {@code true} for the value {@code Long.MIN_VALUE} which can be + * handled as an unsigned long of 2^63.</p> + * + * @param value Value. + * @return {@code true} if a power of 2 + */ + private static boolean isPowerOf2(final long value) { + return value != 0 && (value & (value - 1)) == 0; + } +} diff --git a/commons-rng-sampling/src/test/java/org/apache/commons/rng/sampling/RandomAssert.java b/commons-rng-sampling/src/test/java/org/apache/commons/rng/sampling/RandomAssert.java index 5860501..d977764 100644 --- a/commons-rng-sampling/src/test/java/org/apache/commons/rng/sampling/RandomAssert.java +++ b/commons-rng-sampling/src/test/java/org/apache/commons/rng/sampling/RandomAssert.java @@ -21,6 +21,7 @@ import org.junit.Assert; import org.apache.commons.rng.sampling.distribution.ContinuousSampler; import org.apache.commons.rng.sampling.distribution.DiscreteSampler; +import org.apache.commons.rng.sampling.distribution.LongSampler; /** * Utility class for testing random samplers. @@ -77,6 +78,20 @@ public final class RandomAssert { } /** + * Exercise the {@link LongSampler} interface, and + * ensure that the two samplers produce the same sequence. + * + * @param sampler1 First sampler. + * @param sampler2 Second sampler. + */ + public static void assertProduceSameSequence(LongSampler sampler1, + LongSampler sampler2) { + for (int i = 0; i < SAMPLES; i++) { + Assert.assertEquals(sampler1.sample(), sampler2.sample()); + } + } + + /** * Exercise the {@link Sampler} interface, and * ensure that the two samplers produce the same sequence. * diff --git a/commons-rng-sampling/src/test/java/org/apache/commons/rng/sampling/distribution/UniformLongSamplerTest.java b/commons-rng-sampling/src/test/java/org/apache/commons/rng/sampling/distribution/UniformLongSamplerTest.java new file mode 100644 index 0000000..33ced6b --- /dev/null +++ b/commons-rng-sampling/src/test/java/org/apache/commons/rng/sampling/distribution/UniformLongSamplerTest.java @@ -0,0 +1,324 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.commons.rng.sampling.distribution; + +import org.apache.commons.rng.UniformRandomProvider; +import org.apache.commons.rng.core.source64.LongProvider; +import org.apache.commons.rng.sampling.RandomAssert; +import org.apache.commons.rng.simple.RandomSource; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Locale; + +/** + * Test for the {@link UniformLongSampler}. The tests hit edge cases for the sampler + * and demonstrates uniformity of output when the underlying RNG output is uniform. + * + * <p>Note: No statistical tests for uniformity are performed on the output. The tests + * are constructed on the premise that the underlying sampling methods correctly + * use the random bits from {@link UniformRandomProvider}. Correctness + * for a small range is tested against {@link UniformRandomProvider#nextLong(long)} + * and correctness for a large range is tested that the {@link UniformRandomProvider#nextLong()} + * is within the range limits. Power of two ranges are tested against a bit shift + * of a random long. + */ +public class UniformLongSamplerTest { + /** + * Test the constructor with a bad range. + */ + @Test(expected = IllegalArgumentException.class) + public void testConstructorThrowsWithLowerAboveUpper() { + final long upper = 55; + final long lower = upper + 1; + final UniformRandomProvider rng = RandomSource.SPLIT_MIX_64.create(0L); + UniformLongSampler.of(rng, lower, upper); + } + + @Test + public void testSamplesWithRangeOf1() { + final long upper = 99; + final long lower = upper; + final UniformRandomProvider rng = RandomSource.SPLIT_MIX_64.create(); + final UniformLongSampler sampler = UniformLongSampler.of(rng, lower, upper); + for (int i = 0; i < 10; i++) { + Assert.assertEquals(lower, sampler.sample()); + } + } + + /** + * Test samples with a full long range. + * The output should be the same as the long values produced from a RNG. + */ + @Test + public void testSamplesWithFullRange() { + final long upper = Long.MAX_VALUE; + final long lower = Long.MIN_VALUE; + final UniformRandomProvider rng1 = RandomSource.SPLIT_MIX_64.create(0L); + final UniformRandomProvider rng2 = RandomSource.SPLIT_MIX_64.create(0L); + final UniformLongSampler sampler = UniformLongSampler.of(rng2, lower, upper); + for (int i = 0; i < 10; i++) { + Assert.assertEquals(rng1.nextLong(), sampler.sample()); + } + } + + /** + * Test samples with a non-power of 2 range. + * The output should be the same as the long values produced from a RNG + * based on o.a.c.rng.core.BaseProvider as the rejection algorithm is + * the same. + */ + @Test + public void testSamplesWithSmallNonPowerOf2Range() { + final long upper = 234293789329234L; + for (final long lower : new long[] {-13, 0, 13}) { + final long n = upper - lower + 1; + final UniformRandomProvider rng1 = RandomSource.SPLIT_MIX_64.create(0L); + final UniformRandomProvider rng2 = RandomSource.SPLIT_MIX_64.create(0L); + final UniformLongSampler sampler = UniformLongSampler.of(rng2, lower, upper); + for (int i = 0; i < 10; i++) { + Assert.assertEquals(lower + rng1.nextLong(n), sampler.sample()); + } + } + } + + /** + * Test samples with a power of 2 range. + * This tests the minimum and maximum output should be the range limits. + */ + @Test + public void testSamplesWithPowerOf2Range() { + final UniformRandomProvider rngZeroBits = new LongProvider() { + @Override + public long next() { + // No bits + return 0L; + } + }; + final UniformRandomProvider rngAllBits = new LongProvider() { + @Override + public long next() { + // All bits + return -1L; + } + }; + + final long lower = -3; + UniformLongSampler sampler; + // The upper range for a positive long is 2^63-1. So the max positive power of + // 2 is 2^62. However the sampler should handle a bit shift of 63 to create a range + // of Long.MIN_VALUE as this is a power of 2 as an unsigned long (2^63). + for (int i = 0; i < 64; i++) { + final long range = 1L << i; + final long upper = lower + range - 1; + sampler = UniformLongSampler.of(rngZeroBits, lower, upper); + Assert.assertEquals("Zero bits sample", lower, sampler.sample()); + sampler = UniformLongSampler.of(rngAllBits, lower, upper); + Assert.assertEquals("All bits sample", upper, sampler.sample()); + } + } + + /** + * Test samples with a power of 2 range. + * This tests the output is created using a bit shift. + */ + @Test + public void testSamplesWithPowerOf2RangeIsBitShift() { + final long lower = 0; + UniformLongSampler sampler; + // Power of 2 sampler used for a bit shift of 1 to 63. + for (int i = 1; i <= 63; i++) { + // Upper is inclusive so subtract 1 + final long upper = (1L << i) - 1; + final int shift = 64 - i; + final UniformRandomProvider rng1 = RandomSource.SPLIT_MIX_64.create(0L); + final UniformRandomProvider rng2 = RandomSource.SPLIT_MIX_64.create(0L); + sampler = UniformLongSampler.of(rng2, lower, upper); + for (int j = 0; j < 10; j++) { + Assert.assertEquals(rng1.nextLong() >>> shift, sampler.sample()); + } + } + } + + /** + * Test samples with a large non-power of 2 range. + * This tests the large range algorithm uses a rejection method. + */ + @Test + public void testSamplesWithLargeNonPowerOf2RangeIsRejectionMethod() { + // Create a range bigger than 2^63 + final long upper = Long.MAX_VALUE / 2 + 1; + final long lower = Long.MIN_VALUE / 2 - 1; + final UniformRandomProvider rng1 = RandomSource.SPLIT_MIX_64.create(0L); + final UniformRandomProvider rng2 = RandomSource.SPLIT_MIX_64.create(0L); + final UniformLongSampler sampler = UniformLongSampler.of(rng2, lower, upper); + for (int i = 0; i < 10; i++) { + // Get the expected value by the rejection method + long expected; + do { + expected = rng1.nextLong(); + } while (expected < lower || expected > upper); + Assert.assertEquals(expected, sampler.sample()); + } + } + + @Test + public void testOffsetSamplesWithNonPowerOf2Range() { + assertOffsetSamples(257); + } + + @Test + public void testOffsetSamplesWithPowerOf2Range() { + assertOffsetSamples(256); + } + + @Test + public void testOffsetSamplesWithRangeOf1() { + assertOffsetSamples(1); + } + + private static void assertOffsetSamples(long range) { + final Long seed = RandomSource.createLong(); + final UniformRandomProvider rng1 = RandomSource.SPLIT_MIX_64.create(seed); + final UniformRandomProvider rng2 = RandomSource.SPLIT_MIX_64.create(seed); + final UniformRandomProvider rng3 = RandomSource.SPLIT_MIX_64.create(seed); + + // Since the upper limit is inclusive + range = range - 1; + final long offsetLo = -13; + final long offsetHi = 42; + final UniformLongSampler sampler = UniformLongSampler.of(rng1, 0, range); + final UniformLongSampler samplerLo = UniformLongSampler.of(rng2, offsetLo, offsetLo + range); + final UniformLongSampler samplerHi = UniformLongSampler.of(rng3, offsetHi, offsetHi + range); + for (int i = 0; i < 10; i++) { + final long sample1 = sampler.sample(); + final long sample2 = samplerLo.sample(); + final long sample3 = samplerHi.sample(); + Assert.assertEquals("Incorrect negative offset sample", sample1 + offsetLo, sample2); + Assert.assertEquals("Incorrect positive offset sample", sample1 + offsetHi, sample3); + } + } + + /** + * Test the sample uniformity when using a small range that is a power of 2. + */ + @Test + public void testSampleUniformityWithPowerOf2Range() { + // Test using a RNG that outputs a counter of integers. + // The n most significant bits will be represented uniformly over a + // sequence that is a 2^n long. + final UniformRandomProvider rng = new LongProvider() { + private long bits = 0; + + @Override + public long next() { + // We reverse the bits because the most significant bits are used + return Long.reverse(bits++); + } + }; + + // n = upper range exclusive + final int n = 32; // power of 2 + final int[] histogram = new int[n]; + + final long lower = 0; + final long upper = n - 1; + + final UniformLongSampler sampler = UniformLongSampler.of(rng, lower, upper); + + final int expected = 2; + for (int i = expected * n; i-- > 0;) { + histogram[(int) sampler.sample()]++; + } + + // This should be even across the entire range + for (int value : histogram) { + Assert.assertEquals(expected, value); + } + } + + @Test + public void testSharedStateSamplerWithSmallRange() { + assertSharedStateSampler(5, 67); + } + + @Test + public void testSharedStateSamplerWithLargeRange() { + // Set the range so rejection below or above the threshold occurs with approximately + // p=0.25 for each bound. + assertSharedStateSampler(Long.MIN_VALUE / 2 - 1, Long.MAX_VALUE / 2 + 1); + } + + @Test + public void testSharedStateSamplerWithPowerOf2Range() { + assertSharedStateSampler(0, (1L << 45) - 1); + } + + @Test + public void testSharedStateSamplerWithRangeOf1() { + assertSharedStateSampler(968757657572323L, 968757657572323L); + } + + /** + * Test the SharedStateSampler implementation returns the same sequence as the source sampler + * when using an identical RNG. + * + * @param lower Lower. + * @param upper Upper. + */ + private static void assertSharedStateSampler(long lower, long upper) { + final UniformRandomProvider rng1 = RandomSource.SPLIT_MIX_64.create(0L); + final UniformRandomProvider rng2 = RandomSource.SPLIT_MIX_64.create(0L); + final UniformLongSampler sampler1 = UniformLongSampler.of(rng1, lower, upper); + final UniformLongSampler sampler2 = sampler1.withUniformRandomProvider(rng2); + RandomAssert.assertProduceSameSequence(sampler1, sampler2); + } + + @Test + public void testToStringWithSmallRange() { + assertToString(5, 67); + } + + @Test + public void testToStringWithLargeRange() { + assertToString(-99999999, Long.MAX_VALUE); + } + + @Test + public void testToStringWithPowerOf2Range() { + // Note the range is upper - lower + 1 + assertToString(0, 31); + } + + @Test + public void testToStringWithRangeOf1() { + assertToString(9, 9); + } + + /** + * Test the toString method contains the term "uniform". This is true of all samplers + * even for a fixed sample from a range of 1. + * + * @param lower Lower. + * @param upper Upper. + */ + private static void assertToString(long lower, long upper) { + final UniformRandomProvider rng = RandomSource.SPLIT_MIX_64.create(0L); + final UniformLongSampler sampler = UniformLongSampler.of(rng, lower, upper); + Assert.assertTrue(sampler.toString().toLowerCase(Locale.US).contains("uniform")); + } +}