How to get random element from CharacterSet in Swift

Viewed 154

I have an array of characters the user has already entered. I would like to get a random character that the user has not already entered from a combined list of all of the alphanumeric characters, the punctuation characters and the symbols. I have looked through the documentation but can't find a way to get a random element from a CharacterSet, which seems odd... I feel like I am missing something obvious.

func newRandomCharacter() -> Character {

    let validCharacters: CharacterSet = .alphanumerics.union(.punctuationCharacters).union(.symbols)

    var setOfUsedCharacters = CharacterSet()
    // usedCharacters: [Character]
    usedCharacters.forEach { setOfUsedCharacters.insert(charactersIn: String($0)) }

    let setOfUnusedCharacters = validCharacters.subtracting(setOfUsedCharacters)

    return setOfUnusedCharacters.randomElement() <- ???
}
1 Answers

A CharacterSet is not a Collection. It is a SetAlgebra.

It is not, despite its name, "a Set of Characters." It is a "set" of UnicodeScalars (not Characters) in the mathematical sense: a list of rules that define whether a given UnicodeScalar is contained. There is no direct way to enumerate the values. There are inefficient ways to generate a complete list since UnicodeScalar is over a finite range, but it's quite massive.

I'm curious how you would use this, though. This may include a lot of characters you're not expecting like UNDERTIE (‿), SAMARITAN PUNCTUATION BAU (࠳), and THAI CHARACTER FONGMAN (๏). Are you really looking to pick a random value out all Unicode alphanumerics and punctuation? (There are over 800 punctuation characters, for example, and by my rough count maybe 25k alphanumerics. I haven't counted symbols yet, but there are a lot of them. The chance that you'll get a character on a US keyboard is pretty close to zero.)

I expect this is the code you're really looking for:

let asciiRange = 33...126
let randomCharacter = asciiRange.randomElement()
    .flatMap(UnicodeScalar.init)
    .flatMap(Character.init)!

This will return a random, printable ASCII character.

Given that your set is such a large proportion of the Unicode space, the following is how you would get a truly random one reasonably quickly:

func randomCharacter() -> Character {
    // Drops the control characters and SPACE, the private use areas, tags, and the variation selectors.
    // The full range is 0x00...0x10FFFD
    let unicodeRange = 0x21...0x2FA1D
    let validCharacters: CharacterSet = .alphanumerics.union(.punctuationCharacters).union(.symbols)

    repeat {
        if let c = unicodeRange.randomElement().flatMap(UnicodeScalar.init),
           validCharacters.contains(c) {
            return Character(c)
        }
    } while true
}

I just keeps guessing until it finds one. This will tend to converge as long as the set you're picking from is similar in size to the full set. This is likely more efficient than generating a massive Set<Character> which you could do by walking a similar space.

Related