How can I generate large, ranged random numbers in Swift?
Asked Answered
P

1

1

I'm looking for an efficient method of generating large numbers (that includes floating point types!) in Swift, with arbitrary ranges (which may even be UInt.max or Int.max)

All the existing questions I've seen either crash for large values (UInt.max) or don't support ranges. I know that you can read from /dev/urandom for random bytes, but that doesn't help restrict these values to a given interval (and I'm pretty sure looping until it does isn't efficient).

Persuader answered 13/7, 2015 at 19:14 Comment(5)
Not a complete answer to your question, but here are solutions for 64-bit random numbers: #26550330Inanity
@MartinR: I've seen that question. How does your answer account for minimum/maximum limits?Persuader
Oops! I just got it, ignore my question.Persuader
Please post your answer here for future visitors.Patience
@ndmeiri: I was referring to my question in the comments.Persuader
I
4

Here is a possible solution for UInt, Int and Double which works with the full range of those types. It is written as extension methods (now updated for Swift 2), but the same can be done with global functions.

Note that arc4random_uniform() produces 32-bit numbers only, so that can not be used if Int/UInt are 64-bit integers (which is the case for all OS X machines and all newer iOS devices).

For UInt we use the technique from https://mcmap.net/q/139094/-swift-random-number-for-64-bit-integers (which is just a Swift translation of https://mcmap.net/q/15753/-why-do-people-say-there-is-modulo-bias-when-using-a-random-number-generator). The case where the range covers the full range of UInt is treated separately.

extension UInt {
    static func random(minValue minValue : UInt, maxValue : UInt) -> UInt {
        precondition(minValue <= maxValue, "attempt to call random() with minValue > maxValue")

        if minValue == UInt.min && maxValue == UInt.max {
            // Random number in the full range of UInt:

            var rnd : UInt = 0
            arc4random_buf(&rnd, sizeofValue(rnd))
            return rnd
        } else {
            // Compute random number in the range 0 ... (maxValue-minValue),
            // using the technique from 
            // https://mcmap.net/q/139094/-swift-random-number-for-64-bit-integers, https://mcmap.net/q/15753/-why-do-people-say-there-is-modulo-bias-when-using-a-random-number-generator
            // and avoiding the "modulo bias problem":

            let range = maxValue - minValue + 1
            let randLimit = UInt.max - UInt.max % range
            var rnd : UInt = 0
            repeat {
                arc4random_buf(&rnd, sizeofValue(rnd))
            } while rnd >= randLimit
            rnd = rnd % range

            // Transform `rnd` back to the range minValue ... maxValue:
            return minValue + rnd
        }
    }
}

Examples:

let u1 = UInt.random(minValue: 1000, maxValue: 2000)
let u2 = UInt.random(minValue: UInt.min, maxValue: UInt.max)

The case of signed integers can be reduced to the unsigned case using the overflow operators and the bitPattern: conversion:

extension Int {
    static func random(minValue minValue : Int, maxValue : Int) -> Int {
        precondition(minValue <= maxValue, "attempt to call random() with minValue > maxValue")

        // Compute unsigned random number in the range 0 ... (maxValue-minValue):
        let diff = UInt(bitPattern: maxValue &- minValue)
        let rnd = UInt.random(minValue: 0, maxValue: diff)

        // Transform `rnd` back to the range minValue ... maxValue:
        return minValue &+ Int(bitPattern: rnd)
    }
}

Examples:

let i1 = Int.random(minValue: -1000, maxValue: 1000)
let i2 = Int.random(minValue: Int.min, maxValue: Int.max)

Finally, a straight-forward implementation for Double:

extension Double {
    static func random(minValue minValue : Double, maxValue : Double) -> Double {
        precondition(minValue <= maxValue, "attempt to call random() with minValue > maxValue")

        // Random floating point number in the range 0.0 ... 1.0:
        let rnd = Double(UInt.random(minValue: 0, maxValue: UInt.max))/Double(UInt.max)

        // Scale to range minValue ... maxValue:
        return minValue + rnd * (maxValue - minValue)
    }
}

Example:

let d = Double.random(minValue: 10.5, maxValue: 123.5)

Update for Swift 3:

extension UInt {
    static func random(minValue: UInt, maxValue: UInt) -> UInt {
        precondition(minValue <= maxValue, "attempt to call random() with minValue > maxValue")

        if minValue == UInt.min && maxValue == UInt.max {
            // Random number in the full range of UInt:

            var rnd: UInt = 0
            arc4random_buf(&rnd, MemoryLayout.size(ofValue: rnd))
            return rnd
        } else {
            // Compute random number in the range 0 ... (maxValue-minValue),
            // using the technique from 
            // https://mcmap.net/q/139094/-swift-random-number-for-64-bit-integers, https://mcmap.net/q/15753/-why-do-people-say-there-is-modulo-bias-when-using-a-random-number-generator
            // and avoiding the "modulo bias problem":

            let range = maxValue - minValue + 1
            let randLimit = UInt.max - UInt.max % range
            var rnd: UInt = 0
            repeat {
                arc4random_buf(&rnd, MemoryLayout.size(ofValue: rnd))
            } while rnd >= randLimit
            rnd = rnd % range

            // Transform `rnd` back to the range minValue ... maxValue:
            return minValue + rnd
        }
    }
}

extension Int {
    static func random(minValue: Int, maxValue: Int) -> Int {
        precondition(minValue <= maxValue, "attempt to call random() with minValue > maxValue")

        // Compute unsigned random number in the range 0 ... (maxValue-minValue):
        let diff = UInt(bitPattern: maxValue &- minValue)
        let rnd = UInt.random(minValue: 0, maxValue: diff)

        // Transform `rnd` back to the range minValue ... maxValue:
        return minValue &+ Int(bitPattern: rnd)
    }
}

extension Double {
    static func random(minValue: Double, maxValue: Double) -> Double {
        precondition(minValue <= maxValue, "attempt to call random() with minValue > maxValue")

        // Random floating point number in the range 0.0 ... 1.0:
        let rnd = Double(UInt.random(minValue: 0, maxValue: UInt.max))/Double(UInt.max)

        // Scale to range minValue ... maxValue:
        return minValue + rnd * (maxValue - minValue)
    }
}
Inanity answered 15/7, 2015 at 12:8 Comment(3)
A big thank you from me! I just implemented this, and I have to say that the performance is fantastic.Persuader
Thank you, @MartinR. I suppose you mean repeat { ... } while instead of until in the Swift 2 version?Towhead
@Stefan: Yes, thanks. I have now replaced the Swift 1.2 version by Swift 2 to avoid confusion, hopefully it is OK now.Inanity

© 2022 - 2024 — McMap. All rights reserved.