|
| 1 | +Let's get started. |
| 2 | + |
| 3 | +## Prepare our tests |
| 4 | + |
| 5 | +Before we start to understand the potential pitfalls and ask clarifying questions, let's define the signature of our solution. |
| 6 | + |
| 7 | +The palindrome check is going to take a `String` as input and return a `bool` as output. Dart is very versatile, so there are many ways to solve this, but the simplest is using a function. |
| 8 | + |
| 9 | +I find the alternatives worse and unnecessarily verbose, but hey, people with a different background than mine might find one of those solutions more idiomatic and if the problem was more complicated, a simple function might not cut it. The reason why I mention this is because there are always many ways to solve a problem and you can also provide your solution using different programming styles. One way could be: |
| 10 | + |
| 11 | +```dart |
| 12 | +abstract class PalindromeContainer { |
| 13 | + const PalindromeContainer(this.input); |
| 14 | + final String input; |
| 15 | + bool isPalindrome(); |
| 16 | +} |
| 17 | +
|
| 18 | +class IterativePalindromeContainer implements PalindromeContainer { |
| 19 | + const IterativePalindromeContainer(this.input); |
| 20 | + |
| 21 | + @override |
| 22 | + final String input; |
| 23 | +
|
| 24 | + @override |
| 25 | + bool isPalindrome() { |
| 26 | + // Implement method here. |
| 27 | + return true; // TODO |
| 28 | + } |
| 29 | +} |
| 30 | +``` |
| 31 | + |
| 32 | +Let's stick with functions for now, as I'm against over-engineering solutions and premature generalization and I'll always favor the simple way of solving things, whenever possible. Also good to keep in mind that in Dart, you can absolutely use functions instead of classes, so there's that (some languages require you to wrap everything in a class). |
| 33 | + |
| 34 | +So let's write our first test. |
| 35 | + |
| 36 | +```dart |
| 37 | +
|
| 38 | +import 'package:test/test.dart'; |
| 39 | +
|
| 40 | +import 'palindrome_string_check.dart'; |
| 41 | +
|
| 42 | +void main() { |
| 43 | + test('is palindrome', () { |
| 44 | + expect(isPalindrome('madam'), true); |
| 45 | + }); |
| 46 | +} |
| 47 | +``` |
| 48 | + |
| 49 | +We get a compiler error, so add a function to make this test pass: |
| 50 | + |
| 51 | +```dart |
| 52 | +bool isPalindrome(String input) { |
| 53 | + return false; |
| 54 | +} |
| 55 | +``` |
| 56 | + |
| 57 | +And let's run the tests |
| 58 | + |
| 59 | +``` |
| 60 | +pub run test . |
| 61 | +00:01 +1: All tests passed! |
| 62 | +``` |
| 63 | + |
| 64 | +And there you go, our palindrome check is ready! Huh? Not so fast, let's write another test to make sure it works. |
| 65 | + |
| 66 | +```dart |
| 67 | +test('is palindrome', () { |
| 68 | + expect(isPalindrome('madam'), true); |
| 69 | +}); |
| 70 | +test('is not palindrome', () { |
| 71 | + expect(isPalindrome('sir'), false); |
| 72 | +}); |
| 73 | +``` |
| 74 | + |
| 75 | +``` |
| 76 | +$ pub run test . |
| 77 | +00:01 +1 -1: ./palindrome_string_check/palindrome_string_check_test.dart: is not palindrome [E] |
| 78 | + Expected: <false> |
| 79 | + Actual: <true> |
| 80 | +
|
| 81 | + package:test_api expect |
| 82 | + palindrome_string_check/palindrome_string_check_test.dart 10:5 main.<fn> |
| 83 | +
|
| 84 | +00:01 +1 -1: Some tests failed |
| 85 | +``` |
| 86 | + |
| 87 | +When you start running tests, it's important to remember: never trust a test until you haven't seen it fail. We just have, so now, we trust our tests. |
| 88 | + |
| 89 | +## Clarify |
| 90 | + |
| 91 | +Now is time to ask clarifying questions about the programming challenge at hand and write our test cases. |
| 92 | + |
| 93 | +Keep in mind that your interviewer might define different rules for the palindrome check problem, and this might make the solution more difficult. |
| 94 | + |
| 95 | +### Spaces |
| 96 | + |
| 97 | +> Can we ignore spaces in the string? For example, should our function return `true` or `false` for `'taco cat'`? |
| 98 | +
|
| 99 | +Spaces are significant, so for `taco cat` the solution should return false as `'taco cat' != 'tac ocat'`. |
| 100 | + |
| 101 | +```dart |
| 102 | +test('spaces are significant', () { |
| 103 | + expect(isPalindrome('taco cat'), false); |
| 104 | +}); |
| 105 | +``` |
| 106 | + |
| 107 | +### Letter case |
| 108 | + |
| 109 | +> Can we consider uppercase and lowercase versions of the same letters as equal? |
| 110 | +
|
| 111 | +No, let's not do that, `'Abba' != 'abbA'`, so therefore `isPalindrome('Abba'')` must return `false`. |
| 112 | + |
| 113 | + |
| 114 | +### Punctuation |
| 115 | + |
| 116 | +> Are letter casing and punctuation significant in deciding whether a string is a palindrome or not? What should our function return for `'Was it a car or a cat I saw?'` |
| 117 | +
|
| 118 | +Punctuation and capital letters matter, so `'a' != 'A'` and `'ab!' != 'ab'`, therefore the function should return `false`. |
| 119 | + |
| 120 | +```dart |
| 121 | +test('punctuation cannot be ignored', () { |
| 122 | + expect(isPalindrome('was it a car or a cat I saw?'), false); |
| 123 | +}); |
| 124 | +``` |
| 125 | + |
| 126 | +### Empty strings |
| 127 | + |
| 128 | +> What should the function do when it receives an empty string as input? Throw? Return `true`? Return `false`? |
| 129 | +
|
| 130 | +To make things more interesting and learn more about the Dart `test` package, our requirements say the function should throw. To be honest, we could have also set the requirements to consider empty strings palindrome, but where's the fun in that? |
| 131 | + |
| 132 | + |
| 133 | +```dart |
| 134 | +test('empty string is an invalid input', () { |
| 135 | + expect(() => isPalindrome(''), throwsArgumentError); |
| 136 | +}); |
| 137 | +``` |
| 138 | + |
| 139 | +## Non-ASCII characters |
| 140 | + |
| 141 | +> Does our function need to handle special letters (that are not part of the English alphabet)? |
| 142 | +
|
| 143 | +Yes, yes, yes. The function should be able to handle all kinds of letters, it should not abort or should crash the app. Also, don't coerce non-ascii characters to ascii characters, so `isPalindrome('neuquén')` should return false as `'e' != 'é'`. |
| 144 | + |
| 145 | +```dart |
| 146 | +test('non-ASCII', () { |
| 147 | + expect(isPalindrome('rör'), true); |
| 148 | + expect(isPalindrome('röor'), false); |
| 149 | + expect(isPalindrome('neuquén'), false); |
| 150 | +}); |
| 151 | +``` |
| 152 | + |
| 153 | +### Emojis |
| 154 | + |
| 155 | +> What's with emojis? Can we expect emojis as input? |
| 156 | +
|
| 157 | +Yes, we all love emojis, so why shouldn't our function support that? Let's write more expectation in this test case, just for the hell of it. |
| 158 | + |
| 159 | +Depending on your programming language, dealing with emojis could be more difficult, but worry not, it's not going to make much difference in Dart. |
| 160 | + |
| 161 | +```dart |
| 162 | +test('supports emojis', () { |
| 163 | + expect(isPalindrome('🍎'), true); |
| 164 | + expect(isPalindrome('🍎🍏'), false); |
| 165 | + expect(isPalindrome('🍎🍎'), false); |
| 166 | + expect(isPalindrome('🍎🍏🍎'), true); |
| 167 | + expect(isPalindrome('🍎🍏🍎'), true); |
| 168 | + expect(isPalindrome('🍎🍏🍏🍎'), true); |
| 169 | + expect(isPalindrome('🍎🍏🍎🍏'), false); |
| 170 | +}); |
| 171 | +``` |
| 172 | + |
| 173 | +Well, I'm a little paranoid, so let's add a different test for a mix of emojis and normal characters. |
| 174 | + |
| 175 | +```dart |
| 176 | +expect(isPalindrome('abc🐦⛅️🐦cba'), true); |
| 177 | +``` |
| 178 | + |
| 179 | +### MOAR Test cases |
| 180 | + |
| 181 | +As part of the clarifying questions, we already defined a couple of test cases. Those test cases are based on the *question*, now let's define a couple of test cases for *common programming errors*: potential difference for strings with odd and even length, strings with length of 1 long strings, spaces, tabs, mix of weird letters, capital letters, etc... |
| 182 | + |
| 183 | +I consider unit tests to be cheap as they run very fast compared to typical integration and end-to-end tests. In less than a second, you can run hundreds of unit tests to help you verify that your code works and there are no bugs in your code even with strange inputs. So let's add a couple of more assertions to our tests. |
| 184 | + |
| 185 | +Of course, if you are working on a real app, you might want to be more picky about what kinds of test cases you execute, as it can add up and can make your pipeline run slower or could be annoying to maintain. I'd still start with plenty of test cases, then remove them if you "proved" it's too slow and it's not worth the price. |
| 186 | + |
| 187 | +```dart |
| 188 | +// only spaces (length odd and even) |
| 189 | +expect(isPalindrome(' '), true); |
| 190 | +expect(isPalindrome(' '), true); |
| 191 | +// \n new lines and \t tabs are valid input |
| 192 | +expect(isPalindrome('\nabc\tcba\n'), true); |
| 193 | +expect(isPalindrome('\nabc\tcba '), false); |
| 194 | +// length 1 words are always palindrome |
| 195 | +expect(isPalindrome('a'), true); |
| 196 | +expect(isPalindrome('á'), true); |
| 197 | +expect(isPalindrome('Á'), true); |
| 198 | +// mixed strings |
| 199 | +expect(isPalindrome(' abcABC😀🔗🔧@%\n \t X \t \n%@🔧🔗😀CBAcba '), true); |
| 200 | +``` |
| 201 | + |
| 202 | +## Are our tests clean? |
| 203 | + |
| 204 | +Well, you might notice some issues with our tests. We repeat code a lot and the tests require too much typing. Wouldn't it be nicer if we could just write something like: |
| 205 | + |
| 206 | +``` |
| 207 | +// NOT ACTUAL DART CODE YET |
| 208 | +// C magically runs all expectations and function calls in the background. |
| 209 | +C('abba', true), |
| 210 | +C('Abba', false) |
| 211 | +C('baba', false), |
| 212 | +... |
| 213 | +``` |
| 214 | + |
| 215 | +Well, hell yeah, it would! We will come back to cleaning up our unit tests later. We will also refactor the tests to support multiple solutions easily. |
| 216 | + |
| 217 | +# Solve the problem |
| 218 | + |
| 219 | +Oh, finally, we can start working on a solution. On *solutions*, plural! |
| 220 | + |
| 221 | +## Compare characters |
| 222 | + |
| 223 | +Let's just think a bit about what we really need to do in order to determine whether a string is palindrome or not. |
| 224 | + |
| 225 | +What we need to do is check whether the first and last string match. Then we compare the second letter with the penultimate letter. And continue until we get to the middle of the string. |
| 226 | + |
| 227 | +It's a good example that real life problem-solving does not always match the tutorials. In tutorials, people always start with the solution that performs the worst. Then, they show you a slight improvement over that solution. Then, you prepare to have your mind blown, and read the last solution that you never would have thought of, and realize it's blazingly fast. In real life, sometimes your first instinct is right, and the simplest approach provides the fastest solution. You think more about the issue, find another possible solutions, and yet, still your first approach is the best. |
| 228 | + |
| 229 | +I like this solution as it's very simple, matches how I think about the problem, doesn't require additional data structures and it's fast: both for short strings and assymptotically. |
| 230 | + |
| 231 | +### Compare characters - show the code |
| 232 | + |
| 233 | +We need to loop over the characters. |
| 234 | + |
| 235 | +Getting the range right requires some thought, so let's be careful not to overlook things. |
| 236 | + |
| 237 | +For odd lengths: |
| 238 | + |
| 239 | + |
| 240 | +* length = 1, it's automatically palindrome |
| 241 | +* length = 3, check range (index): 0 |
| 242 | +* length = 5, check range: 0-1 |
| 243 | +* length = 7, check range: 0-2 |
| 244 | +* ... |
| 245 | + |
| 246 | +For even lengths: |
| 247 | + |
| 248 | +* length = 0, throw exception |
| 249 | +* length = 2, check range: 0 |
| 250 | +* length = 4, check range: 0-1 |
| 251 | +* length = 6, check range: 0-2 |
| 252 | +* ... |
| 253 | + |
| 254 | +Let's write the `for` loop |
| 255 | + |
| 256 | +TODO: write code with strings first |
| 257 | + |
| 258 | +https://medium.com/@hvost/why-you-should-not-use-emojis-in-your-passwords-b8db0607e169#.ee3f1qr43 |
| 259 | +https://www.joelonsoftware.com/2003/10/08/the-absolute-minimum-every-software-developer-absolutely-positively-must-know-about-unicode-and-character-sets-no-excuses/ |
| 260 | +https://blog.jonnew.com/posts/poo-dot-length-equals-two |
| 261 | + |
| 262 | +TODO: Write code with runes |
| 263 | + |
| 264 | +TODO: HOW TO SUPPORT SURROGATE PAIRS? |
| 265 | + |
| 266 | +TODO: Solution class. Rethink naming. ASCII, NON ASCII, EMOJI, UNICODE, SURROGATE PAIRS... |
| 267 | + |
| 268 | + |
| 269 | + |
| 270 | + |
| 271 | +```dart |
| 272 | +bool isPalindrome(String input) { |
| 273 | + for () |
| 274 | +} |
| 275 | +``` |
| 276 | + |
| 277 | + |
| 278 | +For odd numbers, we |
| 279 | + |
| 280 | +Before we continue, let's codify the limitations of some of the limitations that we found out about the algorithms. |
| 281 | + |
| 282 | + |
| 283 | + |
| 284 | + |
| 285 | +## Reverse and compare |
| 286 | + |
| 287 | +// TODO: mention there are many ways to reverse... |
| 288 | + |
| 289 | +### Reverse by creating way too many strings |
| 290 | + |
| 291 | +### Reverse by splitting the strings, reversing the array, then converting the array back to a string |
| 292 | + |
| 293 | +### Reverse the string by using Dart's `reverse` method on the `String` class |
| 294 | + |
| 295 | +## Recursive |
| 296 | + |
| 297 | +# What else? |
| 298 | + |
| 299 | +TODO: How else could we make it more complicated? Support arrays. Support byte arrays. Support integers. Support doubles. Ignore letter casing, white spaces and punctuation, so that we can verify palindrome sentences. |
0 commit comments