Play With Code: Palindromes In Swift
The date 02-02-2020 is a palindrome. Palindromes are words that read the same forward as backward. And they’re great fun to play with in Swift! In this tutorial, we’ll discuss and code 3 approaches to check if a string is a palindrome in Swift.
Ready? Let’s go.
- What’s A Palindrome?
- Palindrome 1: Comparing String Characters
- Palindrome 2: Solving It Recursively
- Palindrome 3: This One Trick…
- Further Reading
What’s A Palindrome?
Firs things first: What’s a palindrome?
A palindrome is a word, number, phrase or other sequence that reads the same forward as it reads backward. A few examples:
- Madam, racecar, Hannah, radar, level or 02-02-2020
- A man, a plan, a canal – Panama
- Doc, note: I dissent. A fast never prevents a fatness. I diet on cod.
- Step on no pets
(Source: Palindrome)
A palindrome is a nerdy word joke!
If you look closely, you’ll see that you can essentially “flip” or rotate a palindrome around its middle. What’s on the left of the middle, is also on the right. It doesn’t matter if the number of characters is even or odd. And as we can see in the examples above, palindromes can also include punctuation.
You can get pretty crazy with palindromes. For example, a Lychrel number is an integer number that cannot form a palindrome after repeated reversal and addition of its digits. Take 56, which becomes palindromic after one iteration: 56 + 65 = 121.
And as you’ve guessed, palindromes are a prime subject for playing around with strings in Swift!
Palindrome 1: Comparing String Characters
We’re going to figure out if a given string is a palindrome, by using Swift programming. Before we write some code, we’ll have to agree on a strategy or algorithm.
Here’s one such algorithm:
- Take the first and last character of the string
- Compare them against each other:
- If they’re the same, continue with step 3
- If they’re not the same, stop, the string is not a palindrome
- Look at the next character, so the second one from the start, and the before last character at the end, then go to step 2
With this algorithm, we’re essentially “walking” the characters from both the start and the end of the string. You compare them against each other, and when every character pair is equal, the string is a palindrome.
Let’s code that in Swift. We’ll start by defining the function isPalindrome(_:)
. Like this:
func isPalindrome(_ value: String) -> Bool
{
}
In the above code, we’ve defined a function named isPalindrome(_:)
. It has one unnamed parameter value
of type String
. The function returns a Bool
value, either true
or false
. We’re going to check if value
is a palindrome, and return true
if it is, and false
if it’s not.
Next up, we’re adding the following code to the function:
let len = value.count / 2
for i in 0..<len
{
}
This is a for loop. Here’s how it works:
- We’re first dividing the length of the input string
value
by 2. We’ll only need to iterate half of the string, because we’re starting at both ends. The length of half the string is assigned tolen
. - The for loop will iterate from
0
tolen
(exclusive). So, when the input string has 7 characters, we’ll loop from 0 to 3:0, 1, 2, 3
. We can use these integer values as the constanti
within the loop.
The above code also uses a trick. What happens when the length of the string is an odd number, like 7? In that case, len
will be 3. The code value.count / 2
is essentially 7 / 2
, which is 3.5, but since both 7
and 3
are integers, the resulting type of len
is an integer too. You can’t assign a decimal value, like 3.5, to an integer. As a result, the fraction of the decimal value is “chopped off”, and we end up with 3
.
OK, next up. We’ll need to get the characters at the start and end of the input string. Add the following code inside the loop:
let start = value.index(value.startIndex, offsetBy: i)
let end = value.index(value.endIndex, offsetBy: (i * -1) - 1)
What’s going on here?
- The first line, with
value.index(...)
, will create an index that starts at the beginning of the string plusi
. So, wheni
is0
, we’ll get the first character of the string. Wheni = 1
, we’ll get the second, and so on. - The second line does the same thing, except it starts at the end of the string and goes backwards. We start at the end of the string, then subtract
i
from the index number, and subtract1
to get the one before that.
In Swift, string indices are complicated. First, we’ll need to work with an index to get a character from a string. Integers as indices won’t work. For now, let’s assume that start
and end
correspond to the first and last positions in the string.
Then, add the following code in the for loop:
if value[start] != value[end] {
return false
}
The above code will compare the character for the start
index with the character for the end
index, by using subscript syntax. If they’re not the same, the function returns false
, because this string is not a palindrome!
Finally, at the end of the function, outside the for loop, add this code:
return true
Why do we need that? When the for loop has walked all the characters in the string, we’ve proved that they are all equal (not not equal), so the string is a palindrome! We return true
.
Here’s the entire function:
func isPalindrome(_ value: String) -> Bool
{
let len = value.count / 2
for i in 0..<len
{
let start = value.index(value.startIndex, offsetBy: i)
let end = value.index(value.endIndex, offsetBy: (i * -1) - 1)
if value[start] != value[end] {
return false
}
}
return true
}
Does it work? Let’s find out!
{
let len = value.count / 2
for i in 0..<len
{
let start = value.index(value.startIndex, offsetBy: i)
let end = value.index(value.endIndex, offsetBy: (i * -1) - 1)
if value[start] != value[end] {
return false
}
}
return true
}
print(isPalindrome("02022020"))
Awesome!
Palindrome 2: Solving It Recursively
Let’s continue with another approach. Instead of using a for loop, we’ll recursively iterate the string. We’ll take smaller and smaller pieces of the string, continually checking if the first and last characters are the same.
Here, check this out:
func isPalindrome( _ value: String) -> Bool
{
guard value.count >= 2 else {
return true
}
let end = value.index(value.endIndex, offsetBy: -1)
if value[value.startIndex] == value[end] {
let start = value.index(value.startIndex, offsetBy: 1)
return isPalindrome(String(value[start..<end]))
} else {
return false
}
}
Whoah, what’s going on here?
First, note that the function is essentially the same as the previous one. We’ve got one input value
, and we’re either returning true
or false
. The principle of the function remains the same too: compare the first character of the input string against the last.
Recursion happens when a function calls itself. You can see that happening inside the if
statement in the above code. We’re calling the isPalindrome(_:)
function from within itself. As a result, the function repeats itself, and loops over the given values.
The if value[value.startIndex] == value[end]
code is similar to what we had before. When the first and last characters are equal, continue execution. When they’re not equal, return false
, in the else
block.
When both characters are equal, and we continue, what string do we use for the next call of isPalindrome(_:)
? It’s simple: we cut off the first and last characters of the string, so that the new first and last characters are actually the second and before last characters of the whole string.
Here’s how that works:
-
hannah →
h == h
→ anna -
anna →
a == a
→ nn -
nn →
n == n
→ DONE!
The next time isPalindrome(_:)
runs, the string has become shorter. We check the first and last characters again, and call the function again, and again, and again, until…
Until when? That’s where this block of code comes in:
guard value.count >= 2 else {
return true
}
The guard statement will check if value.count
is bigger than or equal to 2
, and if it isn’t, the function will return true
. Differently said, when the length of value
is 1 or 2, the function returns true
.
Here’s why:
- When the input string is empty, it’s length is
0
, and given that we’ve come so far, the entire input string must be a palindrome. If it weren’t, we would have calledreturn false
sooner. - When the input string’s length is
1
, we assume the entire string is a palindrome, because we’ve come so far without returningfalse
. Also, a string of one character is by definition a palindrome. - When the input string’s length is
2
or more, theelse
block is not invoked and the function executes normally. Differently said, an input stringxx
will continue, and callisPalindrome(_:)
once more, with an empty string as input.
Awesome!
Why are let end ...
and let start ...
so oddly placed? That’s because of value[start..<end]
. The value.endIndex
corresponds to the after last index, i.e. the character after the last character in the string. The end
index thus corresponds to the actual last character in the string. We need this in the if
statement’s logic, so let end ...
is placed outside of it. Inside the if
block, we’re using the range start..<end
to create a substring starting at start
, up to end
, but not including. In other words, let end ...
is placed outside the if
statement because we need it twice, whereas let start ...
is placed inside the if
block because we only need it in there.
Palindrome 3: This One Trick…
So far we’ve gone through a lot of trouble to naively check every character of the input string against the other one. We’ve walked from one end of the string to the middle, and from the other end to the middle, checking every character against the other.
Based on this, we can assert that a string is a palindrome if both halves of the string are equal to each other, in reverse. Like this:
-
hannah
→han
+nah
→han
+han
→han == han
-
rotor
→ro
+or
→ro
+ro
→ro == ro
(ignoring middlet
)
Provided that both string halves can be mirrored, we might as well reverse the entire string if both halves are the same anyway. Said differently, would you notice if I spelled hannah
in reverse? (No.) Conversely, would you notice that I spelled doghouse
in reverse? (Yes.)
And that, dear coder, is why the simplest algorithm to check if a given string is a palindrome is…
func isPalindrome(_ value: String) -> Bool
{
return value == String(value.reversed())
}
A string is a palindrome if it’s equal to the same string in reverse. Awesome!
By the way, while we’re at it, you may have noticed that many palindromes are sentences, riddles, etc. Since string equality in Swift is strict, i.e. it includes punctuation too, we’ll need something to sanitize one such riddle if we want to run it through our isPalindrome(_:)
function.
Here’s how you do that:
func sanitize(_ value: String) -> String
{
return value.lowercased().replacingOccurrences(of: "[^a-z]+", with: "", options: .regularExpression)
}
The sanitize(_:)
function will take a value
string as input, and returns the same string as lowercase, with any character but lowercase A to Z removed. It uses regular expressions to replace characters in the string.
-
Step on no pets
becomessteponnopets
, for whichisPalindrome(_:)
returnstrue
-
A man, a plan, a canal – Panama
becomesamanaplanacanalpanama
, which is a palindrome
Sweet!
Further Reading
And there you have it: palindromes. We’ve discussed 3 approaches to check if a given string is a palindrome, and we’ve touched on many Swift syntaxes and topics in between.
The big question of course, is when the next 02-02-2020
palindromic date occurs…
var value = "10" // Anything but a palindrome
var day = 1 // Increase by one day
var date = Date() // Now
let formatter = DateFormatter()
formatter.dateFormat = "ddMMyyyy"
// Keep checking `value` for palindrome until one is found
while isPalindrome(value) == false
{
// Add 1 day to `date`, assign ddMMyyyy format to `value`
let component = DateComponents(day: day)
date = Calendar.current.date(byAdding: component, to: date)!
value = formatter.string(from: date)
}
print(value)
(It’s February 12th, 2021, or 12-02-2021
. Or December 2nd, 2021, depending on where you live.)
Want to learn more? Check out these resources: