Play With Code: Palindromes In Swift

Written by Reinder de Vries on February 2 2020 in App Development, Swift

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 article, we’ll discuss and code 3 approaches to check if a string is a palindrome in Swift.

Ready? Let’s go.

  1. What’s A Palindrome?
  2. Palindrome 1: Comparing String Characters
  3. Palindrome 2: Solving It Recursively
  4. Palindrome 3: This One Trick…
  5. 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!

Learn how to build iOS apps

Get started with iOS 14 and Swift 5

Sign up for my iOS development course, and learn how to build great iOS 14 apps with Swift 5 and Xcode 12.

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:

  1. Take the first and last character of the string
  2. 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
  3. 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 to len.
  • The for loop will iterate from 0 to len (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 constant i 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 plus i. So, when i is 0, we’ll get the first character of the string. When i = 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 subtract 1 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!

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
}

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:

  • hannahh == hanna
  • annaa == ann
  • nnn == 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 called return 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 returning false. Also, a string of one character is by definition a palindrome.
  • When the input string’s length is 2 or more, the else block is not invoked and the function executes normally. Differently said, an input string xx will continue, and call isPalindrome(_:) 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:

  • hannahhan + nahhan + hanhan == han
  • rotorro + orro + roro == ro (ignoring middle t)

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 becomes steponnopets, for which isPalindrome(_:) returns true
  • A man, a plan, a canal – Panama becomes amanaplanacanalpanama, which is a palindrome

Sweet!

Learn how to build iOS apps

Get started with iOS 14 and Swift 5

Sign up for my iOS development course, and learn how to build great iOS 14 apps with Swift 5 and Xcode 12.

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:

Reinder de Vries

Hi, I'm Reinder.
I help developers play with code.

Get the Weekly

Get iOS/Swift tutorials and insights in your inbox, every Monday.
  • This field is for validation purposes and should be left unchanged.

Most Popular

Browse Topics

Swift Sandbox

Code Swift right in your browser!
Go to the Swift Sandbox

Reinder de Vries

Reinder de Vries

Reinder de Vries is a professional iOS developer. He teaches app developers how to build their own apps at LearnAppMaking.com. Since 2009 he has developed a few dozen apps for iOS, worked for global brands and lead development at several startups. When he’s not coding, he enjoys strong espresso and traveling.

×

Build great iOS apps
Learn how in my free 7-day course

  • This field is for validation purposes and should be left unchanged.

No spam, ever. Unsubscribe anytime. Privacy Policy