Skip to content

Commit b3f8338

Browse files
committed
🙈 WIP: Palindrome check initial approach
1 parent 3697cd2 commit b3f8338

File tree

4 files changed

+456
-3
lines changed

4 files changed

+456
-3
lines changed

palindrome_string_check/README.md

Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
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.

palindrome_string_check/TASK.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Write a function that takes in a string and determines if it's a palindrome or not. Return `true` if the string is a palindrome and return `false` if it isn't.
2+
3+
A palindrome is a word, number, phrase, or other sequence of characters which reads the same backward as forward, such as `'madam'` or `'racecar'` or the number `10801`.
4+
5+
https://en.wikipedia.org/wiki/Palindrome
6+
7+
TODO: add links to solutions from other sites maybe not here but in a separate md file?
Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,80 @@
1-
const int x = 2;
1+
import 'package:meta/meta.dart';
2+
3+
const String _empty = 'Empty string is invalid input';
4+
5+
bool _iterativeWithRunes(String s) {
6+
if (s.isEmpty) {
7+
throw ArgumentError(_empty);
8+
}
9+
final List<int> runes = s.runes.toList();
10+
for (int i = 0; i < runes.length / 2; i++) {
11+
if (runes[i] != runes[runes.length - i - 1]) {
12+
return false;
13+
}
14+
}
15+
return true;
16+
}
17+
18+
bool _reverseRunesAndCompare(String s) {
19+
if (s.isEmpty) {
20+
throw ArgumentError(_empty);
21+
}
22+
final String reversed = String.fromCharCodes(s.runes.toList().reversed);
23+
return reversed == s;
24+
}
25+
26+
// bool _reverseStringAndCompare(String s) {
27+
// if (s.isEmpty) {
28+
// throw ArgumentError(_empty);
29+
// }
30+
// final String reversed = s.split('').toList().reversed.join('');
31+
// return reversed == s;
32+
// }
33+
34+
/// The signature in wich we expect the palindrome check solutions.
35+
typedef PalindromeCheck = bool Function(String);
36+
37+
/// Class representing a palindrome check solution and its limitations
38+
class Solution {
39+
const Solution({
40+
@required this.name,
41+
@required this.fn,
42+
@required this.unicode,
43+
@required this.surrogatePairs,
44+
});
45+
46+
/// Algorithm checking whether a string is a palindrome or not.
47+
final PalindromeCheck fn;
48+
49+
/// Name of the solution.
50+
///
51+
/// Used for prettier output in unit tests.
52+
final String name;
53+
54+
/// Supports unicode strings.
55+
final bool unicode;
56+
57+
/// Supports surrogate pairs.
58+
final bool surrogatePairs;
59+
}
60+
61+
const List<Solution> solutions = <Solution>[
62+
Solution(
63+
name: 'Iterative with runes',
64+
fn: _iterativeWithRunes,
65+
unicode: true,
66+
surrogatePairs: false,
67+
),
68+
Solution(
69+
name: 'Reverse runes and compare',
70+
fn: _reverseRunesAndCompare,
71+
unicode: true,
72+
surrogatePairs: false,
73+
),
74+
// Solution(
75+
// name: 'Reverse string and compare',
76+
// fn: _reverseStringAndCompare,
77+
// unicode: false,
78+
// surrogatePairs: false,
79+
// ),
80+
];

0 commit comments

Comments
 (0)