Creating a Countableclosedrange<Character>

Creating a CountableClosedRange Character

As already said, because a Character can be made up of multiple unicode scalars, you cannot accurately determine how many different valid character representations lie between two arbitrary characters, and is therefore not a good candidate for conformance to Stridable.

One approach to your problem of simply wanting to print out the alphabet is to conform UnicodeScalar, rather than Character, to Stridable – allowing you to work with characters that are represented by a single unicode code point, and advance them based on that code point.

extension UnicodeScalar : Strideable {

public func distance(to other: UnicodeScalar) -> Int {
return Int(other.value) - Int(self.value)
}

/// Returns a UnicodeScalar where the value is advanced by n.
///
/// - precondition: self.value + n represents a valid unicode scalar.
///
public func advanced(by n: Int) -> UnicodeScalar {
let advancedValue = n + Int(self.value)
guard let advancedScalar = UnicodeScalar(advancedValue) else {
fatalError("\(String(advancedValue, radix: 16)) does not represent a valid unicode scalar value.")
}
return advancedScalar
}
}

Now you can form a CountableClosedRange<UnicodeScalar>, and can freely convert each individual element to a Character or String if desired:

("A"..."Z").forEach {

// You can freely convert scalar to a Character or String
print($0, Character($0), String($0))
}

// Convert CountableClosedRange<UnicodeScalar> to [Character]
let alphabetCharacters = ("A"..."Z").map {Character($0)}

Swift: generate an array of (Swift) characters

It's a little cumbersome to get the initial character code (i.e. 'a' in c / Obj-C) in Swift, but you can do it like this:

let aScalars = "a".unicodeScalars
let aCode = aScalars[aScalars.startIndex].value

let letters: [Character] = (0..<26).map {
i in Character(UnicodeScalar(aCode + i))
}

subscript' is unavailable: cannot subscript String with a CountableClosedRange Int , see the documentation comment for discussion

  1. If you want to use subscripts on Strings like "palindrome"[1..<3] and "palindrome"[1...3], use these extensions.

Swift 4

extension String {
subscript (bounds: CountableClosedRange<Int>) -> String {
let start = index(startIndex, offsetBy: bounds.lowerBound)
let end = index(startIndex, offsetBy: bounds.upperBound)
return String(self[start...end])
}

subscript (bounds: CountableRange<Int>) -> String {
let start = index(startIndex, offsetBy: bounds.lowerBound)
let end = index(startIndex, offsetBy: bounds.upperBound)
return String(self[start..<end])
}
}

Swift 3

For Swift 3 replace with return self[start...end] and return self[start..<end].


  1. Apple didn't build this into the Swift language because the definition of a 'character' depends on how the String is encoded. A character can be 8 to 64 bits, and the default is usually UTF-16. You can specify other String encodings in String.Index.

This is the documentation that Xcode error refers to.

More on String encodings like UTF-8 and UTF-16

Initialize a String from a range of Characters in Swift

"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) // αβγδεζηθικλμνξοπρςστυφχψω

How do you turn a string into a unicode family in Swift?

You can define a character map. Here's one to get you started.

let circledMap: [Character : Character] = ["A": ", "B": ", "C": ", "D": "] // The rest are left as an exercise 
let circledRes = String("abacab".uppercased().map { circledMap[$0] ?? $0 })
print(circledRes)

If your map contains mappings for both upper and lowercase letters then don't call uppercased.

Create whatever maps you want. Spend lots of time with the "Emoji & Symbols" viewer found on the Edit menu of every macOS program.

let invertedMap: [Character : Character] = ["a": "ɐ", "b": "q", "c": "ɔ", "d": "p", "e": "ǝ", "f": "ɟ", "g": "ƃ", "h": "ɥ"]

In a case like the circled letters, it would be nice to define a range where you can transform "A"..."Z" to "..."/code>.

That actually takes more code than I expected but the following does work:

extension String {
// A few sample ranges to get started
// NOTE: Make sure each mapping pair has the same number of characters or bad things may happen
static let circledLetters: [ClosedRange<UnicodeScalar> : ClosedRange<UnicodeScalar>] = ["A"..."Z" : "...", "a"..."z" : "..."]
static let boxedLetters: [ClosedRange<UnicodeScalar> : ClosedRange<UnicodeScalar>] = ["A"..."Z" : "...", "a"..."z" : "..."]
static let italicLetters: [ClosedRange<UnicodeScalar> : ClosedRange<UnicodeScalar>] = ["A"..."Z" : "...", "a"..."z" : "..."]

func transformed(using mapping: [ClosedRange<UnicodeScalar> : ClosedRange<UnicodeScalar>]) -> String {
let chars: [UnicodeScalar] = self.unicodeScalars.map { ch in
for transform in mapping {
// If the character is found in the range, convert it
if let offset = transform.key.firstIndex(of: ch) {
// Convert the offset from key range into an Int
let dist = transform.key.distance(from: transform.key.startIndex, to: offset)
// Build new index into value range
let newIndex = transform.value.index(transform.value.startIndex, offsetBy: dist)
// Get the mapped character
let newch = transform.value[newIndex]
return newch
}
}

// Not found in any of the mappings so return the original as-is
return ch
}

// Convert the final [UnicodeScalar] into a new String
var res = ""
res.unicodeScalars.append(contentsOf: chars)

return res
}
}

