Lesson 8 - Strings in Swift - Working with single characters
In the previous exercise, Solved tasks for Swift lesson 7, we've practiced our knowledge from previous lessons.
If you noticed some similarities between arrays and strings, you were
absolutely onto something. For the others, it may be a surprise that a
String
is essentially an array of Character
s
and we can work with it like so.
First, we'll check out how it works by simply printing the character at a
given position. Unfortunately, we can't just specify the character index as a
number, we have to retrieve it first via the index()
method. This
method returns a special String.Index
type through which we can get
to the character.
let s = "Hello ICT.social" print(s) let index = s.index(s.startIndex, offsetBy: 2) print(s[index])
The output:
Hello ICT.social l
We can see that we can access characters of a string through the brackets as it was with the array. It may be disappointing that characters at the given positions are read-only in Swift, so we can't write the following:
// This code won't work var s = "Hello, ICT.social" let index = s.index(s.startIndex, offsetBy: 2) s[index] = "l" println(s)
Of course, there is a way to do it, but we'll go over it later. For now, we'll be just reading characters.
Character frequency analysis
Let's write a simple program that analyzes a given sentence for us. We'll
search for the number of vowels, consonants and non-alphanumeric characters
(e.g. space or !
).
We'll hard-code the input string in our code, so we won't have to enter it
again every time. Once the program is complete, we'll replace the string with
readLine()
. We'll iterate over characters using a loop. I should
start out by saying that we won't focus as much on program speed here, we'll
choose practical and simple solutions.
First, let's define vowels and consonants. We don't have to count non-alphanumeric characters since it'll be the string length minus the number of vowels and consonants. Since we don't want to deal with the letter case, uppercase/lowercase, we'll convert the entire string to lowercase at the start. Let's set up variables for the individual counters, and also, because it's a more complex code, we'll add comments.
// the string that we want to analyze var s = "A programmer gets stuck in the shower because the instructions on the shampoo were: Lather, Wash, and Repeat." print(s) s = s.lowercased() // counters initialization var vowelsCount = 0 var consonantsCount = 0 // definition of character groups let vowels = "aeiouy" let consonants = "bcdfghjklmnpqrstvwxz" // main loop for c in s { }
First of all, we prepare the string and convert it to lowercase. Then, we
reset the counters. For the definition of characters groups, we only need
ordinary String
s. The main loop iterates over each character in the
String
s. In each iteration of the loop the variable
c
will contain the current character.
Now let's increment the counters. For simplicity's sake, I'll focus on the loop instead of rewriting the code repeatedly:
// main loop for c in s { if vowels.contains(c) { vowelsCount += 1 } else if consonants.contains(c) { consonantsCount += 1 } }
We already know the contains()
method on a String. As a
parameter, it can take both a substring or a character. Firstly, we try to find
the character c
from our sentence in the String
vowels
and possibly increase their counter. If it's not included in
vowels, we look in consonants and possibly increase their counter.
Now all we're missing is the printing, displaying text, part at the end:
var s = "A programmer gets stuck in the shower because the instructions on the shampoo were: Lather, Wash, and Repeat." println(s) s = s.lowercased() // counters initialization var vowelCount = 0 var consonantCount = 0 // definition of character groups let vowels = "aeiouy" let consonants = "bcdfghjklmnpqrstvwxyz" // main loop for c in s { if vowels.contains(c) { vowelCount += 1 } else if consonants.contains(c) { consonantCount += 1 } } print("Vowels: \(vowelCount)") print("Consonants: \(consonantCount)") print("Other characters: \(s.length - (vowelCount + consonantCount))")
The output:
A programmer gets stuck in the shower because the instructions on the shampoo were: Lather, Wash, and Repeat. Vowels: 33 Consonants: 55 Non-alphanumeric characters: 21
That's it, we're done!
ASCII value
Maybe you've already heard about the ASCII table. Especially, in the MS-DOS
era when there was practically no other way to store text. Individual characters
were stored as numbers of the byte datatype, so of a range from 0
to 255
. The system provided the ASCII table which had 256
characters and each ASCII code (numerical code) was assigned to one
character.
Perhaps you understand why this method is no longer as relevant. The table
simply could not contain all the characters of all international alphabets, now
we use Unicode (UTF-8) encoding where characters are represented in a different
way. In Swift, we have the option to work with ASCII values of individual
characters. The main advantage is that the characters are stored in the table
next to each other, alphabetically. For example, at the position 97
we can find "a"
, at 98
"b"
etc. It's the
same with numbers, but unfortunately, the accent characters are messed up.
Now, let's convert a character into its ASCII value and vice versa create the character according to its ASCII value. The code is a little bit complicated; we'll describe it in a moment.
var c: Character // character c = "a" // conversion from text to ASCII value let optionalASCIIvalue = c.unicodeScalars.filter{$0.isASCII}.first?.value // we got an Optional but we already know what to do with it if let ASCIIvalue = optionalASCIIvalue { print("The character \(c) was converted to its ASCII value of \(ASCIIvalue)") } // conversion from an ASCII value to text c = Character(UnicodeScalar(98)) print("The ASCII value of 98 was converted to its textual value of \(c)")
Because especially the conversion of the character to its ASCII value looks quite frightening, let's describe briefly what this line of code actually does:
let optionalASCIIvalue = c.unicodeScalars.filter{$0.isASCII}.first?.value
In Swift, Character
s and String
s are represented by
Unicode Scalars, a 21-bit representation of one character. It can be, for
example, "a"
, digits, special characters or emojis. We get this
value or values from the unicodeScalars
property of a
Character
or String
.
filter
allows us to select ASCII characters only, since Unicode
Scalars also contain other special characters. We pass $0
into the
filter, which (in this case) represents one Unicode Scalar. Using the
isASCII
property, we simply ask whether this character is part of
ASCII table which is more limited than the Unicode Scalars representation. Then
we just get the value of the first element found. That's an
Optional
since it may happen we won't find anything.
The Caesar cipher
Let's create a simple program to encrypt text. If you've ever heard of the
Caesar cipher, then you already know exactly what we're going to program. The
text encryption is based on shifting characters in the alphabet by a certain
fixed number of characters. For example, if we shift the word
"hello"
by 1
character forwards, we'd get
"ifmmp"
. The user will be allowed to select the number of character
shifts.
Let's get right into it! We need variables for the original text, the
encrypted message, and the shift. Then, we need a loop iterating over each
character and we'll print the encrypted message at the end. We'll hard-code the
message into our code again, so we won't have to enter it over and over during
the testing phase. Once we finish the program, we'll replace the contents of the
variable with the readLine()
method. The cipher doesn't work with
accent characters, spaces and punctuation marks. We'll just assume the user
won't enter them. Ideally, we should remove accent characters before the
encryption, as well as anything except letters.
// variable initialization let s = "blackholesarewheregoddividedbyzero" print("Original message: \(s)") var message : String = "" var shift = 1 // loop iterating over characters for c in s { } // printing print("Encrypted message: \(message)")
We'll now move into the loop. We'll convert the character in c
to its ASCII value, its ordinal value, increase the value by the number of
shifts
and cast it back to a character. This character will be
added to the final message:
// variable initialization let s = "blackholesarewheregoddividedbyzero" print("Original message: \(s)") var message : String = "" var shift : UInt32 = 1 // loop iterating over characters for c in s { let optionalASCIIvalue = c.unicodeScalars.filter{$0.isASCII}.first?.value if let ASCIIvalue = optionalASCIIvalue { let newCharacter = Character(UnicodeScalar(ASCIIvalue + shift)!) message += [newCharacter] } } // printing print("Encrypted message: \(message)")
The output:
Original message: blackholesarewheregoddividedbyzero Encrypted message: cmbdlipmftbsfxifsfhpeejwjefeczafsp
You can see we opened Optional
'by force' once but we know what
we're doing Why are there
brackets around newCharacter
when we add it to our message? Swift
lets us to use the +=
operator to add another String
or an array of characters to the original String
, but not a single
character. That's why we created a temporary array of a single character.
Let's try it out! The result looks pretty good. However, we can see that the
characters after "z"
overflow to ASCII values of other characters
(e.g. "{"
). Therefore, the characters are no longer just
alphanumeric, but other nasty characters. Let's enclose our characters as a
cyclical pattern, so the shifting could flow smoothly from "z"
to
"a"
and so on. We'll get by with a simple condition that decreases
the ASCII value by the length of the alphabet so we'd end back up at
"a"
.
We'll create a new variable above the loop with the ASCII value of
"z"
:
let zASCIIvalue = "z".unicodeScalars.filter{$0.isASCII}.first!.value
And modify the inside of the loop like this:
let optionalASCIIvalue = c.unicodeScalars.filter{$0.isASCII}.first?.value if let ASCIIvalue = optionalASCIIvalue { var shiftedCharacter = ASCIIvalue + shift if shiftedCharacter > zASCIIvalue { shiftedCharacter -= 26 } let newCharacter = Character(UnicodeScalar(shiftedCharacter)!) message += [newCharacter] }
If shiftedCharacter
exceeds the ASCII value of "z"
,
we reduce it by 26
characters (the number of characters in the
English alphabet). The -=
operator does the same as we would do
with shiftedCharacter = shiftedCharacter - 26
. It's simple and our
program is now working properly. Notice that we don't use direct character codes
anywhere. There's there's the "z"
character value in the condition
which we retrieved before (once is enough, we don't have to do it in every
iteration of the loop) even though we could write 122
there
directly. We did it this way so that our program is fully independent from
explicit ASCII values, so it'd be clearer how it works. Try to code the
decryption program as practice for yourself.
In the following exercise, Solved tasks for Swift lesson 8, we're gonna practice our knowledge from previous lessons.
Download
By downloading the following file, you agree to the license termsDownloaded 445x (68.67 kB)