Our checkReverse method does the job and the nice thing is that the implementation is very simple. As hinted earlier, at some point, though, we might want to exchange the implementation for a better version. One problem of the naive reverse approach is that for every string we check, we need twice the memory to perform the actual equality check. With growing adoption of our palindrome-checking-service however, users want to have larger and larger strings checked for palindromicity, reaching millions of chars in length! When we want to check such a large string, it is unthinkable to just allocate twice the memory, certainly we can do better than that!

After a quick round on the white board, we come up with an alternative way to check for palindromicity: we keep track of two indices, starting from the very left and the very right of the palindrome. The idea is to then work our way towards the middle, comparing the characters as we go:

We start with an implementation and on the first try, we come up with this:

def checkIndices (s : String ) : Boolean = { @tailrec def loop (i : Int , j : Int ) : Boolean = (i,j) match { case ( i , j ) if i == j => true case ( i , j ) if s(i) != s(j) => false case ( i , j ) if s(i) == s(j) => loop(i+ 1 ,j-1) } loop( 0 ,s.length-1) }

To test this, we need a property. As we are pretty sure that checkReverse is a correct implementation, why not use that as reference:

property( "checkIndices" ) = forAll(maybePalindrome) { s => checkReverse(s) == checkIndices(s) }

We say that for all strings generated by maybePalindrome , our checkIndices method should return the same result as checkReverse . We define maybePalindrome as:

val maybePalindrome : Gen [ String ] = Gen .oneOf(palindromeGen,arbitrary[ String ])

Note the use the oneOf method from Gen to create a generator that will sometimes produce palindromes and sometimes arbitrary strings. Now let’s test our new palindrome checker:

> test [info] ! Palindrome.checkIndices: Exception raised on property evaluation. [info] > ARG_0: "" [info] > Exception: java.lang.StringIndexOutOfBoundsException: String index out of range: 0

We forgot about the empty string! Okay that’s easy, let’s fix that and just run the tests again:

> test [info] ! Palindrome.checkIndices2: Exception raised on property evaluation. [info] > ARG_0: "꫖꫖" [info] > Exception: java.lang.StringIndexOutOfBoundsException: String index out of range: 2

Another error, this time when handling palindromes with an even number of chars. Investigating this, we come to the conclusion, that we forgot to consider what happens when the two indices never meet because they pass each other, due to an even number of chars:

Here in the next step, our current algorithm would continue with i and j in swapped position and then continue both indexes until we run out of chars, leading to the StringIndexOutOfBoundsException from above. To fix this, we add another case to our implementation:

def checkIndices (s : String ) : Boolean = { @tailrec def loop (i : Int , j : Int ) : Boolean = (i,j) match { // additional case: check for adjacent indices case ( i , j ) if i+ 1 == j => s(i) == s(j) case ( i , j ) if i == j => true case ( i , j ) if s(i) != s(j) => false case ( i , j ) if s(i) == s(j) => loop(i+ 1 ,j-1) } // additional check: "" is a palindrome s.isEmpty || loop( 0 ,s.length-1) }

Feeling lucky, we run our tests again:

> test [info] + Palindrome.checkReverse: OK, passed 100 tests. [info] + Palindrome.checkIndices: OK, passed 100 tests. [info] Passed: Total 2, Failed 0, Errors 0, Passed 2