Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

like image 859
Cœur Avatar asked Apr 13 '18 03:04

Cœur


People also ask

How do you initialize a string in Swift?

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.

Does range work on strings?

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().

How do you initialize a character in Swift?

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.

How do you define a range in Swift?

In Swift, there are three types of range: Closed Range. Half-Open Range. One-Sided Range.


4 Answers

"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) // αβγδεζηθικλμνξοπρςστυφχψω
like image 124
Martin R Avatar answered Oct 24 '22 15:10

Martin R


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

like image 43
Mike Taverne Avatar answered Oct 24 '22 14:10

Mike Taverne


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.

like image 4
Cœur Avatar answered Oct 24 '22 15:10

Cœur


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
like image 1
Leo Dabus Avatar answered Oct 24 '22 14:10

Leo Dabus