'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 |