print("This works".transformed(using: String.circledLetters)) // br>

The above String extension also requires the following extension (thanks to this answer):

extension UnicodeScalar: Strideable {
public func distance(to other: UnicodeScalar) -> Int {
return Int(other.value) - Int(self.value)
}

public func advanced(by n: Int) -> UnicodeScalar {
let advancedValue = n + Int(self.value)
guard let advancedScalar = UnicodeScalar(advancedValue) else {
fatalError("\(String(advancedValue, radix: 16)) does not represent a valid unicode scalar value.")
}
return advancedScalar
}
}

How to create range in Swift?

Updated for Swift 4

Swift ranges are more complex than NSRange, and they didn't get any easier in Swift 3. If you want to try to understand the reasoning behind some of this complexity, read this and this. I'll just show you how to create them and when you might use them.

Closed Ranges: a...b

This range operator creates a Swift range which includes both element a and element b, even if b is the maximum possible value for a type (like Int.max). There are two different types of closed ranges: ClosedRange and CountableClosedRange.

1. ClosedRange

The elements of all ranges in Swift are comparable (ie, they conform to the Comparable protocol). That allows you to access the elements in the range from a collection. Here is an example:

let myRange: ClosedRange = 1...3

let myArray = ["a", "b", "c", "d", "e"]
myArray[myRange] // ["b", "c", "d"]

However, a ClosedRange is not countable (ie, it does not conform to the Sequence protocol). That means you can't iterate over the elements with a for loop. For that you need the CountableClosedRange.

2. CountableClosedRange

This is similar to the last one except now the range can also be iterated over.

let myRange: CountableClosedRange = 1...3

let myArray = ["a", "b", "c", "d", "e"]
myArray[myRange] // ["b", "c", "d"]

for index in myRange {
print(myArray[index])
}

Half-Open Ranges: a..<b

This range operator includes element a but not element b. Like above, there are two different types of half-open ranges: Range and CountableRange.

1. Range

As with ClosedRange, you can access the elements of a collection with a Range. Example:

let myRange: Range = 1..<3

let myArray = ["a", "b", "c", "d", "e"]
myArray[myRange] // ["b", "c"]

Again, though, you cannot iterate over a Range because it is only comparable, not stridable.

2. CountableRange

A CountableRange allows iteration.

let myRange: CountableRange = 1..<3

let myArray = ["a", "b", "c", "d", "e"]
myArray[myRange] // ["b", "c"]

for index in myRange {
print(myArray[index])
}

NSRange

You can (must) still use NSRange at times in Swift (when making attributed strings, for example), so it is helpful to know how to make one.

let myNSRange = NSRange(location: 3, length: 2)

Note that this is location and length, not start index and end index. The example here is similar in meaning to the Swift range 3..<5. However, since the types are different, they are not interchangeable.

Ranges with Strings

The ... and ..< range operators are a shorthand way of creating ranges. For example:

let myRange = 1..<3

The long hand way to create the same range would be

let myRange = CountableRange<Int>(uncheckedBounds: (lower: 1, upper: 3)) // 1..<3

You can see that the index type here is Int. That doesn't work for String, though, because Strings are made of Characters and not all characters are the same size. (Read this for more info.) An emoji like , for example, takes more space than the letter "b".

Problem with NSRange

Try experimenting with NSRange and an NSString with emoji and you'll see what I mean. Headache.

let myNSRange = NSRange(location: 1, length: 3)

let myNSString: NSString = "abcde"
myNSString.substring(with: myNSRange) // "bcd"

let myNSString2: NSString = "acde"
myNSString2.substring(with: myNSRange) // "c" Where is the "d"!?

The smiley face takes two UTF-16 code units to store, so it gives the unexpected result of not including the "d".

Swift Solution

Because of this, with Swift Strings you use Range<String.Index>, not Range<Int>. The String Index is calculated based on a particular string so that it knows if there are any emoji or extended grapheme clusters.

