From ca6f140ac7074fcacf1dcba32f071ba465875c41 Mon Sep 17 00:00:00 2001 From: aherbert Date: Wed, 16 Mar 2022 14:39:06 +0000 Subject: [PATCH] RNG-169: Update array seed conversion to use optimum seed length Avoid duplication of the input byte[] to zero fill it to a length modulus 4 or 8 for conversion by the NumberFactory. During array to array conversions only convert the minimum of the required number of bytes for the native seed type, or the number of input bytes. Change all array conversions to use little-endian byte order. --- .../simple/internal/ByteArray2IntArray.java | 49 +++++-- .../simple/internal/ByteArray2LongArray.java | 49 +++++-- .../simple/internal/IntArray2LongArray.java | 50 ++++++-- .../simple/internal/LongArray2IntArray.java | 49 +++++-- .../rng/simple/internal/NativeSeedType.java | 16 ++- .../rng/simple/internal/SeedUtils.java | 61 +++++++++ .../internal/ArrayConverterEndianTest.java | 77 +++++++++++ .../internal/ByteArray2IntArrayTest.java | 73 +++++++++-- .../internal/ByteArray2LongArrayTest.java | 73 +++++++++-- .../internal/IntArray2LongArrayTest.java | 81 ++++++++++-- .../internal/LongArray2IntArrayTest.java | 104 +++++++++++++++ .../simple/internal/NativeSeedTypeTest.java | 120 +++++++++++++++++- .../rng/simple/internal/SeedUtilsTest.java | 27 +++- src/changes/changes.xml | 12 ++ 14 files changed, 754 insertions(+), 87 deletions(-) create mode 100644 commons-rng-simple/src/test/java/org/apache/commons/rng/simple/internal/ArrayConverterEndianTest.java create mode 100644 commons-rng-simple/src/test/java/org/apache/commons/rng/simple/internal/LongArray2IntArrayTest.java diff --git a/commons-rng-simple/src/main/java/org/apache/commons/rng/simple/internal/ByteArray2IntArray.java b/commons-rng-simple/src/main/java/org/apache/commons/rng/simple/internal/ByteArray2IntArray.java index 60cb997a1..832c08c64 100644 --- a/commons-rng-simple/src/main/java/org/apache/commons/rng/simple/internal/ByteArray2IntArray.java +++ b/commons-rng-simple/src/main/java/org/apache/commons/rng/simple/internal/ByteArray2IntArray.java @@ -16,26 +16,51 @@ */ package org.apache.commons.rng.simple.internal; -import java.util.Arrays; - -import org.apache.commons.rng.core.util.NumberFactory; - /** * Creates a {@code int[]} from a {@code byte[]}. * * @since 1.0 */ -public class ByteArray2IntArray implements SeedConverter { - /** Number of bytes in an {@code int}. */ - private static final int INT_SIZE = 4; - +public class ByteArray2IntArray implements Seed2ArrayConverter { /** {@inheritDoc} */ @Override public int[] convert(byte[] seed) { - final byte[] tmp = seed.length % INT_SIZE == 0 ? - seed : - Arrays.copyOf(seed, INT_SIZE * ((seed.length + INT_SIZE - 1) / INT_SIZE)); + // Full length conversion + return convertSeed(seed, SeedUtils.intSizeFromByteSize(seed.length)); + } + + /** + * {@inheritDoc} + * + * @since 1.5 + */ + @Override + public int[] convert(byte[] seed, int outputSize) { + return convertSeed(seed, outputSize); + } + + /** + * Creates an array of {@code int} values from a sequence of bytes. The integers are + * filled in little-endian order (least significant byte first). + * + * @param input Input bytes + * @param length Output length + * @return an array of {@code int}. + */ + private static int[] convertSeed(byte[] input, int length) { + final int[] output = new int[length]; + + // Overflow-safe minimum using long + final int n = (int) Math.min(input.length, length * (long) Integer.BYTES); + // Little-endian fill + for (int i = 0; i < n; i++) { + // i = byte index + // i >> 2 = integer index + // i & 0x3 = byte number in the integer [0, 3] + // (i & 0x3) << 3 = little-endian byte shift to the integer {0, 8, 16, 24} + output[i >> 2] |= (input[i] & 0xff) << ((i & 0x3) << 3); + } - return NumberFactory.makeIntArray(tmp); + return output; } } diff --git a/commons-rng-simple/src/main/java/org/apache/commons/rng/simple/internal/ByteArray2LongArray.java b/commons-rng-simple/src/main/java/org/apache/commons/rng/simple/internal/ByteArray2LongArray.java index 6805155c9..6369f00aa 100644 --- a/commons-rng-simple/src/main/java/org/apache/commons/rng/simple/internal/ByteArray2LongArray.java +++ b/commons-rng-simple/src/main/java/org/apache/commons/rng/simple/internal/ByteArray2LongArray.java @@ -16,26 +16,51 @@ */ package org.apache.commons.rng.simple.internal; -import java.util.Arrays; - -import org.apache.commons.rng.core.util.NumberFactory; - /** * Creates a {@code long[]} from a {@code byte[]}. * * @since 1.0 */ -public class ByteArray2LongArray implements SeedConverter { - /** Number of bytes in a {@code long}. */ - private static final int LONG_SIZE = 8; - +public class ByteArray2LongArray implements Seed2ArrayConverter { /** {@inheritDoc} */ @Override public long[] convert(byte[] seed) { - final byte[] tmp = seed.length % LONG_SIZE == 0 ? - seed : - Arrays.copyOf(seed, LONG_SIZE * ((seed.length + LONG_SIZE - 1) / LONG_SIZE)); + // Full length conversion + return convertSeed(seed, SeedUtils.longSizeFromByteSize(seed.length)); + } + + /** + * {@inheritDoc} + * + * @since 1.5 + */ + @Override + public long[] convert(byte[] seed, int outputSize) { + return convertSeed(seed, outputSize); + } + + /** + * Creates an array of {@code long} values from a sequence of bytes. The longs are + * filled in little-endian order (least significant byte first). + * + * @param input Input bytes + * @param length Output length + * @return an array of {@code long}. + */ + private static long[] convertSeed(byte[] input, int length) { + final long[] output = new long[length]; + + // Overflow-safe minimum using long + final int n = (int) Math.min(input.length, length * (long) Long.BYTES); + // Little-endian fill + for (int i = 0; i < n; i++) { + // i = byte index + // i >> 3 = long index + // i & 0x7 = byte number in the long [0, 7] + // (i & 0x7) << 3 = little-endian byte shift to the long {0, 8, 16, 24, 32, 36, 40, 48, 56} + output[i >> 3] |= (input[i] & 0xffL) << ((i & 0x7) << 3); + } - return NumberFactory.makeLongArray(tmp); + return output; } } diff --git a/commons-rng-simple/src/main/java/org/apache/commons/rng/simple/internal/IntArray2LongArray.java b/commons-rng-simple/src/main/java/org/apache/commons/rng/simple/internal/IntArray2LongArray.java index dd77d2d0a..c5572358d 100644 --- a/commons-rng-simple/src/main/java/org/apache/commons/rng/simple/internal/IntArray2LongArray.java +++ b/commons-rng-simple/src/main/java/org/apache/commons/rng/simple/internal/IntArray2LongArray.java @@ -16,25 +16,55 @@ */ package org.apache.commons.rng.simple.internal; -import org.apache.commons.rng.core.util.NumberFactory; - /** * Creates a {@code long[]} from an {@code int[]}. * + *

Note: From version 1.5 this conversion uses the int bytes to compose the long bytes + * in little-endian order. + * The output {@code long[]} can be converted back using {@link LongArray2IntArray}. + * * @since 1.0 */ -public class IntArray2LongArray implements SeedConverter { +public class IntArray2LongArray implements Seed2ArrayConverter { /** {@inheritDoc} */ @Override public long[] convert(int[] seed) { - final int outSize = (seed.length + 1) / 2; - final long[] out = new long[outSize]; - for (int i = 0; i < outSize; i++) { - final int lo = seed[i]; - final int hi = outSize + i < seed.length ? seed[outSize + i] : 0; - out[i] = NumberFactory.makeLong(hi, lo); + // Full length conversion + return convertSeed(seed, SeedUtils.longSizeFromIntSize(seed.length)); + } + + /** + * {@inheritDoc} + * + * @since 1.5 + */ + @Override + public long[] convert(int[] seed, int outputSize) { + return convertSeed(seed, outputSize); + } + + /** + * Creates an array of {@code long} values from a sequence of ints. The longs are + * filled in little-endian order (least significant byte first). + * + * @param input Input bytes + * @param length Output length + * @return an array of {@code long}. + */ + private static long[] convertSeed(int[] input, int length) { + final long[] output = new long[length]; + + // Overflow-safe minimum using long + final int n = (int) Math.min(input.length, length * 2L); + // Little-endian fill + for (int i = 0; i < n; i++) { + // i = int index + // i >> 1 = long index + // i & 0x1 = int number in the long [0, 1] + // (i & 0x1) << 5 = little-endian byte shift to the long {0, 32} + output[i >> 1] |= (input[i] & 0xffffffffL) << ((i & 0x1) << 5); } - return out; + return output; } } diff --git a/commons-rng-simple/src/main/java/org/apache/commons/rng/simple/internal/LongArray2IntArray.java b/commons-rng-simple/src/main/java/org/apache/commons/rng/simple/internal/LongArray2IntArray.java index d5f2bcade..d33f4ea69 100644 --- a/commons-rng-simple/src/main/java/org/apache/commons/rng/simple/internal/LongArray2IntArray.java +++ b/commons-rng-simple/src/main/java/org/apache/commons/rng/simple/internal/LongArray2IntArray.java @@ -16,24 +16,55 @@ */ package org.apache.commons.rng.simple.internal; -import org.apache.commons.rng.core.util.NumberFactory; - /** * Creates an {@code int[]} from a {@code long[]}. * + *

Note: From version 1.5 this conversion uses the long bytes in little-endian order. + * The output {@code int[]} can be converted back using {@link IntArray2LongArray}. + * * @since 1.0 */ -public class LongArray2IntArray implements SeedConverter { +public class LongArray2IntArray implements Seed2ArrayConverter { /** {@inheritDoc} */ @Override public int[] convert(long[] seed) { - final int[] out = new int[seed.length * 2]; - for (int i = 0; i < seed.length; i++) { - final long current = seed[i]; - out[i] = NumberFactory.extractLo(current); - out[seed.length + i] = NumberFactory.extractHi(current); + // Full length conversion + return convertSeed(seed, SeedUtils.intSizeFromLongSize(seed.length)); + } + + /** + * {@inheritDoc} + * + * @since 1.5 + */ + @Override + public int[] convert(long[] seed, int outputSize) { + return convertSeed(seed, outputSize); + } + + /** + * Creates an array of {@code int} values from a sequence of bytes. The integers are + * filled in little-endian order (least significant byte first). + * + * @param input Input bytes + * @param length Output length + * @return an array of {@code int}. + */ + private static int[] convertSeed(long[] input, int length) { + final int[] output = new int[length]; + + // Overflow-safe minimum using long + final int n = (int) Math.min(input.length * 2L, length); + // Little-endian fill + // Alternate low/high 32-bits from each long + for (int i = 0; i < n; i++) { + // i = int index + // i >> 1 = long index + // i & 0x1 = int number in the long [0, 1] + // (i & 0x1) << 5 = little-endian long shift to the int {0, 32} + output[i] = (int)((input[i >> 1]) >>> ((i & 0x1) << 5)); } - return out; + return output; } } diff --git a/commons-rng-simple/src/main/java/org/apache/commons/rng/simple/internal/NativeSeedType.java b/commons-rng-simple/src/main/java/org/apache/commons/rng/simple/internal/NativeSeedType.java index 9bbc12648..012868691 100644 --- a/commons-rng-simple/src/main/java/org/apache/commons/rng/simple/internal/NativeSeedType.java +++ b/commons-rng-simple/src/main/java/org/apache/commons/rng/simple/internal/NativeSeedType.java @@ -118,11 +118,15 @@ protected int[] convert(int[] seed, int size) { } @Override protected int[] convert(long[] seed, int size) { - return LONG_ARRAY_TO_INT_ARRAY.convert(seed); + // Avoid zero filling seeds that are too short + return LONG_ARRAY_TO_INT_ARRAY.convert(seed, + Math.min(size, SeedUtils.intSizeFromLongSize(seed.length))); } @Override protected int[] convert(byte[] seed, int size) { - return BYTE_ARRAY_TO_INT_ARRAY.convert(seed); + // Avoid zero filling seeds that are too short + return BYTE_ARRAY_TO_INT_ARRAY.convert(seed, + Math.min(size, SeedUtils.intSizeFromByteSize(seed.length))); } }, /** The seed type is {@code long[]}. */ @@ -143,7 +147,9 @@ protected long[] convert(Long seed, int size) { } @Override protected long[] convert(int[] seed, int size) { - return INT_ARRAY_TO_LONG_ARRAY.convert(seed); + // Avoid zero filling seeds that are too short + return INT_ARRAY_TO_LONG_ARRAY.convert(seed, + Math.min(size, SeedUtils.longSizeFromIntSize(seed.length))); } @Override protected long[] convert(long[] seed, int size) { @@ -151,7 +157,9 @@ protected long[] convert(long[] seed, int size) { } @Override protected long[] convert(byte[] seed, int size) { - return BYTE_ARRAY_TO_LONG_ARRAY.convert(seed); + // Avoid zero filling seeds that are too short + return BYTE_ARRAY_TO_LONG_ARRAY.convert(seed, + Math.min(size, SeedUtils.longSizeFromByteSize(seed.length))); } }; diff --git a/commons-rng-simple/src/main/java/org/apache/commons/rng/simple/internal/SeedUtils.java b/commons-rng-simple/src/main/java/org/apache/commons/rng/simple/internal/SeedUtils.java index 5d6aa14d4..5612ada32 100644 --- a/commons-rng-simple/src/main/java/org/apache/commons/rng/simple/internal/SeedUtils.java +++ b/commons-rng-simple/src/main/java/org/apache/commons/rng/simple/internal/SeedUtils.java @@ -210,4 +210,65 @@ private static int copyToOutput(byte[] digits, int bits, int upper, int lower) { digits[lower] = digits[upper]; return newbits; } + + /** + * Compute the size of an {@code int} array required to hold the specified number of bytes. + * Allows space for any remaining bytes that do not fit exactly in a 4 byte integer. + *

+     * n = ceil(size / 4)
+     * 
+ * + * @param size the size in bytes (assumed to be positive) + * @return the size in ints + */ + static int intSizeFromByteSize(int size) { + return (size + 3) >>> 2; + } + + /** + * Compute the size of an {@code long} array required to hold the specified number of bytes. + * Allows space for any remaining bytes that do not fit exactly in an 8 byte long. + *
+     * n = ceil(size / 8)
+     * 
+ * + * @param size the size in bytes (assumed to be positive) + * @return the size in longs + */ + static int longSizeFromByteSize(int size) { + return (size + 7) >>> 3; + } + + /** + * Compute the size of an {@code int} array required to hold the specified number of longs. + * Prevents overflow to a negative number when doubling the size. + *
+     * n = min(size * 2, 2^31 - 1)
+     * 
+ * + * @param size the size in longs (assumed to be positive) + * @return the size in ints + */ + static int intSizeFromLongSize(int size) { + // Avoid overflow when doubling the length. + // If n is negative the signed shift creates a mask with all bits set; + // otherwise it is zero and n is unchanged after the or operation. + // The final mask clears the sign bit in the event n did overflow. + final int n = size << 1; + return (n | (n >> 31)) & Integer.MAX_VALUE; + } + + /** + * Compute the size of an {@code long} array required to hold the specified number of ints. + * Allows space for an odd int. + *
+     * n = ceil(size / 2)
+     * 
+ * + * @param size the size in ints (assumed to be positive) + * @return the size in longs + */ + static int longSizeFromIntSize(int size) { + return (size + 1) >>> 1; + } } diff --git a/commons-rng-simple/src/test/java/org/apache/commons/rng/simple/internal/ArrayConverterEndianTest.java b/commons-rng-simple/src/test/java/org/apache/commons/rng/simple/internal/ArrayConverterEndianTest.java new file mode 100644 index 000000000..fe141f6d6 --- /dev/null +++ b/commons-rng-simple/src/test/java/org/apache/commons/rng/simple/internal/ArrayConverterEndianTest.java @@ -0,0 +1,77 @@ +/* + * 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.simple.internal; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.Arrays; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.IntStream; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Tests the endian conversion of all array converters. + */ +class ArrayConverterEndianTest { + /** + * Gets the lengths for the byte[] seeds. + * + * @return the lengths + */ + static IntStream getLengths() { + return IntStream.rangeClosed(0, 16); + } + + @ParameterizedTest + @MethodSource(value = {"getLengths"}) + void testLittleEndian(int bytes) { + final byte[] seedBytes = new byte[bytes]; + ThreadLocalRandom.current().nextBytes(seedBytes); + + // Reference implementation using a ByteBuffer + final ByteBuffer bb = ByteBuffer.wrap( + Arrays.copyOf(seedBytes, SeedUtils.longSizeFromByteSize(bytes) * Long.BYTES)) + .order(ByteOrder.LITTLE_ENDIAN); + + // byte[] -> int[] + final int[] expectedInt = new int[SeedUtils.intSizeFromByteSize(bytes)]; + for (int i = 0; i < expectedInt.length; i++) { + expectedInt[i] = bb.getInt(); + } + Assertions.assertArrayEquals(expectedInt, new ByteArray2IntArray().convert(seedBytes)); + Assertions.assertArrayEquals(expectedInt, (int[]) NativeSeedType.INT_ARRAY.convert(seedBytes, expectedInt.length)); + + // byte[] -> long[] + bb.clear(); + final long[] expectedLong = new long[SeedUtils.longSizeFromByteSize(bytes)]; + for (int i = 0; i < expectedLong.length; i++) { + expectedLong[i] = bb.getLong(); + } + Assertions.assertArrayEquals(expectedLong, new ByteArray2LongArray().convert(seedBytes)); + Assertions.assertArrayEquals(expectedLong, (long[]) NativeSeedType.LONG_ARRAY.convert(seedBytes, expectedLong.length)); + + // int[] -> long[] + Assertions.assertArrayEquals(expectedLong, new IntArray2LongArray().convert(expectedInt)); + Assertions.assertArrayEquals(expectedLong, (long[]) NativeSeedType.LONG_ARRAY.convert(expectedInt, expectedLong.length)); + + // long[] -> int[] + Assertions.assertArrayEquals(expectedInt, new LongArray2IntArray().convert(expectedLong, expectedInt.length)); + Assertions.assertArrayEquals(expectedInt, (int[]) NativeSeedType.INT_ARRAY.convert(expectedLong, expectedInt.length)); + } +} diff --git a/commons-rng-simple/src/test/java/org/apache/commons/rng/simple/internal/ByteArray2IntArrayTest.java b/commons-rng-simple/src/test/java/org/apache/commons/rng/simple/internal/ByteArray2IntArrayTest.java index 0b21364b0..939676abc 100644 --- a/commons-rng-simple/src/test/java/org/apache/commons/rng/simple/internal/ByteArray2IntArrayTest.java +++ b/commons-rng-simple/src/test/java/org/apache/commons/rng/simple/internal/ByteArray2IntArrayTest.java @@ -16,28 +16,73 @@ */ package org.apache.commons.rng.simple.internal; +import java.util.Arrays; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.IntStream; +import org.apache.commons.rng.core.util.NumberFactory; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; /** * Tests for the {@link ByteArray2IntArray} converter. */ class ByteArray2IntArrayTest { - @Test - void testSeedSizeIsMultipleOfIntSize() { - final byte[] seed = new byte[128]; + /** + * Gets the lengths for the byte[] seeds to convert. + * + * @return the lengths + */ + static IntStream getLengths() { + return IntStream.rangeClosed(0, Integer.BYTES * 2); + } + + /** + * Gets the expected output length. + * + * @param bytes Number of bytes + * @return the output length + */ + private static int getOutputLength(int bytes) { + return (int) Math.ceil((double) bytes / Integer.BYTES); + } + + @ParameterizedTest + @MethodSource(value = {"getLengths"}) + void testSeedSizeIsMultipleOfIntSize(int bytes) { + final byte[] seed = new byte[bytes]; + + // This calls convert without a length final int[] out = new ByteArray2IntArray().convert(seed); - Assertions.assertEquals(32, out.length); + Assertions.assertEquals(getOutputLength(bytes), out.length); + } + + @ParameterizedTest + @MethodSource(value = {"getLengths"}) + void testSeedConversion(int bytes) { + assertSeedConversion(bytes, getOutputLength(bytes)); + } + + @ParameterizedTest + @MethodSource(value = {"getLengths"}) + void testFilledSeedConversion(int bytes) { + assertSeedConversion(bytes, bytes / 3); } - @Test - void testSeedSizeIsNotMultipleOfIntSize() { - final int len = 16; - final ByteArray2IntArray conv = new ByteArray2IntArray(); - for (int i = 1; i < 4; i++) { - final byte[] seed = new byte[len + i]; - final int[] out = conv.convert(seed); - Assertions.assertEquals(5, out.length); - } + @ParameterizedTest + @MethodSource(value = {"getLengths"}) + void testTruncatedSeedConversion(int bytes) { + assertSeedConversion(bytes, bytes * 3); + } + + private static void assertSeedConversion(int bytes, int outLength) { + final byte[] seed = new byte[bytes]; + ThreadLocalRandom.current().nextBytes(seed); + final byte[] filledSeed = Arrays.copyOf(seed, outLength * Integer.BYTES); + final int[] expected = NumberFactory.makeIntArray(filledSeed); + + // This calls convert with a length + final int[] out = new ByteArray2IntArray().convert(seed, outLength); + Assertions.assertArrayEquals(expected, out); } } diff --git a/commons-rng-simple/src/test/java/org/apache/commons/rng/simple/internal/ByteArray2LongArrayTest.java b/commons-rng-simple/src/test/java/org/apache/commons/rng/simple/internal/ByteArray2LongArrayTest.java index 66780148d..9ecd0b41f 100644 --- a/commons-rng-simple/src/test/java/org/apache/commons/rng/simple/internal/ByteArray2LongArrayTest.java +++ b/commons-rng-simple/src/test/java/org/apache/commons/rng/simple/internal/ByteArray2LongArrayTest.java @@ -16,28 +16,73 @@ */ package org.apache.commons.rng.simple.internal; +import java.util.Arrays; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.IntStream; +import org.apache.commons.rng.core.util.NumberFactory; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; /** * Tests for the {@link ByteArray2LongArray} converter. */ class ByteArray2LongArrayTest { - @Test - void testSeedSizeIsMultipleOfLongSize() { - final byte[] seed = new byte[128]; + /** + * Gets the lengths for the byte[] seeds to convert. + * + * @return the lengths + */ + static IntStream getLengths() { + return IntStream.rangeClosed(0, Long.BYTES * 2); + } + + /** + * Gets the expected output length. + * + * @param bytes Number of bytes + * @return the output length + */ + private static int getOutputLength(int bytes) { + return (int) Math.ceil((double) bytes / Long.BYTES); + } + + @ParameterizedTest + @MethodSource(value = {"getLengths"}) + void testSeedSizeIsMultipleOfIntSize(int bytes) { + final byte[] seed = new byte[bytes]; + + // This calls convert without a length final long[] out = new ByteArray2LongArray().convert(seed); - Assertions.assertEquals(16, out.length); + Assertions.assertEquals(getOutputLength(bytes), out.length); + } + + @ParameterizedTest + @MethodSource(value = {"getLengths"}) + void testSeedConversion(int bytes) { + assertSeedConversion(bytes, getOutputLength(bytes)); + } + + @ParameterizedTest + @MethodSource(value = {"getLengths"}) + void testFilledSeedConversion(int bytes) { + assertSeedConversion(bytes, bytes / 3); } - @Test - void testSeedSizeIsNotMultipleOfLongSize() { - final int len = 16; - final ByteArray2LongArray conv = new ByteArray2LongArray(); - for (int i = 1; i < 8; i++) { - final byte[] seed = new byte[len + i]; - final long[] out = conv.convert(seed); - Assertions.assertEquals(3, out.length); - } + @ParameterizedTest + @MethodSource(value = {"getLengths"}) + void testTruncatedSeedConversion(int bytes) { + assertSeedConversion(bytes, bytes * 3); + } + + private static void assertSeedConversion(int bytes, int outLength) { + final byte[] seed = new byte[bytes]; + ThreadLocalRandom.current().nextBytes(seed); + final byte[] filledSeed = Arrays.copyOf(seed, outLength * Long.BYTES); + final long[] expected = NumberFactory.makeLongArray(filledSeed); + + // This calls convert with a length + final long[] out = new ByteArray2LongArray().convert(seed, outLength); + Assertions.assertArrayEquals(expected, out); } } diff --git a/commons-rng-simple/src/test/java/org/apache/commons/rng/simple/internal/IntArray2LongArrayTest.java b/commons-rng-simple/src/test/java/org/apache/commons/rng/simple/internal/IntArray2LongArrayTest.java index e1c66e26e..d671d2e03 100644 --- a/commons-rng-simple/src/test/java/org/apache/commons/rng/simple/internal/IntArray2LongArrayTest.java +++ b/commons-rng-simple/src/test/java/org/apache/commons/rng/simple/internal/IntArray2LongArrayTest.java @@ -16,24 +16,85 @@ */ package org.apache.commons.rng.simple.internal; +import java.util.Arrays; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.IntStream; +import org.apache.commons.rng.core.util.NumberFactory; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; /** * Tests for the {@link IntArray2LongArray} converter. */ class IntArray2LongArrayTest { - @Test - void testSeedSizeIsMultipleOfLongSize() { - final int[] seed = new int[12]; - final long[] out = new IntArray2LongArray().convert(seed); - Assertions.assertEquals(6, out.length); + /** + * Gets the lengths for the int[] seeds to convert. + * + * @return the lengths + */ + static IntStream getLengths() { + return IntStream.rangeClosed(0, (Long.BYTES / Integer.BYTES) * 2); + } + + /** + * Gets the expected output length. + * + * @param ints Number of ints + * @return the output length + */ + private static int getOutputLength(int ints) { + return (int) Math.ceil((double) ints / 2); } - @Test - void testSeedSizeIsNotMultipleOfLongSize() { - final int[] seed = new int[13]; + @ParameterizedTest + @MethodSource(value = {"getLengths"}) + void testSeedSizeIsMultipleOfIntSize(int ints) { + final int[] seed = new int[ints]; + + // This calls convert without a length final long[] out = new IntArray2LongArray().convert(seed); - Assertions.assertEquals(7, out.length); + Assertions.assertEquals(getOutputLength(ints), out.length); + } + + @ParameterizedTest + @MethodSource(value = {"getLengths"}) + void testSeedConversion(int ints) { + assertSeedConversion(ints, getOutputLength(ints)); + } + + @ParameterizedTest + @MethodSource(value = {"getLengths"}) + void testFilledSeedConversion(int ints) { + assertSeedConversion(ints, ints / 3); + } + + @ParameterizedTest + @MethodSource(value = {"getLengths"}) + void testTruncatedSeedConversion(int ints) { + assertSeedConversion(ints, ints * 3); + } + + private static void assertSeedConversion(int ints, int outLength) { + final int[] seed = ThreadLocalRandom.current().ints(ints).toArray(); + + // Convert to little-endian int array + final int[] filledSeed = Arrays.copyOf(seed, outLength * 2); + final long[] expected = new long[filledSeed.length / 2]; + for (int i = 0; i < expected.length; i++) { + expected[i] = NumberFactory.makeLong(filledSeed[2 * i + 1], filledSeed[2 * i]); + } + + // This calls convert with a length + final long[] out = new IntArray2LongArray().convert(seed, outLength); + Assertions.assertArrayEquals(expected, out); + } + + @ParameterizedTest + @MethodSource(value = {"getLengths"}) + void testIntArrayToLongArrayToIntArray(int ints) { + final int[] expected = ThreadLocalRandom.current().ints(ints).toArray(); + final int[] out = new LongArray2IntArray().convert(new IntArray2LongArray().convert(expected), ints); + Assertions.assertArrayEquals(expected, out); } } diff --git a/commons-rng-simple/src/test/java/org/apache/commons/rng/simple/internal/LongArray2IntArrayTest.java b/commons-rng-simple/src/test/java/org/apache/commons/rng/simple/internal/LongArray2IntArrayTest.java new file mode 100644 index 000000000..40c6bb8cf --- /dev/null +++ b/commons-rng-simple/src/test/java/org/apache/commons/rng/simple/internal/LongArray2IntArrayTest.java @@ -0,0 +1,104 @@ +/* + * 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.simple.internal; + +import java.util.Arrays; +import java.util.concurrent.ThreadLocalRandom; +import java.util.stream.IntStream; +import org.apache.commons.rng.core.util.NumberFactory; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +/** + * Tests for the {@link LongArray2IntArray} converter. + */ +class LongArray2IntArrayTest { + /** + * Gets the lengths for the output int[] seeds. + * + * @return the lengths + */ + static IntStream getLengths() { + return IntStream.rangeClosed(0, 5); + } + + /** + * Gets the expected input length to produce the specified number of ints. + * + * @param ints Number of ints + * @return the input length + */ + private static int getInputLength(int ints) { + return (int) Math.ceil((double) ints / Integer.BYTES); + } + + @ParameterizedTest + @MethodSource(value = {"getLengths"}) + void testIntSizeIsMultipleOfSeedSize(int ints) { + final long[] seed = new long[getInputLength(ints)]; + + // This calls convert without a length + final int[] out = new LongArray2IntArray().convert(seed); + Assertions.assertEquals(seed.length * 2, out.length); + } + + @ParameterizedTest + @MethodSource(value = {"getLengths"}) + void testSeedConversion(int ints) { + assertSeedConversion(ints, getInputLength(ints)); + } + + @ParameterizedTest + @MethodSource(value = {"getLengths"}) + void testFilledSeedConversion(int ints) { + assertSeedConversion(ints, getInputLength(ints) / 3); + } + + @ParameterizedTest + @MethodSource(value = {"getLengths"}) + void testTruncatedSeedConversion(int ints) { + assertSeedConversion(ints, getInputLength(ints) * 3); + } + + private static void assertSeedConversion(int ints, int inLength) { + final long[] seed = ThreadLocalRandom.current().longs(inLength).toArray(); + + // Convert to little-endian int array + final long[] filledSeed = Arrays.copyOf(seed, ints * 2); + final int[] expected = new int[ints]; + for (int i = 0; i < expected.length; i++) { + if ((i & 1) == 0) { + expected[i] = NumberFactory.extractLo(filledSeed[i >> 1]); + } else { + expected[i] = NumberFactory.extractHi(filledSeed[i >> 1]); + } + } + + // This calls convert with a length + final int[] out = new LongArray2IntArray().convert(seed, ints); + Assertions.assertArrayEquals(expected, out); + } + + @ParameterizedTest + @MethodSource(value = {"getLengths"}) + void testLongArrayToIntArrayToIntArray(int ints) { + final long[] expected = ThreadLocalRandom.current().longs(ints / 2).toArray(); + final long[] out = new IntArray2LongArray().convert(new LongArray2IntArray().convert(expected)); + Assertions.assertArrayEquals(expected, out); + } +} diff --git a/commons-rng-simple/src/test/java/org/apache/commons/rng/simple/internal/NativeSeedTypeTest.java b/commons-rng-simple/src/test/java/org/apache/commons/rng/simple/internal/NativeSeedTypeTest.java index b5280c95b..4ad3c5ce4 100644 --- a/commons-rng-simple/src/test/java/org/apache/commons/rng/simple/internal/NativeSeedTypeTest.java +++ b/commons-rng-simple/src/test/java/org/apache/commons/rng/simple/internal/NativeSeedTypeTest.java @@ -17,15 +17,17 @@ package org.apache.commons.rng.simple.internal; import java.math.BigDecimal; +import java.util.Arrays; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.MethodSource; /** * Tests for the {@link NativeSeedType} factory seed conversions. * - *

Note: All supported types are tested in the {@link NativeSeedTypeParametricTest} + *

Note: All supported types are tested in the {@link NativeSeedTypeParametricTest}. */ class NativeSeedTypeTest { /** @@ -72,4 +74,120 @@ void testConvertSeedToBytesUsingByteArray() { final byte[] seed = {42, 78, 99}; Assertions.assertSame(seed, NativeSeedType.convertSeedToBytes(seed)); } + + @ParameterizedTest + @CsvSource({ + "3, 1", + "4, 1", + "5, 1", + "7, 2", + "8, 2", + "9, 2", + "13, 2", + "0, 0", + }) + void testConvertByteArrayToIntArray(int byteSize, int intSize) { + final byte[] bytes = new byte[byteSize]; + // Get the maximum number of ints to use all the bytes + final int size = SeedUtils.intSizeFromByteSize(byteSize); + // If the size is too big, fill the remaining bytes with non-zero values. + // These should not be used during conversion. + if (size > intSize) { + Arrays.fill(bytes, intSize * Integer.BYTES, bytes.length, (byte) -1); + } + final int expected = Math.min(size, intSize); + final int[] ints = (int[]) NativeSeedType.INT_ARRAY.convert(bytes, intSize); + Assertions.assertEquals(expected, ints.length); + // The seed should be zero, i.e. extra bytes have not been used + for (final int i : ints) { + Assertions.assertEquals(0, i); + } + } + + @ParameterizedTest + @CsvSource({ + "7, 1", + "8, 1", + "9, 1", + "15, 2", + "16, 2", + "17, 2", + "25, 2", + "0, 0", + }) + void testConvertByteArrayToLongArray(int byteSize, int longSize) { + final byte[] bytes = new byte[byteSize]; + // Get the maximum number of longs to use all the bytes + final long size = SeedUtils.longSizeFromByteSize(byteSize); + // If the size is too big, fill the remaining bytes with non-zero values. + // These should not be used during conversion. + if (size > longSize) { + Arrays.fill(bytes, longSize * Long.BYTES, bytes.length, (byte) -1); + } + final long expected = Math.min(size, longSize); + final long[] longs = (long[]) NativeSeedType.LONG_ARRAY.convert(bytes, longSize); + Assertions.assertEquals(expected, longs.length); + // The seed should be zero, i.e. extra bytes have not been used + for (final long i : longs) { + Assertions.assertEquals(0, i); + } + } + + @ParameterizedTest + @CsvSource({ + "1, 1", + "2, 1", + "3, 1", + "3, 2", + "4, 2", + "5, 2", + "7, 2", + "0, 0", + }) + void testConvertIntArrayToLongArray(int intSize, int longSize) { + final int[] ints = new int[intSize]; + // Get the maximum number of longs to use all the ints + final long size = SeedUtils.longSizeFromIntSize(intSize); + // If the size is too big, fill the remaining ints with non-zero values. + // These should not be used during conversion. + if (size > longSize) { + Arrays.fill(ints, longSize * 2, ints.length, -1); + } + final long expected = Math.min(size, longSize); + final long[] longs = (long[]) NativeSeedType.LONG_ARRAY.convert(ints, longSize); + Assertions.assertEquals(expected, longs.length); + // The seed should be zero, i.e. extra ints have not been used + for (final long i : longs) { + Assertions.assertEquals(0, i); + } + } + + @ParameterizedTest + @CsvSource({ + "0, 1", + "1, 1", + "2, 1", + "0, 2", + "1, 2", + "2, 2", + "3, 2", + "0, 0", + }) + void testConvertLongArrayToIntArray(int longSize, int intSize) { + final long[] longs = new long[longSize]; + // Get the maximum number of ints to use all the longs + final int size = longSize * 2; + // If the size is too big, fill the remaining longs with non-zero values. + // These should not be used during conversion. + if (size > intSize) { + Arrays.fill(longs, SeedUtils.longSizeFromIntSize(intSize), longs.length, -1); + } + final int expected = Math.min(size, intSize); + final int[] ints = (int[]) NativeSeedType.INT_ARRAY.convert(longs, intSize); + Assertions.assertEquals(expected, ints.length); + // The seed should be zero, i.e. extra longs have not been used + for (final int i : ints) { + Assertions.assertEquals(0, i); + } + } } diff --git a/commons-rng-simple/src/test/java/org/apache/commons/rng/simple/internal/SeedUtilsTest.java b/commons-rng-simple/src/test/java/org/apache/commons/rng/simple/internal/SeedUtilsTest.java index d87d941fb..1949c335a 100644 --- a/commons-rng-simple/src/test/java/org/apache/commons/rng/simple/internal/SeedUtilsTest.java +++ b/commons-rng-simple/src/test/java/org/apache/commons/rng/simple/internal/SeedUtilsTest.java @@ -22,7 +22,8 @@ import org.apache.commons.rng.core.source64.SplitMix64; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; - +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import java.util.Arrays; /** @@ -101,4 +102,28 @@ void testCreateLongHexPermutation() { "Not uniform in digit " + j); } } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, Integer.MAX_VALUE}) + void testIntSizeFromByteSize(int size) { + Assertions.assertEquals((int) Math.ceil((double) size / Integer.BYTES), SeedUtils.intSizeFromByteSize(size)); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, Integer.MAX_VALUE}) + void testLongSizeFromByteSize(int size) { + Assertions.assertEquals((int) Math.ceil((double) size / Long.BYTES), SeedUtils.longSizeFromByteSize(size)); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, Integer.MAX_VALUE}) + void testIntSizeFromLongSize(int size) { + Assertions.assertEquals((int) Math.min(size * 2L, Integer.MAX_VALUE), SeedUtils.intSizeFromLongSize(size)); + } + + @ParameterizedTest + @ValueSource(ints = {0, 1, 2, 3, 4, 5, Integer.MAX_VALUE}) + void testLongSizeFromIntSize(int size) { + Assertions.assertEquals((int) Math.ceil((double) size / 2), SeedUtils.longSizeFromIntSize(size)); + } } diff --git a/src/changes/changes.xml b/src/changes/changes.xml index ee36c92d7..cac1d419d 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -81,6 +81,18 @@ re-run tests that fail, and pass the build if they succeed within the allotted number of reruns (the test will be marked as 'flaky' in the report). "> + + "RandomSource.create": Update array seed conversion to use optimum seed length. + Avoid duplication of input bytes and conversion of bytes that will be discarded. + This introduces a behavioural change for int[] to long[] and long[] to int[] + seed conversions. All array-to-array seed conversions now use little endian format, + matching the byte[] conversion behaviour since 1.0. + Any fixed seeds from previous versions in byte[], Long, Int, or the native seed type + will create the same RNG state. Any int[] or long[] fixed seeds converted to long[] + or int[] native seeds use the same number of bits from the seed but arranged in a + different order after conversion. Seeds larger than the native seed size will use + a different subset of the bits. + Update implementations of "UniformRandomProvider.nextBytes" with a range [start, start + length) to be consistent with the exception conditions of the