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:
@@ -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])
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user