7
\$\begingroup\$

In the process of learning Swift, I'm building a lotto numbers generator that generates a random number and appends it into an array, if the number already exists then it should re-generate a random numbers and append that one to the array.

I've come up with this. I want to be able to have a reusable class where I can decide how many numbers I want it to generate.

How would you guys solve this in a simpler way?

class EuroMillions {

    var randomNumber:Int = 0
    var numberArray = [Int]()

    func generateRandomNumber (numbers: Int, repetitions: Int) -> (Int, Int, Int) {

    for number in 1...6 {
        randomNumber = Int(arc4random_uniform(UInt32(numbers)))
        checkArray(numbers: numbers)
    }
        return (randomNumber, numbers, repetitions)

    }

func checkArray(numbers: Int) {
if numberArray.contains(randomNumber){
    //re-generate
    print("Number \(randomNumber) exists!")
    randomNumber = Int(arc4random_uniform(UInt32(numbers)))
    checkArray(numbers: numbers)

        print("Replacing it with \(randomNumber)")
    } else {
        numberArray.append(randomNumber)
    }
  }
 }
}

let firstNumbers = EuroMillions()
firstNumbers.generateRandomNumber(numbers: 50, repetitions: 6)
let finalNumbers = firstNumbers.numberArray
print(finalNumbers)
\$\endgroup\$
  • 2
    \$\begingroup\$ You might be interested in this other question, where the asker solves the same issue. \$\endgroup\$ – jacwah Nov 4 '16 at 17:49
5
\$\begingroup\$

Your code made me think of a few improvements I could made: the data workflow is not easy to follow and this complexity is not necessary for this task, and this is what will change in my version. Other than that, I'm just proposing general improvements.

Let's start by listing what we need:

  • a dedicated class

  • generate x times a random number

  • each random number must be unique

  • each random number has a max value

For this, I start by making a class template:

class EuroMillions {

    var numbers: [Int] = []

    func generateNumbers(repetitions: Int, maxValue: Int) -> [Int] {
        // here, generate a random number x times
        // then populate the 'numbers' array and return it
        return numbers
    }

}

We have what we need: parameters for how many random numbers to generate and a maximum value.

Now how do we populate the array?

First we need to ensure that the maxValue is more or equal the number of repetitions, otherwise the task is not achievable with unique random numbers:

func generateNumbers(repetitions: Int, maxValue: Int) -> [Int] {

    guard maxValue >= repetitions else {
        fatalError("maxValue must be >= repetitions")
    }

    // work to do here

    return numbers
}

We also need to actually generate a random number:

private func random(maxValue: Int) -> Int {
    return Int(arc4random_uniform(UInt32(maxValue + 1)))
}

This will generate a number between 0 and maxValue, the latter included. If you don't want the maxValue being inclusive, remove the + 1.

We also need to check if the generated number is not already in our array.

For this, I've chosen to use a repeat while loop inside the counting loop and to use contains for checking the array:

for _ in 1...repetitions {
    var n: Int
    repeat {
        n = random(maxValue: maxValue)
    } while numbers.contains(n)
    numbers.append(n)
}

We loop x number of times (number of iterations = number of numbers to get). Since we don't need the index we discard it with _.

We prepare an Int, and populate this Int with a random value in a while loop.

The repeat loop generates a number then checks, in the while, if this value is already in the array with .contains.

If the number is not in the array, we break out of the inner loop and add it. If already present we continue generating until we have one that isn't in the array.

