Сегодня рассмотрим первую ката «Палиндром». Напомню, что ката в программировании, это упражнение, которое должно занимать не более 10 минут, которое выполняется снова и снова, для совершенствования скорости и качества написания кода. Определение, является ли слово палиндром в качестве первого ката выбрано по 2 причинам, во-первых, задача поиска палиндрома достаточно проста и её написание займет точно не более 10 минут, во-вторых она не столь тривиальна, как кажется на первый взгляд, но в сети очень легко найти примеры достаточно сложных палиндромов для тестов.
Начнём.
Первым тестом проверим строку на null, есть она равна null пусть метод, определяющий палиндром, возвращает исключение «ArgumentNullException» с именем параметра «value».
Первый тест будет выглядеть следующим образом:
public void StringIsNull()
{
string str = null;
try
{
var result = str.IsPalindrome();
}
catch (ArgumentNullException e)
{
Assert.AreEqual("value", e.ParamName);
}
}
Теперь добьёмся того, чтобы наш тест проходил. Создадим расширение для типа String. С методом IsPalindrome. Пусть он всегда возвращает true. А если входящий объект равен null, пусть возвращается исключение ArgumentNullException
if(str == null) throw new ArgumentNullException("value");
Запустим тест, убедимся что он проходит. Теперь пишем следующий тест. В нем мы проверим, является ли палиндромом пустая строка. Думаю, что является.
public void StringIsEmpty()
{
var str = "";
var result = str.IsPalindrome();
Assert.IsTrue(result);
}
Запустим наш тест, и убедимся, что он проходит. Следующий тест будет определять палиндром строки из одного символа.
public void StringContainsOneLetter()
{
var str = "a";
var result = str.IsPalindrome();
Assert.IsTrue(result);
}
Данный тест также проходит, ведь в существующей реализации всё что не является null, является палиндромом. Пришло проверить слово, не являющееся палиндромом.
public void NotPalindrome()
{
var str = "abcdefg";
var result = str.IsPalindrome();
Assert.IsFalse(result);
}
Данный тест не прошёл, пришло время дополнять метод. Реализуем простую логику проверки строки. Будем проходить от начала строки до её середины, сравнивая первый символ с последним, второй с предпоследним и т.д. Если на каком-то этапе символы не совпадают, возвращаем false.
if(str == null) throw new ArgumentNullException("value");
for (var i = 0; i < str.Length/2; i++)
{
if (str[i] != str[str.Length - i - 1])
{
return false;
}
}
return true;
Запускаем тесты, все они теперь проходят. Следующим тестом проверим строку, содержащую символы различного регистра, ведь слова могут быть записаны по-разному, но для определения палиндрома это не должно иметь значения.
public void IsPalindromeWithRegister()
{
var str = "Shahs";
var result = str.IsPalindrome();
Assert.IsTrue(result);
}
Дополним сравнение символов приведением к нижнему регистру.
for (var i = 0; i < str.Length/2; i++)
{
if (char.ToLower(str[i]) != char.ToLower(str[str.Length - i - 1]))
{
return false;
}
}
Теперь сделаем немного более интересный тест. Ведь в длинных предложениях палиндромах, обязательно будет встречаться символы пунктуации и разделители.
public void IsPalindromeWithPunctuation()
{
var str = "Was it a car, or a cat I saw?";
var result = str.IsPalindrome();
Assert.IsTrue(result);
}
Дополним метод так, чтобы в строке учитывались только буквы и цифры.
public static bool IsPalindrome(this string str)
{
if(str == null) throw new ArgumentNullException("value");
str = string.Join("", str.Where(char.IsLetterOrDigit).Select(char.ToLower));
for (var i = 0; i < str.Length/2; i++)
{
if (char.ToLower(str[i]) != char.ToLower(str[str.Length - i - 1]))
{
return false;
}
}
return true;
}
Теперь все тесты проходят.
Можно еще добавить несколько тестов, пытаясь «сломать» наше расширение. Убедившись, что все они проходят, можно завершать нашу первую ката.

Увидеть исходный код данного упражнения можно в
GitHub оепозитории по
ссылке