29

I have an array like:

var names: String = [ "Peter", "Steve", "Max", "Sandra", "Roman", "Julia" ]

I would like to get 3 random elements from that array. I'm coming from C# but in swift I'm unsure where to start. I think I should shuffle the array first and then pick the first 3 items from it for example?

I tried to shuffle it with the following extension:

extension Array
{
    mutating func shuffle()
    {
        for _ in 0..<10
        {
            sort { (_,_) in arc4random() < arc4random() }
        }
    }
}

but it then says "'()' is not convertible to '[Int]'" at the location of "shuffle()".

For picking a number of elements I use:

var randomPicks = names[0..<4];

which looks good so far.

How to shuffle? Or does anyone have a better/more elegant solution for this?

Cœur
  • 37,241
  • 25
  • 195
  • 267
Patric
  • 2,789
  • 9
  • 33
  • 60
  • 5
    See http://stackoverflow.com/questions/24026510/how-do-i-shuffle-an-array-in-swift for a better shuffle method. – Martin R Dec 02 '14 at 21:33
  • 1
    Thanks, I used the mutating extension method of the accepted anaswer now for shuffling. – Patric Dec 03 '14 at 08:29
  • 2
    Yes, there are better/more elegant solutions: **a full shuffling is not optimal** as if you need 4 random elements out of 10, picking those one by one only costs 4 `arc4random_uniform`, but full shuffling costs 9 `arc4random_uniform`. – Cœur Jul 10 '17 at 07:45
  • Using `sort` to shuffle like that just doesn't work. Sorting intentionally does as little comparison as possible, and certainly not enough to achieve a decent shuffle. – Alexander Jul 11 '17 at 01:50

6 Answers6

63

Xcode 11 • Swift 5.1

extension Collection {
    func choose(_ n: Int) -> ArraySlice<Element> { shuffled().prefix(n) }
}

Playground testing

var alphabet = ["A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z"]
let shuffledAlphabet = alphabet.shuffled()  // "O", "X", "L", "D", "N", "K", "R", "E", "S", "Z", "I", "T", "H", "C", "U", "B", "W", "M", "Q", "Y", "V", "A", "G", "P", "F", "J"]
let letter = alphabet.randomElement()  // "D"
var numbers = Array(0...9)
let shuffledNumbers = numbers.shuffled()
shuffledNumbers                              // [8, 9, 3, 6, 0, 1, 4, 2, 5, 7]
numbers            // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
numbers.shuffle() // mutate it  [6, 0, 2, 3, 9, 1, 5, 7, 4, 8]
numbers            // [6, 0, 2, 3, 9, 1, 5, 7, 4, 8]
let pick3numbers = numbers.choose(3)  // [8, 9, 2]

extension RangeReplaceableCollection {
    /// Returns a new Collection shuffled
    var shuffled: Self { .init(shuffled()) }
    /// Shuffles this Collection in place
    @discardableResult
    mutating func shuffledInPlace() -> Self  {
        self = shuffled
        return self
    }
    func choose(_ n: Int) -> SubSequence { shuffled.prefix(n) }
}

var alphabetString = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
let shuffledAlphabetString = alphabetString.shuffled  // "DRGXNSJLFQHPUZTBKVMYAWEICO"
let character = alphabetString.randomElement()  // "K"
alphabetString.shuffledInPlace() // mutate it  "WYQVBLGZKPFUJTHOXERADMCINS"
alphabetString            // "WYQVBLGZKPFUJTHOXERADMCINS"
let pick3Characters = alphabetString.choose(3)  // "VYA"
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
  • When I try to use the indexRandom() method, it raises the compilation error saying find() is unavailable. Let me know in if this is any custom method you forgot to mention... I am using Xcode 7. – DShah Sep 25 '15 at 11:43
  • Thanks for the prompt response... But I think you removed the Int extension due to which indexRandom() method will give error. – DShah Sep 25 '15 at 12:27
  • 1
    Nice! I have to give it to you, you come up with some really good extensions. – Lance Samaria Apr 28 '19 at 03:23
  • Hi, does using this extension choose randomly from the Array. For example, I have an array of over 40+ heart rate reading that I want to make into only 40, but I don't want them to be out of order, so I can plot them in a graph, Can I use `var fortyReadings = heartratereading.choose(40)` to get 40 values in the order they were in? Or what is a way I can do that? Thanks in advance ~ Kurt – Kurt L. Nov 15 '19 at 08:45
  • I am not sure if I understood your question but looks like you just need to shuffle your elements once and them group the random by n (40) elements https://stackoverflow.com/a/48089097/2303865 – Leo Dabus Nov 17 '19 at 05:55
  • If you need to assign the result to an Array type or pass it to a method that expects an Array parameter you will need to force the result to an Array type `Array(yourArray.choose(3))` – Joannes Nov 21 '21 at 10:40
  • @Joannes Sure. This true for any collection subsequence. If you get a Substring and need a String you need to initialize a new one. – Leo Dabus Nov 21 '21 at 14:11
24

Or does anyone have a better/more elegant solution for this?

I do. Algorithmically better than the accepted answer, which does count-1 arc4random_uniform operations for a full shuffle, we can simply pick n values in n arc4random_uniform operations.

And actually, I got two ways of doing better than the accepted answer:

Better solution

