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