Another security check is to set the array to empty before generating, just in case the user would use our method several consecutive times (we could also avoid that by returning the result directly instead of populating an array property, and we'll see that later).

Complete code:

class EuroMillions {

    var numbers: [Int] = []

    func generateNumbers(repetitions: Int, maxValue: Int) -> [Int] {
        guard maxValue >= repetitions else {
            fatalError("maxValue must be >= repetitions for the numbers to be unique")
        }

        numbers = []

        for _ in 1...repetitions {
            var n: Int
            repeat {
                n = random(maxValue: maxValue)
            } while numbers.contains(n)
            numbers.append(n)
        }

        return numbers
    }

    private func random(maxValue: Int) -> Int {
        return Int(arc4random_uniform(UInt32(maxValue + 1)))
    }

}

Usage:

let lottery = EuroMillions()
let numbers = lottery.generateNumbers(repetitions: 6, maxValue: 49)
print(numbers)

Possible result:

[7, 12, 0, 9, 24, 35]


This was my main answer to your question.

How could we improve this class a bit more, or have fun, or both?

I would add the possibility to choose different lotteries, for example, and have defaults for them (methods prepared with preset values).

We also should set access privileges to methods and properties.

And I also like to have a printable identifier for the class.

For the identifier I like to use CustomStringConvertible which makes the class printable by adding a description:

class EuroMillions: CustomStringConvertible {

    // ...

    public var description: String {
        // return something relevant here
    }

}

And for the different lottery modes I would use an enum:

public enum LotteryMode: String {
    case euroMillions = "Euro Millions"
    case lotto = "Lotto"
}

public class Lottery {

    private var mode: LotteryMode

    public init(mode: LotteryMode = .euroMillions) {
        self.mode = mode
    }

    public func generate() -> [Int] {
        switch mode {
        case .euroMillions:
            return generateNumbers(repetitions: 5, maxValue: 49)
        case .lotto:
            return generateNumbers(repetitions: 6, maxValue: 49)
        }
    }

    // ...

}

Now we make generateNumbers(repetitions:, maxValue:) private because in this version of the class we only use the new preset method generate(), but this is not mandatory, it's just an example.

I also make the array private so that it can't be tempered with from outside, and add a public computed property to get its contents again once generated (since the class keeps state).

Complete code:

public enum LotteryMode: String {
    case euroMillions = "Euro Millions"
    case lotto = "Lotto"
}

public class Lottery: CustomStringConvertible {

    private var numbers: [Int] = []

    private var mode: LotteryMode

    public init(mode: LotteryMode = .euroMillions) {
        self.mode = mode
    }

    public var results: [Int] {
        return numbers
    }

    public func generate() -> [Int] {
        switch mode {
        case .euroMillions:
            return generateNumbers(repetitions: 5, maxValue: 49)
        case .lotto:
            return generateNumbers(repetitions: 6, maxValue: 49)
        }
    }

    private func generateNumbers(repetitions: Int, maxValue: Int) -> [Int] {
        guard maxValue >= repetitions else {
            fatalError("maxValue must be >= repetitions")
        }

        numbers = []

        for _ in 1...repetitions {
            var n: Int
            repeat {
                n = random(maxValue: maxValue)
            } while numbers.contains(n)
            numbers.append(n)
        }

        return numbers
    }

    private func random(maxValue: Int) -> Int {
        return Int(arc4random_uniform(UInt32(maxValue + 1)))
    }

    public var description: String {
        if numbers.isEmpty {
            return "\(mode.rawValue) - No result"
        } else {
            let content = numbers.map { String($0) }.joined(separator: ", ")
            return "\(mode.rawValue) - Result: \(content)"
        }
    }

}

Usage examples:

let euros = Lottery() // or Lottery(mode: .euroMillions)
print(euros)
let eurosNumbers = euros.generate()
print(euros)
print(eurosNumbers)
print(euros.results)

let lotto = Lottery(mode: .lotto)
print(lotto)
let lottoNumbers = lotto.generate()
print(lotto)
print(lottoNumbers)
print(lotto.results)
| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Note: in Swift 4.2 there's new standard methods for shuffling and randomizing. \$\endgroup\$ – Eric Aya Oct 3 '18 at 9:03
2
\$\begingroup\$

If you compare this to lottery draws that pick numbered balls out of a basket, you're actually applying a variable probability on the decreasing number of remaining balls. Using this analogy, you can process random selection in an array of "remaining balls" and never have to worry about testing for duplication (which would bias the probabilities):

Here's an example of how this approach could work:

let count   = 6
var basket  = Array(1...50) // this could be a list of strings, or even objects

let randomBalls = (0..<count)
                  .map{basket.count - $0}
                  .map{Int(arc4random_uniform(UInt32($0)))}
                  .map{basket.remove(at:$0)}                  

It generates 6 indexes in the range corresponding to the number of remaining balls after each draw. The random indexes are then extracted from the array, removing the selected element each time.

This will perform exactly the same number of iterations as your count and never have to do any searches for already drawn balls. And, as a bonus, it will work with any type of elements you want to pick at random (e.g. BINGO balls, Card game, ...) and you can make additional draws from the remaining basket (like picking x number of cards from the top of a card deck).

| improve this answer | |
\$\endgroup\$
1
\$\begingroup\$

Something I'd consider is implementing the Sequence and IteratorProtocol protocol's because then you get a bunch of really cool Array and Sequence features.

The main requirement for this is to implement the a function func next() -> Element? that should returns the next element in a sequence, or nil if there are no more elements to add.

Here's an example of a generator that always returns the number '7'.

public struct OnlySevens: Sequence, IteratorProtocol {
    public mutating func next() -> Int? {
        return 7
    }
}

Using this, you can use the prefix(_ maxLength: Int) function to generate a bunch of numbers from that sequence:

var sevens = OnlySevens()
let results = Array(sevens.prefix(100))
// -> [7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7,7...] (you get the idea... 100 7's)
results.count
// -> 100

Building on top of that you can build a pretty robust unique random number generator:

public struct UniqueRandomGenerator: Sequence, IteratorProtocol {

    private let ceiling: Int
    private var _existing: Set<Int>

    /// ceiling is the highest number that can be drawn
    init(ceiling: Int) {
        self.ceiling = ceiling
        self._existing = Set<Int>()
    }

    // generate random number between 0..<ceiling
    private var randomPick: Int {
        return Int(arc4random_uniform(UInt32(ceiling)))
    }

    public mutating func next() -> Int? {
        // if we have a ceiling of 10, then it's impossible to 
        // more than 10 unique numbers
        guard _existing.count < ceiling else {
            return nil
        }

        // continue getting a random pick until we find one 
        // that hasn't been picked yet
        var pick: Int
        repeat {
            pick = randomPick
        } while _existing.contains(pick)

        // insert into existing picks so it won't be picked again
        _existing.insert(pick)

        // return unique pick
        return pick
    }
}

Usage:

// will generate unique random numbers between 0..<100
var lottoMaker = UniqueRandomGenerator(ceiling: 100)

// fill up an array with at most 10 results
let results = Array(lottoMaker.prefix(10))
// -> [52, 60, 30, 49, 11, 26, 58, 15, 50, 94] 

Another way to think about UniqueRandomGenerator is to think of it collection of numbers between 0 and the ceiling that you pick off one at a time until there are none remaining.

// will generate unique random numbers between 0..<10
var random10 = UniqueRandomGenerator(ceiling: 10)

for number in random10 {
    print(number)
}
// prints something like this: 
// 2 
// 7 
// 5 
// 8 
// 0 
// 9 
// 6 
// 1
// 3 
// 4
| improve this answer | |
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.