extension Array {
    /// Picks `n` random elements (straightforward approach)
    subscript (randomPick n: Int) -> [Element] {
        var indices = [Int](0..<count)
        var randoms = [Int]()
        for _ in 0..<n {
            randoms.append(indices.remove(at: Int(arc4random_uniform(UInt32(indices.count)))))
        }
        return randoms.map { self[$0] }
    }
}

Best solution

The following solution is twice faster than previous one.

for Swift 3.0 and 3.1

extension Array {
    /// Picks `n` random elements (partial Fisher-Yates shuffle approach)
    subscript (randomPick n: Int) -> [Element] {
        var copy = self
        for i in stride(from: count - 1, to: count - n - 1, by: -1) {
            let j = Int(arc4random_uniform(UInt32(i + 1)))
            if j != i {
                swap(&copy[i], &copy[j])
            }
        }
        return Array(copy.suffix(n))
    }
}

for Swift 3.2 and 4.x

extension Array {
    /// Picks `n` random elements (partial Fisher-Yates shuffle approach)
    subscript (randomPick n: Int) -> [Element] {
        var copy = self
        for i in stride(from: count - 1, to: count - n - 1, by: -1) {
            copy.swapAt(i, Int(arc4random_uniform(UInt32(i + 1))))
        }
        return Array(copy.suffix(n))
    }
}

Usage:

let digits = Array(0...9)  // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
let pick3digits = digits[randomPick: 3]  // [8, 9, 0]
Cœur
  • 37,241
  • 25
  • 195
  • 267
  • You are very welcome, thank you for your algorithm ! Tried the fix, works like a charm. Also, I was convinced that I was running Swift 3.2 (Xcode 8.3.3) Swift version flag only lets me choose between "Swift 3" and "Unspecified". However, a quick swift --version in terminal prompted me with a nasty swift 3.1. Gotta find out how to force Swift 3.2 (Convert to latest swift version did not do the trick). But that's another issue. – H4Hugo Jul 15 '17 at 13:36
5

You could define an extension on Array:

extension Array {
    func pick(_ n: Int) -> [Element] {
        guard count >= n else {
            fatalError("The count has to be at least \(n)")
        }
        guard n >= 0 else {
            fatalError("The number of elements to be picked must be positive")
        }

        let shuffledIndices = indices.shuffled().prefix(upTo: n)
        return shuffledIndices.map {self[$0]}
    }
}

[ "Peter", "Steve", "Max", "Sandra", "Roman", "Julia" ].pick(3)

If the initial array may have duplicates, and you want uniqueness in value:

extension Array where Element: Hashable {
    func pickUniqueInValue(_ n: Int) -> [Element] {
        let set: Set<Element> = Set(self)
        guard set.count >= n else {
            fatalError("The array has to have at least \(n) unique values")
        }
        guard n >= 0 else {
            fatalError("The number of elements to be picked must be positive")
        }

        return Array(set.prefix(upTo: set.index(set.startIndex, offsetBy: n)))
    }
}

[ "Peter", "Steve", "Max", "Sandra", "Roman", "Julia" ].pickUniqueInValue(3)
ielyamani
  • 17,807
  • 10
  • 55
  • 90
  • Not programmer, nor user, friendly to crash, better to return nil if the array has fewer elements than requested. – Cristik Apr 24 '22 at 07:17
4

Swift 4.1 and below

let playlist = ["Nothing Else Matters", "Stairway to Heaven", "I Want to Break Free", "Yesterday"]
let index = Int(arc4random_uniform(UInt32(playlist.count)))
let song = playlist[index]

Swift 4.2 and above

if let song = playlist.randomElement() {
  print(song)
} else {
  print("Empty playlist.")
}
Prashant Gaikwad
  • 3,493
  • 1
  • 24
  • 26
3

You can use the shuffle() method and pick the first 3 items of the shuffled array to get 3 random elements from the original array:

Xcode 14 • Swift 5.7

    var names: String = [ "Peter", "Steve", "Max", "Sandra", "Roman", "Julia" ]
    let shuffledNameArray = names.shuffled()
    let randomNames = Array(shuffledNameArray.prefix(3))
    print(randomNames)
user8675
  • 657
  • 6
  • 13
1

You could also use arc4random() to just choose three elements from the Array. Something like this:

extension Array {
    func getRandomElements() -> (T, T, T) {
        return (self[Int(arc4random()) % Int(count)],
                self[Int(arc4random()) % Int(count)],
                self[Int(arc4random()) % Int(count)])
    }
}

let names = ["Peter", "Steve", "Max", "Sandra", "Roman", "Julia"]
names.getRandomElements()

This is just an example, you could also include logic in the function to get a different name for each one.

akshay
  • 184
  • 2
  • 9
  • 1
    (a) You'll get modulo bias there; you should be using arc4random_uniform; (b) I reckon this may crash about half the time on 32-bit architectures, as you're putting the value of a `UInt32` (the return from `arc4random()`) into a (signed, 32-bit) `Int`, resulting in a negative array index; (c) this might result in, for example, "Peter", "Steve", "Peter", as there's no code to avoid repeated picks of the same item. – Matt Gibson Dec 03 '14 at 08:14
  • 1
    Yeah, thanks for the answer, but this really doesn't makes sure that only elements are returned once (which I didn't state directly but is what I'm looking for) – Patric Dec 03 '14 at 08:31