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:
let 📚1: String = "0"..."9" + "a"..."z"
Adjacent operators are in non-associative precedence group 'RangeFormationPrecedence'
let 📚2: String = ("0"..."9") + ("a"..."z")
Binary operator '+' cannot be applied to two '
ClosedRange<String>' operands
let 📚3: String = String("0"..."9") + String("a"..."z")
Cannot invoke initializer for type 'String' with an argument list of type '
(ClosedRange<String>)'
let 📚4: String = (Character("0")...Character("9")) + (Character("a")...Character("z"))
Binary operator '+' cannot be applied to two '
ClosedRange<Character>' operands
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>)'
Initializing an Empty String To create an empty String value as the starting point for building a longer string, either assign an empty string literal to a variable, or initialize a new String instance with initializer syntax: var emptyString = "" // empty string literal.
range() function only works with the integers i.e. whole numbers. All arguments must be integers. Users can not pass a string or float number or any other type in a start, stop and step argument of a range().
In Swift character is represented by Character type. In its literal form a character should be enclosed in double quote (string literal), and it should be declared explicitly with the Character keyword.
In Swift, there are three types of range: Closed Range. Half-Open Range. One-Sided Range.
"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) // αβγδεζηθικλμνξοπρςστυφχψω
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
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.
We can do:
let alphaRange = ("a" as UnicodeScalar).value...("z" as UnicodeScalar).value
let alpha📚 = String(String.UnicodeScalarView(alphaRange.map { UnicodeScalar($0)! }))
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"))
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.
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"]
Expanding on that if you really want to accomplish that syntax you would need to implement the closed range and half open range operators with a custom precedence group higher than AdditionPrecedence:
precedencegroup RangePrecedence {
associativity: none
higherThan: AdditionPrecedence
}
infix operator ..< : RangePrecedence
func ..< (lhs: Unicode.Scalar, rhs: Unicode.Scalar) -> String {
(lhs..<rhs).string
}
infix operator ... : RangePrecedence
func ... (lhs: Unicode.Scalar, rhs: Unicode.Scalar) -> String {
(lhs...rhs).string
}
Usage:
let 📚1 = "0"..."9" + "a"..."z"
print("📚1", 📚1) // 📚1 0123456789abcdefghijklmnopqrstuvwxyz
A side effect would be that now you would need to explicitly set the resulting type if you want "0"..<"9" to result in a range:
let range: Range<String> = "a" ..< "z"
print("range:", range) // range: a..<z
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With