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
This commit is contained in:
Kerooker
2018-08-22 09:43:39 -03:00
committed by Ilya Gorbunov
parent db131df751
commit f9dec5e794
3 changed files with 369 additions and 2 deletions
+132 -1
View File
@@ -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)."
+225 -1
View File
@@ -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<IllegalArgumentException> { subject.nextInt(0) }
@@ -68,6 +82,23 @@ abstract class RandomSmokeTest {
}
}
@Test
fun nextUIntBound() {
assertFailsWith<IllegalArgumentException> { 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<IllegalArgumentException> { subject.nextInt(0, 0) }
@@ -90,6 +121,28 @@ abstract class RandomSmokeTest {
}
}
@Test
fun nextUIntOriginBound() {
assertFailsWith<IllegalArgumentException> { subject.nextUInt(0u, 0u) }
assertFailsWith<IllegalArgumentException> { subject.nextUInt((-1).toUInt(), (-2).toUInt()) }
assertFailsWith<IllegalArgumentException> { 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<IllegalArgumentException> { subject.nextUInt(1u..0u) }
assertFailsWith<IllegalArgumentException> { subject.nextUInt((-1).toUInt()..UInt.MIN_VALUE) }
assertFailsWith<IllegalArgumentException> { 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<IllegalArgumentException> { subject.nextLong(0) }
@@ -143,6 +236,24 @@ abstract class RandomSmokeTest {
}
}
@Test
fun nextULongBound() {
assertFailsWith<IllegalArgumentException> { 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<IllegalArgumentException> { subject.nextLong(0, 0) }
@@ -164,6 +275,31 @@ abstract class RandomSmokeTest {
}
}
@Test
fun nextULongOriginBound() {
assertFailsWith<IllegalArgumentException> { subject.nextULong(0uL, 0uL) }
assertFailsWith<IllegalArgumentException> { 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<IllegalArgumentException> { subject.nextULong(1uL..0uL) }
assertFailsWith<IllegalArgumentException> { subject.nextULong(ULong.MAX_VALUE..ULong.MIN_VALUE) }
assertFailsWith<IllegalArgumentException> { 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<IllegalArgumentException> { subject.nextUBytes(array, -1, 10) }
assertFailsWith<IllegalArgumentException> { subject.nextUBytes(array, 0, size + 10) }
assertFailsWith<IllegalArgumentException> { 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])
}
}
}
}
@@ -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 {