Example

var myString = "abcde"
let start = myString.index(myString.startIndex, offsetBy: 1)
let end = myString.index(myString.startIndex, offsetBy: 4)
let myRange = start..<end
myString[myRange] // "bcd"

myString = "acde"
let start2 = myString.index(myString.startIndex, offsetBy: 1)
let end2 = myString.index(myString.startIndex, offsetBy: 4)
let myRange2 = start2..<end2
myString[myRange2] // "cd"

One-sided Ranges: a... and ...b and ..<b

In Swift 4 things were simplified a bit. Whenever the starting or ending point of a range can be inferred, you can leave it off.

Int

You can use one-sided integer ranges to iterate over collections. Here are some examples from the documentation.

// iterate from index 2 to the end of the array
for name in names[2...] {
print(name)
}

// iterate from the beginning of the array to index 2
for name in names[...2] {
print(name)
}

// iterate from the beginning of the array up to but not including index 2
for name in names[..<2] {
print(name)
}

// the range from negative infinity to 5. You can't iterate forward
// over this because the starting point in unknown.
let range = ...5
range.contains(7) // false
range.contains(4) // true
range.contains(-1) // true

// You can iterate over this but it will be an infinate loop
// so you have to break out at some point.
let range = 5...

String

This also works with String ranges. If you are making a range with str.startIndex or str.endIndex at one end, you can leave it off. The compiler will infer it.

Given

var str = "Hello, playground"
let index = str.index(str.startIndex, offsetBy: 5)

let myRange = ..<index // Hello

You can go from the index to str.endIndex by using ...

var str = "Hello, playground"
let index = str.index(str.endIndex, offsetBy: -10)
let myRange = index... // playground

See also:

  • How does String.Index work in Swift
  • How does String substring work in Swift

Notes

  • You can't use a range you created with one string on a different string.
  • As you can see, String ranges are a pain in Swift, but they do make it possibly to deal better with emoji and other Unicode scalars.

Further Study

  • String Range examples
  • Range Operator documentation
  • Strings and Characters documentation

how to overload an assignment operator for Range Unicode.Scalar to String in swift

Rather than using ExpressibleByXXXLiteral, you can just declare your own ... operator that returns a String.

This code from your second link makes ClosedRange<UnicodeScalar> conform to Sequence, which allows us to initialise a string from a ClosedRange<UnicodeScalar>:

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 String {
init<S: Sequence>(_ sequence: S) where S.Element == Unicode.Scalar {
self.init(UnicodeScalarView(sequence))
}
}

extension Sequence where Element == Unicode.Scalar {
var string: String { return String(self) }
}

Using that, our string-producing ... operator is simple to define:

func ...(lhs: UnicodeScalar, rhs: UnicodeScalar) -> String {
// note that this is not recursive
// this calls the "..." that produces a closed range
(lhs...rhs).string
}

Usage:

let alphabet = "a"..."z"
print(alphabet)

ClosedInterval String to [String] in Swift

Int conforms to ForwardIndexType, therefore in

let i = 1...100

... is the operator

public func ...<Pos : ForwardIndexType where Pos : Comparable>(start: Pos, end: Pos) -> Range<Pos>

and the result is a Range<Int>. Ranges are collections and sequences:
One can enumerate the elements of the range and create an array from it.

But String and Character do not conform to ForwardIndexType:
There is no method to advance from one string or character to the next.
Therefore in

let i = "a"..."z"

... is the operator

public func ...<Bound : Comparable>(start: Bound, end: Bound) -> ClosedInterval<Bound>

and the result is a ClosedInterval<String>. You can check if a
particular string is contained in that interval, but you can not
enumerate its elements.

"a"..."z" represents all strings s for which "a" <= s <= "z"
according to the Unicode standard, and not just the 26 lowercase letters
from the english alphabet, compare e.g. What does it mean that string and character comparisons in Swift are not locale-sensitive?. For example

let c = i.contains("ä")

yields true.

In Swift 3, the range and interval types have been renamed and
reorganized:

"1" ... "100"  // CountableClosedRange<Int>
"a" ... "z" // ClosedRange<String>

If your intention is to create an array with all characters from
"a" to "z" according to their ordering in the Unicode tables then
you can enumerate the UTF-32 code units:

let i = UnicodeScalar("a").value ... UnicodeScalar("z").value
let charArray = (i.map { Character(UnicodeScalar($0)) })
// or
let stringArray = (i.map { String(UnicodeScalar($0)) })

The result is

["a", "b", "c", ... , "z"]

as an array of Character or an array of String.



Related Topics



Leave a reply



Submit