Is there a clean way to specify character literals in Swift?

Alex Clark picture Alex Clark · Sep 29, 2014 · Viewed 12.5k times · Source

Swift seems to be trying to deprecate the notion of a string being composed of an array of atomic characters, which makes sense for many uses, but there's an awful lot of programming that involves picking through datastructures that are ASCII for all practical purposes: particularly with file I/O. The absence of a built in language feature to specify a character literal seems like a gaping hole, i.e. there is no analog of the C/Java/etc-esque:

String foo="a"
char bar='a'

This is rather inconvenient, because even if you convert your strings into arrays of characters, you can't do things like:

let ch:unichar = arrayOfCharacters[n]
if ch >= 'a' && ch <= 'z' {...whatever...}

One rather hacky workaround is to do something like this:

let LOWCASE_A = ("a" as NSString).characterAtIndex(0)
let LOWCASE_Z = ("z" as NSString).characterAtIndex(0)
if ch >= LOWCASE_A && ch <= LOWCASE_Z {...whatever...}

This works, but obviously it's pretty ugly. Does anyone have a better way?

Answer

Mike S picture Mike S · Sep 29, 2014

Characters can be created from Strings as long as those Strings are only made up of a single character. And, since Character implements ExtendedGraphemeClusterLiteralConvertible, Swift will do this for you automatically on assignment. So, to create a Character in Swift, you can simply do something like:

let ch: Character = "a"

Then, you can use the contains method of an IntervalType (generated with the Range operators) to check if a character is within the range you're looking for:

if ("a"..."z").contains(ch) {
    /* ... whatever ... */
}

Example:

let ch: Character = "m"
if ("a"..."z").contains(ch) {
    println("yep")
} else {
    println("nope")
}

Outputs:

yep


Update: As @MartinR pointed out, the ordering of Swift characters is based on Unicode Normalization Form D which is not in the same order as ASCII character codes. In your specific case, there are more characters between a and z than in straight ASCII (ä for example). See @MartinR's answer here for more info.

If you need to check if a character is in between two ASCII character codes, then you may need to do something like your original workaround. However, you'll also have to convert ch to an unichar and not a Character for it to work (see this question for more info on Character vs unichar):

let a_code = ("a" as NSString).characterAtIndex(0)
let z_code = ("z" as NSString).characterAtIndex(0)
let ch_code = (String(ch) as NSString).characterAtIndex(0)

if (a_code...z_code).contains(ch_code) {
    println("yep")
} else {
    println("nope")
}

Or, the even more verbose way without using NSString:

let startCharScalars = "a".unicodeScalars
let startCode = startCharScalars[startCharScalars.startIndex]

let endCharScalars = "z".unicodeScalars
let endCode = endCharScalars[endCharScalars.startIndex]

let chScalars = String(ch).unicodeScalars
let chCode = chScalars[chScalars.startIndex]

if (startCode...endCode).contains(chCode) {
    println("yep")
} else {
    println("nope")
}

Note: Both of those examples only work if the character only contains a single code point, but, as long as we're limited to ASCII, that shouldn't be a problem.