4

I am trying to extend Character to conform to Strideable in order to create a CountableClosedRange of Character types. In the end, I would like to have something like this which prints the whole alphabet:

("A"..."Z").forEach{
    print($0)
}

For the time being, I am using UnicodeScalar types to calculate the distance between two characters. Because a scalar isn't available from a Charactertype, I need to create a String from the Character, get the first scalar's value, and calculate the distance between them:

extension Character: Strideable {

    func distance(to other: Character) -> Character.Stride {
        return abs(String(self).unicodeScalars.first?.value - String(other).unicodeScalars.first!.value)
    }

    func advanced(by n: Character.Stride) -> Character {
        return Character(UnicodeScalar(String(self).unicodeScalars.first!.value + n))
    }

}

Even with this, I get the error that Character does not conform to protocol Strideable and _Strideable. The compiler does not appear to be picking up the Stride associated type which comes with Strideable:

public protocol Strideable : Comparable {

    /// A type that can represent the distance between two values of `Self`.
    associatedtype Stride : SignedNumber

    // ...

}

What am I missing?

JAL
  • 41,701
  • 23
  • 172
  • 300
  • 1
    You have to explicitly define the Character.Stride type (e.g. as `Int`), the Strideable protocol only requires that it *conforms* to SignedNumber. Also taking the absolute value is wrong. – But there are more problems. A Character describes an extended grapheme cluster and can consist of many Unicode scalars. As an example, with your approach, "" and "" would have distance zero. – Martin R Oct 11 '16 at 16:58
  • @MartinR this sounds like it's less and less worth the effort. I should probably just create an array of characters or one string with all of the letters and be done with it. – JAL Oct 11 '16 at 17:02
  • 2
    This might be interesting if you want to know how strings are compared, it applies to characters as well: http://stackoverflow.com/a/38910703/1187415. – Martin R Oct 11 '16 at 18:23

2 Answers2

6

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)}
Hamish
  • 78,605
  • 19
  • 187
  • 280
  • Thanks Hamish. Maybe I'm missing something but I just dumped your code into an Xcode 8 playground and I'm still seeing `Value of type 'ClosedRange' has no member 'forEach'`. And your `map` example crashes the compiler, haha. – JAL Oct 15 '16 at 14:15
  • @JAL Huh, weird. Compiles fine for me both in a playground and a full project. Although that being said, playgrounds are notoriously buggy, so I would definitely recommend trying it out in a full project. Although from what it sounds like, the compiler is just failing to interpret the string literals as `UnicodeScalar`s – adding an explicit type annotation should help, e.g `("A"..."Z").forEach {(scalar: UnicodeScalar) in ... }` – Hamish Oct 15 '16 at 14:33
  • Yeah, probably just a case of Xcode 8 crapping out. I'll give it another shot in a project. – JAL Oct 15 '16 at 14:34
  • @JAL using `(("A" as UnicodeScalar)..."Z")` will also work as type annotation. – Cœur Apr 13 '18 at 09:25
  • @LeoDabus I can't reproduce the above Xcode 8 Playground issue anymore. Maybe it got solved with the installation of newer tools (Xcode 9, Xcode 10, etc.) – Cœur Feb 08 '19 at 01:19
2

This wouldn't work the way you'd expect it to even if you could make it work. How many characters do you believe are between "A" and "Z"? Without defining your encoding, this isn't meaningful. In fact, if you explore how Characters conform to Comparable, they act more like floats than like integers. For example:

"N" < "Ń" // true
"Ń" < "Ñ" // true
"Ń" < "O" // true

Between N and O are many modifications on N, possibly an unbounded number given Unicode's ability to compose characters. (This is the same if you wrap these in Character().)

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Thank you for your answer. For the sake of simplicity what if I just chose ASCII encoding and defined distance as the value between characters on the table? – JAL Oct 11 '16 at 18:09
  • 1
    That's fine, but you shouldn't apply that to `Character`. You should convert your characters into ASCII (UInt8) and work with the resulting integers. That's of course a lossy conversion, which is why it should be explicit. – Rob Napier Oct 11 '16 at 18:11