9.
Strings
Written by Matt Galloway
So far, you have briefly seen what the type String has to offer for representing text. Text is a ubiquitous data type: people’s names, addresses and the words of a book. These are examples of text that an app might need to handle. It’s worth having a deeper understanding of how String works and what it can do.
This chapter deepens your knowledge of strings in general and how strings work in Swift. Swift is one of the few languages that handle Unicode characters correctly while maintaining maximum predictable performance.
Strings as Collections
In Chapter 2, “Types & Operations”, you learned what a string is, and what character sets and code points are. To recap, they define the mapping numbers to the character it represents. And now, it’s time to look deeper into the String type.
It’s pretty easy to conceptualize a string as a collection of characters. Because strings are collections, you can do things like this:
let string = "Matt"
for char in string {
print(char)
}
This code will print out every character of Matt individually. Simple, eh?
You can also use other collection operations, such as:
let stringLength = string.count
This assignment will give you the length of the string.
Now imagine you want to get the fourth character in the string. You may think of doing something like this:
let fourthChar = string[3]
However, if you did this, you would receive the following error message:
'subscript' is unavailable: cannot subscript String with an Int, see the documentation comment for discussion
Why is that? The short answer is that characters do not have a fixed size, so you can’t access them like an array. Why not? It’s time to take a detour further into how strings work by introducing what a grapheme cluster is.
Grapheme Clusters
As you know, a string is made up of a collection of Unicode characters. Until now, you have considered one code point to precisely equal one character and vice versa. However, the term “character” is relatively loose.
It may come as a surprise, but there are two ways to represent some characters. One example is the é in café, an e with an acute accent. You can represent this character with either one or two characters.
The single character to represent this is code point 233. The two-character case is an e on its own, followed by an acute accent combining character, a special character that modifies the previous character.
So you can represent the e with an acute accent by either of these means:
The combination of these two characters in the second diagram forms what is known as a grapheme cluster defined by the Unicode standard. When you think of a character, you’re probably thinking of a grapheme cluster. Grapheme clusters are represented by the Swift type Character.
Other examples of combining characters are the special characters used to change the skin color of certain emojis.
Here, the thumbs-up emoji is followed by a skin tone-combining character. On platforms that support it, including iOS and macOS, the rendered emoji is a single thumbs-up character with the skin tone applied.
Now, look at what this means for strings when they are used as collections. Consider the following code:
let cafeNormal = "café"
let cafeCombining = "cafe\u{0301}"
cafeNormal.count // 4
cafeCombining.count // 4
Both counts equal four because Swift considers a string a collection of grapheme clusters. Also, evaluating the length of a string takes linear time because you need to go through all characters to determine how many grapheme clusters there are. One can not know the character count by looking at how much memory a string takes.
Note: The backslash character,
\, is the escape character. It is used here followed by auto indicate that what follows the\uis a Unicode code point in hexadecimal in braces. The acute accent combining character is written using this syntax in the code above. You can use this shorthand to write any Unicode character. I had to use it here for the combining character because I cannot type this character on my keyboard!
However, you can access the underlying Unicode code points in the string via the unicodeScalars view. This view is also a collection itself. So, you can do the following:
cafeNormal.unicodeScalars.count // 4
cafeCombining.unicodeScalars.count // 5
In this case, you’re seeing the difference in the counts as you’d expect.
You can iterate through this Unicode scalars view like so:
for codePoint in cafeCombining.unicodeScalars {
print(codePoint.value)
}
This code will print the following list of numbers, as expected:
99
97
102
101
769
Indexing Strings
Swift doesn’t allow you to get a specific character (err, I mean grapheme cluster) using an integer subscript. While it’s certainly possible to write a function to do this, there are good reasons for the standard library not providing it. The first reason is correctness – Characters are variable in size and cannot be accessed using constant offsets. Swift also wants to prevent you from inadvertently writing inefficient, battery-draining string-processing code. You might not see problems with small strings, but performance would be unacceptable with larger strings.
While most other languages sacrifice Unicode correctness and performance with the simplicity of integer indices, Swift uses a special string index type to remove the problem.
In Swift, you must operate on the specific string index type to index into strings. For example, you obtain the index that represents the start of the string like so:
let firstIndex = cafeCombining.startIndex
If you option-click on firstIndex in a playground, you’ll notice that it is of type String.Index and not an integer.
You can then use this value to obtain the Character (grapheme cluster) at that index, like so:
let firstChar = cafeCombining[firstIndex]
In this case, firstChar will, of course, be c. The type of this value is Character, a grapheme cluster.
Similarly, you can obtain the last grapheme cluster like so:
let lastIndex = cafeCombining.endIndex
let lastChar = cafeCombining[lastIndex]
But if you do this, you’ll get a fatal error on the console (and an EXC_BAD_INSTRUCTION error in the code):
Fatal error: String index is out of bounds
This error happens because the endIndex is one past the end of the string. You need to do this to obtain the last character:
let lastIndex = cafeCombining.index(before: cafeCombining.endIndex)
let lastChar = cafeCombining[lastIndex]
Here you’re obtaining the index just before the end index and then obtaining the character at that index. Alternatively, you could offset from the first character like so:
let fourthIndex = cafeCombining.index(cafeCombining.startIndex,
offsetBy: 3)
let fourthChar = cafeCombining[fourthIndex]
In this case, fourthChar is é as expected.
But as you know, the é in that case is made up of multiple code points. You can access these code points on the Character type the same way as you can on String through the unicodeScalars view. So you can do this:
fourthChar.unicodeScalars.count // 2
fourthChar.unicodeScalars.forEach { codePoint in
print(codePoint.value)
}
This time you’re using the forEach function to iterate through the Unicode scalars view. The count is two, and as expected, the loop prints out:
101
769
Equality With Combining Characters
Combining characters make the equality of strings a little trickier. For example, consider the word café written once using the single é character, and once using the combining character, like so:
These two strings are, of course, logically equal. When printed on-screen, they use the same glyph and look the same. But they are represented inside the computer in different ways. Many programming languages would consider these strings to be unequal because those languages work by comparing the code points one by one.
Swift, however, considers these strings to be equal by default. Let’s see that in action.
let equal = cafeNormal == cafeCombining
In this case, equal is true because the two strings are logically the same.
String comparison in Swift uses a technique known as canonicalization. Say that three times fast! Before checking equality, Swift canonicalizes both strings, converting them to the same special character representation.
It doesn’t matter which way Swift does the canonicalization — using the single character or combining character — as long as both strings get converted to the same style. Once the canonicalization is complete, Swift can compare individual characters to check for equality.
The same canonicalization comes into play when considering how many characters are in a particular string. You saw earlier where café using the single é character and café using the e plus combining accent character had the same length.
Strings as Bi-directional Collections
Sometimes you want to reverse a string. Often this is so you can iterate through it backward. Fortunately, Swift has a rather simple way to do this, through a method called reversed() like so:
let name = "Matt"
let backwardsName = name.reversed()
But what is the type of backwardsName? If you said String, then you would be wrong. It is a ReversedCollection<String>. Changing the type is a smart optimization that Swift makes. Instead of it being a concrete String, it is a reversed collection. Think of it as a thin wrapper around any collection that allows you to use the collection as if it were the other way around, without incurring additional memory usage.
You can then access every Characterin the backwards string just as you would any other string, like so:
let secondCharIndex = backwardsName.index(backwardsName.startIndex,
offsetBy: 1)
let secondChar = backwardsName[secondCharIndex] // "t"
But what if you want a String type? Well, you can do that by initializing a String from the reversed collection, like so:
let backwardsNameString = String(backwardsName)
This code will create a new String from the reversed collection. When doing this, you make a fresh (reversed) copy of the original string with its own memory storage. Staying in the reversed collection domain will save memory space, which is fine if you don’t need the whole reversed string.
Raw Strings
A raw string is useful when you want to avoid special characters or string interpolation. Instead, the complete string as you type it is what becomes the string. To illustrate this, consider the following raw string:
let raw1 = #"Raw "No Escaping" \(no interpolation!). Use all the \ you want!"#
print(raw1)
To denote a raw string, you surround the string with # symbols. This code prints:
Raw "No Escaping" \(no interpolation!). Use all the \ you want!
If you didn’t use the # symbols, this string would try to use interpolation and wouldn’t compile because “no interpolation!” is not valid Swift. If you want to include # in your code, you can do that too. You can use any number of # symbols you want as long as the beginning and end match like so:
let raw2 = ##"Aren’t we "# clever"##
print(raw2)
This prints:
Aren’t we "# clever
What if you want to use interpolation with raw strings? Can you do that?
let can = "can do that too"
let raw3 = #"Yes we \#(can)!"#
print(raw3)
Prints:
Yes, we can do that too!
There’s one more rather fun use of raw strings. You might need to use some ASCII art in your programs from time to time. ASCII art is where you use simple characters to draw out a picture. The problem is that ASCII art often contains the backslash character, \, which is usually the escape character, as you saw earlier. Therefore raw strings are good for ASCII art because otherwise, all the \ would be treated as escapes, and bad things would ensure.
You can try out some ASCII art for yourself:
let multiRaw = #"""
_____ _ __ _
/ ____| (_)/ _| |
| (_____ ___| |_| |_
\___ \ \ /\ / / | _| __|
____) \ V V /| | | | |_
|_____/ \_/\_/ |_|_| \__|
"""#
print(multiRaw)
Now that looks neat!
The Swift community seems to have thought of everything with raw strings.
Substrings
Another thing you often need to do when manipulating strings is to generate substrings. That is, pull out a part of the string into its own value. Swift can do this using a subscript that takes a range of indices.
For example, consider the following code:
let fullName = "Matt Galloway"
let spaceIndex = fullName.firstIndex(of: " ")!
let firstName = fullName[fullName.startIndex..<spaceIndex] // "Matt"
This code finds the index representing the first space (using a force unwrap here because you know one exists). Then it uses a range to find the grapheme clusters between the start index and the index of the space (not including the space).
Now is an excellent time to introduce a new type of range you haven’t seen before: the open-ended range. This type of range only takes one index and assumes the other is either the start or the end of the collection.
That last line of code can be rewritten by using an open-ended range:
let firstName = fullName[..<spaceIndex] // "Matt"
This time we omit the fullName.startIndex, and Swift will infer that this is what you mean.
Similarly, you can also use a one-sided range to start at a certain index and go to the end of the collection, like so:
let lastName = fullName[fullName.index(after: spaceIndex)...]
// "Galloway"
There’s something interesting to point out with substrings. If you look at their type, you will see they are of type String.SubSequence rather than String. This String.SubSequence is just a typealias of Substring, which means that Substring is the actual type, and String.SubSequence is an alias.
Just like with the reversed string, you can force this Substring into a String by doing the following:
let lastNameString = String(lastName)
The reason for this extra Substring type is a cunning optimization. A Substring shares the storage with its parent String that it was sliced from. This sharing means that you use no extra memory when you’re slicing a string. Then, when you want the substring as a String, you explicitly create a new string, and the memory is copied into a new buffer for this new string.
The designers of Swift could have made this copying behavior by default. However, by having the separate type Substring, Swift makes it very explicit what is happening. The good news is that String and Substring share almost all the same capabilities. You might not even realize which type you are using until you return or pass your Substring to another function that requires a String. In this case, you can explicitly initialize a new String from your Substring.
Hopefully, it’s clear that Swift is opinionated about strings and very deliberate in how it implements them. It is an important bit of knowledge to carry because strings are complex beasts and are used frequently. Getting the API right is important — that’s an understatement. :]
Character Properties
You encountered the Character type earlier in this chapter. Some rather interesting properties of this type allow you to introspect the character in question and learn about its semantics.
Let’s take a look at a few of the properties.
The first is simply finding out if the character belongs to the ASCII character set. You can achieve this like so:
let singleCharacter: Character = "x"
singleCharacter.isASCII
Note: ASCII stands for American Standard Code for Information Interchange. It is a fixed-width 7-bit code for representing strings developed in the 1960s by Bell Labs. Because of its history and importance, the standard 8-bit Unicode encoding (UTF-8) was created as a superset of ASCII. You will learn more about UTF-8 later in this chapter.
In this case, the result is true because "x" is indeed in the ASCII character set. However, if you did this for something like "🥳", the “party face” emoji, you would get false.
Next up is checking if something is whitespace. This can be useful as whitespace often has meaning in things like programming languages.
You can achieve this like so:
let space: Character = " "
space.isWhitespace
Again, the result here would be true.
Next up is checking if something is a hexadecimal digit or not. This check can be useful if you are parsing some text and want to know if something is valid hexadecimal. You can achieve this like so:
let hexDigit: Character = "d"
hexDigit.isHexDigit
The result is true, but if you changed it to check "s", it would be false.
Finally, a rather powerful property is being able to convert a character to its numeric value. That might sound simple, say converting the character "5" into the number 5. However, it also works on non-Latin characters. For example:
let thaiNine: Character = "๙"
thaiNine.wholeNumberValue
In this case, the result is 9 because that is the Thai character for the number nine. Neat! :]
These features only scratch the surface of the properties of Character. There are too many to go through each one here; however, you can read more in the Swift evolution proposal, which added these.
Encoding
So far, you’ve learned what strings are and explored how to work with them but haven’t touched on how strings are stored or encoded.
Strings consist of a collection of Unicode code points. These code points range from the number 0 up to 1114111 (or 0x10FFFF in hexadecimal). This means that the maximum number of bits you need to represent a code point is 21.
However, if you are only ever using low code points, such as if your text contains only Latin characters, you can get away with using only eight bits per code point.
Numeric types in most programming languages come in sizes of addressable, powers-of-2 bits, such as 8-bits, 16-bits and 32-bits. This is because computers are made of billions of transistors, either off or on; they just love powers of two!
When choosing how to store strings, you could store every code point in a 32-bit type, such as UInt32. Your String type would be backed by a [UInt32] (a UInt32 array). Each of these UInt32s is what is known as a code unit. However, you would be wasting space because not all those bits are needed, especially if the string uses only low code points.
This choice of how to store strings is known as the string’s encoding. This particular scheme described above is known as UTF-32. However, because it has inefficient memory usage, it is rarely used.
UTF-8
A much more common scheme is called UTF-8. This encoding uses 8-bit code units instead. One reason for UTF-8’s popularity is that it is fully compatible with the venerable, English-only, 7-bit ASCII encoding. But how do you store code points that need more than eight bits?! Herein lies the magic of the encoding.
If the code point requires up to seven bits, it is represented by simply one code unit and is identical to ASCII. But for code points above seven bits, a scheme comes into play that uses up to four code units to represent the code point.
Two code units are used for code points of 8 to 11 bits. The first code unit’s initial three bits are 110. The remaining five bits are the first five bits of the code point. The second code unit’s initial two bits are 10. The remaining six bits are the remaining six bits of the code point.
For example, the code point 0x00BD represents the ½ character. In binary, this is 10111101 and uses eight bits. In UTF-8, this would comprise two code units of 11000010 and 10111101.
To illustrate this, consider the following diagram:
Of course, code points higher than 11 bits are also supported. 12- to 16-bit code points use three UTF-8 code units, and 17- to 21-bit code points use four UTF-8 code units, according to the following scheme:
Each x is replaced with the bits from the code points.
In Swift, you can access the UTF-8 string encoding through the utf8 view. For example, consider the following code:
let char = "\u{00bd}"
for i in char.utf8 {
print(i)
}
The utf8 view is a collection, just like the unicodeScalars view. Its values are the UTF-8 code units that make up the string. In this case, it’s a single character, namely the one that we discussed above.
The above code will print the following:
194
189
If you pull out your calculator (or have a fantastic mental arithmetic mind), you can validate that these are 11000010 and 10111101, respectively, as you expected!
Now consider a more complicated example which you’ll refer back to later in this section. Take the following string:
+½⇨🙃
And iterate through the UTF-8 code units it contains:
let characters = "+\u{00bd}\u{21e8}\u{1f643}"
for i in characters.utf8 {
print("\(i) : \(String(i, radix: 2))")
}
This time the print statement will print out both the decimal number and the number in binary. It prints the following, with newlines added to split grapheme clusters:
43 : 101011
194 : 11000010
189 : 10111101
226 : 11100010
135 : 10000111
168 : 10101000
240 : 11110000
159 : 10011111
153 : 10011001
131 : 10000011
Feel free to verify that these are indeed correct. Notice that the first character used one code unit, the second used two code units, and so on.
UTF-8 is much more compact than UTF-32. For this string, you used 10 bytes to store the 4 code points. In UTF-32, this would be 16 bytes (four bytes per code unit, one code unit per code point, four code points).
There is a downside to UTF-8, though. To handle certain string operations, you need to inspect every byte. For example, if you wanted to jump to the n th code point, you would need to inspect every byte until you have gone past n-1 code points. You cannot simply jump into the buffer because you don’t know how far you have to jump.
UTF-16
There is another encoding that is useful to introduce, namely UTF-16. Yes, you guessed it. It uses 16-bit code units!
This means that code points that are up to 16 bits use one code unit. But how are code points of 17 to 21 bits represented? These use a scheme known as surrogate pairs. These are two UTF-16 code units that, when next to each other, represent a code point from the range above 16 bits.
There is a space within Unicode reserved for these surrogate pair code points. They are split into low and high surrogates. The high surrogates range from 0xD800 to 0xDBFF, and the low surrogates range from 0xDC00 to 0xDFFF.
Perhaps that sounds backward — but the high and low here refer to the bits from the original code point represented by this surrogate.
Take the upside-down face emoji from the string you saw earlier. Its code point is 0x1F643. To find out the surrogate pairs for this code point, you apply the following algorithm:
- Subtract
0x10000to give 0xF643, or0000 1111 0110 0100 0011in binary. - Split these 20 bits into two. This gives you
0000 1111 01and10 0100 0011. - Take the first and add
0xD800to it to give0xD83D. This is your high surrogate. - Take the second and add
0xDC00to it to give0xDE43. This is your low surrogate.
So in UTF-16, that upside-down face emoji is represented by the code unit 0xD83D followed by 0xDE43. Neat!
Just as with UTF-8, Swift allows you to access the UTF-16 code units through the utf16 view, like so:
for i in characters.utf16 {
print("\(i) : \(String(i, radix: 2))")
}
In this case, the following is printed, again with newlines added to split grapheme clusters:
43 : 101011
189 : 10111101
8680 : 10000111101000
55357 : 1101100000111101
56899 : 1101111001000011
As you can see, the only code point that needs to use more than one code unit is the last one, your upside-down face emoji. As expected, the values are correct!
So with UTF-16, your string this time uses 10 bytes (5 code units, 2 bytes per code unit), the same as UTF-8. However, memory usage with UTF-8 and UTF-16 is often different. For example, strings comprised of code points of 7 bits or less will take up twice the space in UTF-16 than in UTF-8.
For a string made up of code points 7 bits or less, the string must be entirely made up of Latin characters in that range. Even the “£” sign is not in this range! So, often, the memory usage of UTF-16 and UTF-8 are comparable.
Swift string views make the String type encoding agnostic — Swift is one of the only languages that does this. Internally it uses UTF-8, C-language compatible, NULL terminated strings because it hits a sweet spot between memory usage and complexity of operations.
Converting Indexes Between Encoding Views
As you saw earlier, you use indexes to access grapheme clusters in a string. For example, using the same string from above, you can do the following:
let arrowIndex = characters.firstIndex(of: "\u{21e8}")!
characters[arrowIndex] // ⇨
Here, arrowIndex is of type String.Index and used to obtain the Character at that index.
You can convert this index into the index relating to the start of this grapheme cluster in the unicodeScalars, utf8 and utf16 views. You do that using the samePosition(in:) method on String.Index, like so:
if let unicodeScalarsIndex = arrowIndex.samePosition(in: characters.unicodeScalars) {
characters.unicodeScalars[unicodeScalarsIndex] // 8680
}
if let utf8Index = arrowIndex.samePosition(in: characters.utf8) {
characters.utf8[utf8Index] // 226
}
if let utf16Index = arrowIndex.samePosition(in: characters.utf16) {
characters.utf16[utf16Index] // 8680
}
unicodeScalarsIndex is of type String.UnicodeScalarView.Index. This grapheme cluster is represented by only one code point, so in the unicodeScalars view, the scalar returned is the one and only code point. If the Character were made up of two code points, such as e combined with ´ as you saw earlier, the scalar returned in the code above would be just the “e”.
Likewise, utf8Index is of type String.UTF8View.Index, and the value at that index is the first UTF-8 code unit used to represent this code point. The same goes for the utf16Index, which is of type String.UTF16View.Index.
Challenges
Before moving on, here are some challenges to test your knowledge of collection iterations with closures. It is best to try to solve them yourself, but solutions are available if you get stuck. Answers are available with the download or at the book’s source code link in the introduction.
Challenge 1: Character Count
Write a function that takes a string and prints out the count of each character in the string. For bonus points, print them ordered by the count of each character. For bonus-bonus points, print it as a nice histogram.
Hint: You could use # characters to draw the bars.
Challenge 2: Word Count
Write a function that tells you how many words there are in a string. Do it without splitting the string.
Hint: try iterating through the string yourself.
Challenge 3: Name Formatter
Write a function that takes a string that looks like “Galloway, Matt” and returns one which looks like “Matt Galloway”, i.e., the string goes from "<LAST_NAME>, <FIRST_NAME>" to "<FIRST_NAME> <LAST_NAME>".
Challenge 4: Components
A method exists on a string named components(separatedBy:) that will split the string into chunks, which are delimited by the given string, and return an array containing the results.
Your challenge is to implement this yourself.
Hint: There exists a view on String named indices that lets you iterate through all the indices (of type String.Index) in the string. You will need to use this.
Challenge 5: Word Reverser
Write a function that takes a string and returns a version of it with each individual word reversed.
For example, if the string is “My dog is called Rover” then the resulting string would be “yM god si dellac revoR”.
Try to do it by iterating through the indices of the string until you find a space and then reversing what was before it. Build up the result string by continually doing that as you iterate through the string.
Hint: You’ll need to do a similar thing as you did for Challenge 4 but reverse the word each time. Try to explain to yourself, or the closest unsuspecting family member, why this is better in terms of memory usage than using the function you created in the previous challenge.
Key Points
- Strings are collections of
Charactertypes. - A
Characteris grapheme cluster and is made up of one or more code points. - A combining character is a character that alters the previous character in some way.
- You use special (non-integer) indexes to subscript into the string to a certain grapheme cluster.
- Swift’s use of canonicalization ensures that the comparison of strings accounts for combining characters.
- Slicing a string yields a substring with the type
Substring, which shares storage with its parentString. - You can convert from a
Substringto aStringby initializing a newStringand passing theSubstring. - Swift
Stringhas a view calledunicodeScalars, a collection of the individual Unicode code points that make up the string. - There are multiple ways to encode a string. UTF-8 and UTF-16 are the most popular.
- The individual parts of an encoding are called code units. UTF-8 uses 8-bit code units, and UTF-16 uses 16-bit code units.
- Swift’s
Stringhas views calledutf8andutf16that are collections that allow you to obtain the individual code units in the given encoding.