From f9dec5e7947153f33acf32a7cd0cdf63d35000b0 Mon Sep 17 00:00:00 2001 From: Kerooker Date: Wed, 22 Aug 2018 09:43:39 -0300 Subject: [PATCH] Implement extension functions for Random: nextUInt, nextULong and nextUBytes Given there's a Random API in the stdlib, that generates all random primitives, this commit adds the possibility to generate random unsigned integers. It uses the same implementation from Random.nextInt, nextLong and nextBytes, but uses the appropriate conversions from a signed type to an unsigned type. The use of the same API guarantees that the distribution is uniform, but this commit adds some unit test to enforce that necessity. Fixes #KT-25570 --- libraries/stdlib/src/kotlin/random/Random.kt | 133 ++++++++++- libraries/stdlib/test/random/RandomTest.kt | 226 +++++++++++++++++- .../kotlin-stdlib-runtime-merged.txt | 12 + 3 files changed, 369 insertions(+), 2 deletions(-) diff --git a/libraries/stdlib/src/kotlin/random/Random.kt b/libraries/stdlib/src/kotlin/random/Random.kt index 340c48b4ad0..343576d9146 100644 --- a/libraries/stdlib/src/kotlin/random/Random.kt +++ b/libraries/stdlib/src/kotlin/random/Random.kt @@ -260,7 +260,6 @@ public abstract class Random { */ public open fun nextBytes(size: Int): ByteArray = nextBytes(ByteArray(size)) - // TODO: UInt, ULong, UByteArray /** * The default random number generator. @@ -321,6 +320,136 @@ public fun Random(seed: Int): Random = XorWowRandom(seed, seed.shr(31)) public fun Random(seed: Long): Random = XorWowRandom(seed.toInt(), seed.shr(32).toInt()) + +/** + * Gets the next random [UInt] from the random number generator. + * + * Generates a Unsigned Int random value uniformly distributed between [UInt.MIN_VALUE] and [UInt.MAX_VALUE] (inclusive) + */ +@SinceKotlin("1.3") +public fun Random.nextUInt(): UInt = nextInt().toUInt() + +/** + * Gets the next random [UInt] from the random number generator not greater than the specified [bound]. + * + * Generated an Unsigned Int random value uniformly distributed between `0`(inclusive) and the specified [bound] (exclusive). + * + * @throws IllegalArgumentException if [bound] is zero + */ +@SinceKotlin("1.3") +public fun Random.nextUInt(bound: UInt): UInt = nextUInt(0u, bound) + +/** + * Gets the next random [UInt] from the random number generator in the specified range. + * + * Generated an Unsigned Integer random value uniformly distributed between the specified [origin] (inclusive) and the specified [bound] (exclusive). + * + * @throws IllegalArgumentException if [origin] is greater than or equal to [bound] + */ +@SinceKotlin("1.3") +public fun Random.nextUInt(origin: UInt, bound: UInt): UInt { + checkUIntRangeBounds(origin, bound) + + val originTransformedToInt = origin.toInt() xor Int.MIN_VALUE + val boundTransformedToInt = bound.toInt() xor Int.MIN_VALUE + + val randomValueTransformedBack = nextInt(originTransformedToInt, boundTransformedToInt) xor Int.MIN_VALUE + + return randomValueTransformedBack.toUInt() +} + +/** + * Gets the next random [UInt] from the random number generator in the specified [range]. + * + * Generates an Unsigned Integer random value uniformly distributed in the specified [range]: + * from `range.start` inclusive to `range.endInclusive` inclusive. + * + * @throws IllegalArgumentException if [range] is empty. + */ +@SinceKotlin("1.3") +public fun Random.nextUInt(range: UIntRange): UInt = when { + range.isEmpty() -> throw IllegalArgumentException("Cannot get random in empty range: $range") + range.last < UInt.MAX_VALUE -> nextUInt(range.first, range.last + 1.toUInt()) + range.first > UInt.MIN_VALUE -> nextUInt(range.first, range.last + 1.toUInt()) + else -> nextUInt() +} + +/** + * Gets the next random [ULong] from the random number generator. + * + * Generated a Unsigned Long random value uniformly distributed between [ULong.MIN_VALUE] and [ULong.MAX_VALUE] (inclusive) + */ +@SinceKotlin("1.3") +public fun Random.nextULong(): ULong = nextLong().toULong() + +/** + * Gets the next random [ULong] from the random number generator not greater than the specified [bound]. + * + * Generated an Unsigned Long random value uniformly distributed between `0`(inclusive) and the specified [bound] (exclusive). + * + * @throws IllegalArgumentException if [bound] is zero + */ +@SinceKotlin("1.3") +public fun Random.nextULong(bound: ULong): ULong = nextULong(0uL, bound) + +/** + * Gets the next random [ULong] from the random number generator in the specified range + * + * Generated an Unsigned Long random value uniformly distributed between the specified [origin] (inclusive) and the specified [bound] (exclusive). + * + * @throws IllegalArgumentException if [origin] is greater than or equal to [bound] + */ +@SinceKotlin("1.3") +public fun Random.nextULong(origin: ULong, bound: ULong): ULong { + checkULongRangeBounds(origin, bound) + + val originTransformedToLong = origin.toLong() xor Long.MIN_VALUE + val boundTransformedToLong = bound.toLong() xor Long.MIN_VALUE + + val randomValueTransformedBack = nextLong(originTransformedToLong, boundTransformedToLong) xor Long.MIN_VALUE + return randomValueTransformedBack.toULong() +} + +/** + * Gets the next random [ULong] from the random number generator in the specified [range]. + * + * Generates an Unsigned Long random value uniformly distributed in the specified [range]: + * from `range.start` inclusive to `range.endInclusive` inclusive. + * + * @throws IllegalArgumentException if [range] is empty + */ +@SinceKotlin("1.3") +public fun Random.nextULong(range: ULongRange): ULong = when { + range.isEmpty() -> throw IllegalArgumentException("Cannot get random in empty range: $range") + range.last < ULong.MAX_VALUE -> nextULong(range.first, range.last + 1.toULong()) + range.first > ULong.MIN_VALUE -> nextULong(range.first, range.last + 1.toULong()) + else -> nextULong() +} + +/** + * Fills the specified byte [array] with random bytes and returns it. + * + * @return [array] filled with random bytes. + */ +@SinceKotlin("1.3") +public fun Random.nextUBytes(array: UByteArray) = nextUBytes(array, 0, array.size) + +/** + * Creates a byte array of the specified [size], filled with random bytes. + */ +@SinceKotlin("1.3") +public fun Random.nextUBytes(size: Int): UByteArray = UByteArray(nextBytes(size)) + +/** + * Fills a subrange of the specified UByte [array] starting from [fromIndex] inclusive and ending [toIndex] exclusive with random UBytes + * + * @returns [array] with the subrange filled with random bytes. + */ +@SinceKotlin("1.3") +public fun Random.nextUBytes(array: UByteArray, fromIndex: Int = 0, toIndex: Int = array.size): UByteArray { + return UByteArray(nextBytes(array.storage, fromIndex, toIndex)) +} + internal expect fun defaultPlatformRandom(): Random internal expect fun fastLog2(value: Int): Int // 31 - Integer.numberOfLeadingZeros(value) internal expect fun doubleFromParts(hi26: Int, low27: Int): Double @@ -330,7 +459,9 @@ internal fun Int.takeUpperBits(bitCount: Int): Int = this.ushr(32 - bitCount) and (-bitCount).shr(31) internal fun checkRangeBounds(origin: Int, bound: Int) = require(bound > origin) { boundsErrorMessage(origin, bound) } +internal fun checkUIntRangeBounds(origin: UInt, bound: UInt) = require(bound > origin) { boundsErrorMessage(origin, bound) } internal fun checkRangeBounds(origin: Long, bound: Long) = require(bound > origin) { boundsErrorMessage(origin, bound) } +internal fun checkULongRangeBounds(origin: ULong, bound: ULong) = require(bound > origin) { boundsErrorMessage(origin, bound) } internal fun checkRangeBounds(origin: Double, bound: Double) = require(bound > origin) { boundsErrorMessage(origin, bound) } private fun boundsErrorMessage(origin: Any, bound: Any) = "Random range is empty: [$origin, $bound)." diff --git a/libraries/stdlib/test/random/RandomTest.kt b/libraries/stdlib/test/random/RandomTest.kt index b83112865c0..9975e40e770 100644 --- a/libraries/stdlib/test/random/RandomTest.kt +++ b/libraries/stdlib/test/random/RandomTest.kt @@ -49,6 +49,20 @@ abstract class RandomSmokeTest { assertEquals(0, result2, "All zero bits should present") } + @Test + fun nextUInt() { + var result1 = UInt.MIN_VALUE + var result2 = UInt.MAX_VALUE + repeat(1000) { + val r = subject.nextUInt() + result1 = result1 or r + result2 = result2 and r + } + assertEquals(UInt.MAX_VALUE, result1, "All one bits should be present") + assertEquals(UInt.MIN_VALUE, result2, "All zero bits should present") + + } + @Test fun nextIntBound() { assertFailsWith { subject.nextInt(0) } @@ -68,6 +82,23 @@ abstract class RandomSmokeTest { } } + @Test + fun nextUIntBound() { + assertFailsWith { subject.nextUInt(UInt.MIN_VALUE) } + + repeat(1000) { + assertEquals(0u, subject.nextUInt(1u)) + } + + for (bound in listOf(2u, 3u, 7u, 16u, 32u, 0x4000_0000u, UInt.MAX_VALUE)) { + repeat(1000) { + val x = subject.nextUInt(bound) + if (x !in 0u..(bound - 1u)) + fail("Value $x must be in range [0, $bound)") + } + } + } + @Test fun nextIntOriginBound() { assertFailsWith { subject.nextInt(0, 0) } @@ -90,6 +121,28 @@ abstract class RandomSmokeTest { } } + @Test + fun nextUIntOriginBound() { + assertFailsWith { subject.nextUInt(0u, 0u) } + assertFailsWith { subject.nextUInt((-1).toUInt(), (-2).toUInt()) } + assertFailsWith { subject.nextUInt(UInt.MIN_VALUE, UInt.MIN_VALUE) } + + for (n in UInt.MIN_VALUE until UInt.MAX_VALUE step 0x10000) { + assertEquals(n, subject.nextUInt(n, n + 1u)) + } + (UInt.MAX_VALUE - 1u).let { n -> + assertEquals(n, subject.nextUInt(n, n + 1u)) + } + + for ((origin, bound) in listOf((0u to 2u), (1u to 6u), (0u to 32u), (0u to UInt.MAX_VALUE), (1u to (Int.MAX_VALUE.toUInt() + 1u)), (UInt.MIN_VALUE to UInt.MAX_VALUE))) { + repeat(1000) { + val x = subject.nextUInt(origin, bound) + if (x !in origin..bound - 1u) + fail("Value $x must be in range [$origin, $bound)") + } + } + } + @Suppress("EmptyRange") @Test fun nextIntInIntRange() { @@ -111,6 +164,32 @@ abstract class RandomSmokeTest { } } + @Test + fun nextUIntInUIntRange() { + assertFailsWith { subject.nextUInt(1u..0u) } + assertFailsWith { subject.nextUInt((-1).toUInt()..UInt.MIN_VALUE) } + assertFailsWith { subject.nextUInt(UInt.MAX_VALUE..(UInt.MAX_VALUE - 1u)) } + + repeat(1000) { it -> + val n = it.toUInt() + assertEquals(n, subject.nextUInt(n..n)) + } + + for (range in listOf( + (0u..1u), + (1u..5u), + (0u..31u), + (0u..UInt.MAX_VALUE - 1u), + (0u..UInt.MAX_VALUE) + )) { + repeat(1000) { + val x = subject.nextUInt(range) + if (x !in range) + fail("Value $x must be in range $range") + } + } + } + @Test fun nextLong() { var result1 = 0L @@ -124,6 +203,20 @@ abstract class RandomSmokeTest { assertEquals(0, result2, "All zero bits should present") } + @Test + fun nextULong() { + var result1 = ULong.MIN_VALUE + var result2 = ULong.MAX_VALUE + repeat(1000) { + val r = subject.nextULong() + result1 = result1 or r + result2 = result2 and r + } + + assertEquals(ULong.MAX_VALUE, result1, "All one bits should be present") + assertEquals(ULong.MIN_VALUE, result2, "All zero bits should be present") + } + @Test fun nextLongBound() { assertFailsWith { subject.nextLong(0) } @@ -143,6 +236,24 @@ abstract class RandomSmokeTest { } } + @Test + fun nextULongBound() { + assertFailsWith { subject.nextULong(ULong.MIN_VALUE) } + + repeat(1000) { + assertEquals(0uL, subject.nextULong(1uL)) + } + + for (bound in listOf(2uL, 23uL, 32uL, (0x1_0000_0000).toULong(), 0x4000_0000_0000_000.toULong(), ULong.MAX_VALUE)) { + repeat(1000) { + val x = subject.nextULong(bound) + if (x !in 0uL..(bound - 1uL)) { + fail("Value $x must be in range [0, $bound)") + } + } + } + } + @Test fun nextLongOriginBound() { assertFailsWith { subject.nextLong(0, 0) } @@ -164,6 +275,31 @@ abstract class RandomSmokeTest { } } + @Test + fun nextULongOriginBound() { + assertFailsWith { subject.nextULong(0uL, 0uL) } + assertFailsWith { subject.nextULong((-1).toULong(), (-2).toULong()) } + + for (i in 500uL..1500uL) { + val n = (0x1_0000_0000).toULong() + i + assertEquals(n, subject.nextULong(n, n + 1uL)) + } + + for ((origin, bound) in listOf( + (0uL to 32uL), + (1uL to 6uL), + (0uL to 0x1_0000_0000.toULong()), + (0uL to ULong.MAX_VALUE) + )) { + repeat(1000) { + val x = subject.nextULong(origin, bound) + if (x !in origin..(bound - 1uL)) { + fail("Value $x must be in range [$origin, $bound)") + } + } + } + } + @Suppress("EmptyRange") @Test fun nextLongInLongRange() { @@ -186,6 +322,35 @@ abstract class RandomSmokeTest { } } + @Test + fun nextULongInULongRange() { + assertFailsWith { subject.nextULong(1uL..0uL) } + assertFailsWith { subject.nextULong(ULong.MAX_VALUE..ULong.MIN_VALUE) } + assertFailsWith { subject.nextULong(ULong.MAX_VALUE..(ULong.MAX_VALUE - 1uL))} + + repeat(1000) { i -> + val n = (0x1_0000_0000).toULong() - 500uL + i.toULong() + assertEquals(n, subject.nextULong(n..n)) + + } + + for (range in listOf( + (0uL..1uL), + (1uL..5uL), + (0uL..31uL), + (0uL..(ULong.MAX_VALUE - 1uL)), + (0uL..ULong.MAX_VALUE) + )) { + repeat(1000) { + val x = subject.nextULong(range) + if (x !in range) { + fail("Value $x must be in range $range") + } + } + } + + } + @Test fun nextDouble() { @@ -291,6 +456,18 @@ abstract class RandomSmokeTest { assertFalse(bytes1 contentEquals bytes2) } + @Test + fun nextUBytes() { + val size = 20 + val ubytes1 = subject.nextUBytes(size) + assertEquals(size, ubytes1.size) + assertTrue(ubytes1.any { it != 0.toUByte() }) + + val ubytes2 = subject.nextUBytes(UByteArray(size)) + assertEquals(size, ubytes2.size) + assertTrue(ubytes2.any { it != 0.toUByte() }) + } + @Test fun nextBytesRange() { val size = 100 @@ -321,7 +498,7 @@ abstract class RandomSmokeTest { if (noChanges) { fail("Something should have changed in array after subrange [$from, $to) randomization (${1 + retries} times): " + - array.copyOfRange(from, to).contentToString()) + array.copyOfRange(from, to).contentToString()) } for (p in 0 until from) { @@ -332,6 +509,53 @@ abstract class RandomSmokeTest { } } } + + @Test + fun nextUBytesRange() { + val size = 100 + val array = subject.nextUBytes(size) + + assertFailsWith { subject.nextUBytes(array, -1, 10) } + assertFailsWith { subject.nextUBytes(array, 0, size + 10) } + assertFailsWith { subject.nextUBytes(array, 10, 0) } + + repeat(10000) { + val from = subject.nextInt(0, size - 1) + val to = subject.nextInt(from + 1, size) + + val prev = array.copyOf() + + subject.nextUBytes(array, from, to) + + var noChanges = array contentEquals prev + val rangeSize = to - from + + val retries = 4 / rangeSize + + var n = 0 + while(noChanges && n < retries) { + // there's a small chance that a small range will get the same value as before + // run randomization again + subject.nextUBytes(array, from, to) + noChanges = array contentEquals prev + n++ + } + + if(noChanges) { + fail("Something should have changed in array after subrange [$from, $to) randomization (${1 + retries} times: " + + array.copyOfRange(from, to).contentToString()) + } + + for (p in 0 until from) { + assertEquals(prev[p], array[p]) + } + + for (p in to until size) { + assertEquals(prev[p], array[p]) + } + + } + } } diff --git a/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt b/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt index 290fd59a098..35f7eefdf70 100644 --- a/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt +++ b/libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt @@ -3894,6 +3894,18 @@ public final class kotlin/random/Random$Companion : kotlin/random/Random { public final class kotlin/random/RandomKt { public static final fun Random (I)Lkotlin/random/Random; public static final fun Random (J)Lkotlin/random/Random; + public static final fun nextUBytes (Lkotlin/random/Random;I)[B + public static final fun nextUBytes-4zielkbi (Lkotlin/random/Random;[BII)[B + public static synthetic fun nextUBytes-4zielkbi$default (Lkotlin/random/Random;[BIIILjava/lang/Object;)[B + public static final fun nextUBytes-y7z55vk (Lkotlin/random/Random;[B)[B + public static final fun nextUInt (Lkotlin/random/Random;)I + public static final fun nextUInt (Lkotlin/random/Random;Lkotlin/ranges/UIntRange;)I + public static final fun nextUInt-5wlsgfq1 (Lkotlin/random/Random;II)I + public static final fun nextUInt-97rx7kg5 (Lkotlin/random/Random;I)I + public static final fun nextULong (Lkotlin/random/Random;)J + public static final fun nextULong (Lkotlin/random/Random;Lkotlin/ranges/ULongRange;)J + public static final fun nextULong-4sbioyd2 (Lkotlin/random/Random;J)J + public static final fun nextULong-7szwbobr (Lkotlin/random/Random;JJ)J } public class kotlin/ranges/CharProgression : java/lang/Iterable, kotlin/jvm/internal/markers/KMappedMarker {