'Initialize a String from a range of Characters in Swift

In our code, we found a bug from not writing the alphabet correctly. Instead of "0123456789abcdefghijklmnopqrstuvwxyz", we had "0123456789abcdefghijklmnoqprstuvwxyz". So we are wondering if it's possible to avoid similar typo by declaring Strings made from ranges of characters?

Using Swift 4.1+, we tried:

attempt 1

let 📚1: String = "0"..."9" + "a"..."z"

Adjacent operators are in non-associative precedence group 'RangeFormationPrecedence'

attempt 2

let 📚2: String = ("0"..."9") + ("a"..."z")

Binary operator '+' cannot be applied to two 'ClosedRange<String>' operands

attempt 3

let 📚3: String = String("0"..."9") + String("a"..."z")

Cannot invoke initializer for type 'String' with an argument list of type '(ClosedRange<String>)'

attempt 4

let 📚4: String = (Character("0")...Character("9")) + (Character("a")...Character("z"))

Binary operator '+' cannot be applied to two 'ClosedRange<Character>' operands

attempt 5

let 📚5: String = String(Character("0")...Character("9")) + String(Character("a")...Character("z"))

Cannot invoke initializer for type 'String' with an argument list of type '(ClosedRange<Character>)'



Solution 1:[1]

"a"..."z" is a ClosedRange, but not a CountableClosedRange. It represents all strings s for which "a" <= s <= "z" according to the Unicode standard. That are not just the 26 lowercase letters from the english alphabet but many more, such as "ä", "è", "ô". (Compare also ClosedInterval<String> to [String] in Swift.)

In particular, "a"..."z" is not a Sequence, and that is why String("a"..."z") does not work.

What you can do is to create ranges of Unicode scalar values which are (UInt32) numbers (using the UInt32(_ v: Unicode.Scalar) initializer):

let letters = UInt32("a") ... UInt32("z")
let digits = UInt32("0") ... UInt32("9")

and then create a string with all Unicode scalar values in those (countable!) ranges:

let string = String(String.UnicodeScalarView(letters.compactMap(UnicodeScalar.init)))
    + String(String.UnicodeScalarView(digits.compactMap(UnicodeScalar.init)))

print(string) // abcdefghijklmnopqrstuvwxyz0123456789

(For Swift before 4.1, replace compactMap by flatMap.)

This works also for non-ASCII characters. Example:

let greekLetters = UInt32("?") ... UInt32("?")
let greekAlphabet = String(String.UnicodeScalarView(greekLetters.compactMap(UnicodeScalar.init)))
print(greekAlphabet) // ?????????????????????????

Solution 2:[2]

This isn't necessarily eloquent but it works:

let alphas = UInt8(ascii: "a")...UInt8(ascii: "z")
let digits = UInt8(ascii: "0")...UInt8(ascii: "9")

let ?6 =
      digits.reduce("") { $0 + String(Character(UnicodeScalar($1))) }
    + alphas.reduce("") { $0 + String(Character(UnicodeScalar($1))) }

print(?6) // "0123456789abcdefghijklmnopqrstuvwxyz"

Big assist from Ole Begemann: https://gist.github.com/ole/d5189f20840c52eb607d5cc531e08874

Solution 3:[3]

Unicode ranges will be supported by UInt32. Let's note that UnicodeScalar.init?(_ v: UInt32) will return a non-nil value when:

v is in the range 0...0xD7FF or 0xE000...0x10FFFF

As that's a pretty easy condition to fulfill, because at most we'll have two ranges to concatenate, we'll force unwrap values with ! and avoid undefined behavior.

To support ranges without an extension

We can do:

let alphaRange = ("a" as UnicodeScalar).value...("z" as UnicodeScalar).value
let alpha? = String(String.UnicodeScalarView(alphaRange.map { UnicodeScalar($0)! }))

To support ranges with an extension

If we make UnicodeScalar strideable, we can make the above more concise.

extension UnicodeScalar : Strideable {
    public func advanced(by n: Int) -> UnicodeScalar {
        return UnicodeScalar(UInt32(n) + value)!
    }
    public func distance(to other: UnicodeScalar) -> Int {
        return Int(other.value - value)
    }
}

And the solution simply becomes:

let alpha? = String(String.UnicodeScalarView(("a" as UnicodeScalar)..."z"))

For ASCII ranges only

We can restrict ourselves to UInt8 and we don't have to force unwrap values anymore, especially with UInt8.init(ascii v: Unicode.Scalar):

let alphaRange = UInt8(ascii: "a")...UInt8(ascii: "z")
let alpha? = String(String.UnicodeScalarView(alphaRange.map { UnicodeScalar($0) }))

or:

let alphaRange = UInt8(ascii: "a")...UInt8(ascii: "z")
let alpha? = String(data: Data(alphaRange), encoding: .utf8)!

Big thanks to Martin, Mike, jake.lange and Leo Dabus.

Solution 4:[4]

Putting the elements together I ended up with the following solution:

extension Unicode.Scalar: Strideable {
    public func advanced(by n: Int) -> Unicode.Scalar {
        let value = self.value.advanced(by: n)
        guard let scalar = Unicode.Scalar(value) else {
            fatalError("Invalid Unicode.Scalar value:" + String(value, radix: 16))
        }
        return scalar
    }
    public func distance(to other: Unicode.Scalar) -> Int {
        return Int(other.value - value)
    }
}

extension Sequence where Element == Unicode.Scalar {
     var string: String { return String(self) }
     var characters: [Character] { return map(Character.init) }
}

extension String {
    init<S: Sequence>(_ sequence: S) where S.Element == Unicode.Scalar {
        self.init(UnicodeScalarView(sequence))
    }
}

("a"..<"z").string  // "abcdefghijklmnopqrstuvwxy"
("a"..."z").string  // "abcdefghijklmnopqrstuvwxyz"

String("a"..<"z") // "abcdefghijklmnopqrstuvwxy"
String("a"..."z") // "abcdefghijklmnopqrstuvwxyz"

("a"..<"z").characters  // ["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"] 
("a"..."z").characters // ["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"]

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1
Solution 2 Mike Taverne
Solution 3
Solution 4