Трошки морфології на php
Так вже сталось, що framework, яким я користуюсь для розробки переважної більшості проектів за останній рік (і блог цей входить в їх число), був за останні тижні суттєво мною перероблений. Вирішив додати туди функціонал для автоматичного створення деяких об'єктів (моделей та контролерів). Для цього необхідними є перевірки: чи наявні такі об'єкти серед глобальних змінних та чи були продекларовані раніше всі необхідні класи. Зрозуміло, що на вхід функції для завантаження моделі чи контролера може потрапити назва сутності як в однині так і множині, а завантажувач має вже сам розбиратись, з яким іменем він має створити об'єкт і якого саме класу.
Така приємна штука непокоїла мене давно, з часів мого знайомства з Ruby on Rails. Але все ж знаходилась маса приводів чому це мені не треба робити - економія часу на виклик функції, та і так же все працює, немає власного часу на те щоб посидіти за цією задачею. А тут вже вирішив, що з кодом треба попрацювати, так чому б не додати таку функцію? Тому поставив собі задачу розробити функцій якій б повертали множину та однину переданого параметра для англійської мови
Те що я знайшов..
Те що я знайшов в Інтернет з цього приводу мене не втішило. Всі пропонували схему, коли подається основа слова та закінчення, а функція в залежності від випадку обирає потрібне. Чи то передається основа, а функція використовує технологію gettext. Не буде ж моя функція тягати за собою словник слів та відповідних закінчень у множині та однині. А передавати в функцію можливі закінчення я не збирався - про який автоматизм тоді йтиме мова?
Втішитись варіантом додання літери 's' на кінець слова я теж не міг, бо одразу на думку спадали десятки слів, де закінчення було іншим, та й впевнений що можна це зробити краще, якщо ж хлопці в 37signals зробили це нормально.
Тому пошукав я в іншому напрямку. І знайшов гарну статтю про множину в англійській мові. Здивувало, що після шкільного та університетського курсу в голові не залишилось всієї необхідної інформації та відкрив для себе доволі багато нового (яким користувався, попри те що не знав чому ж має бути так). І вирішив розробити необхідний функціонал сам.
Результати роботи
Проаналізувавши статтю я написав ось таку громіздку функцію, для отримання множини:
function plural($word) {
$pos = strlen($word)-1;
// Check last letter
switch ($word{$pos}) {
case 's': // Maby already plural
if ($word{$pos-1} == 's')
return $word.'es';
else
return $word;
case 'o': // Check for exceptions
if ($word == 'kilo' || $word == 'photo' || $word == 'piano' || $word == 'soprano')
break;
return $word.'es';
case 'h': // Check letter before lat
$letter = $word{$pos-1};
if ($letter == 's' || $letter == 'c')
return $word.'es';
break;
case 'e': // Check previous letter
if ($word{strlen($word)-2} != 'f') {
// Check for exceptions
switch ($word) {
case 'mouse': return 'mice';
case 'goose': return 'geese';
case 'louse': return 'lice';
}
break;
}
$word = substr($word, 0, $pos);
case 'f': $word = substr($word, 0, strlen($word)-1).'v';
return $word.'es';
case 'x': // Check for exception
if ($word == 'ox')
return 'oxen';
return $word.'es';
case 'y': // Check before last letter
switch ($word{$pos-1}) {
case 'a':
case 'e':
case 'i':
case 'o':
case 'u':
case 'y': break;
default: return substr($word, 0, $pos).'ies';
}
break;
default: // Check for exceptions
switch ($word) {
case 'foot': return 'feet';
case 'tooth': return 'teeth';
case 'man': return 'men';
case 'woman': return 'women';
case 'child': return 'children';
case 'sheep':
case 'deer': return $word;
}
}
return $word.'s';
}
Ця функція перевіряє останню літеру слова і якщо це необхідно додає не лише s, а й інші літери, та провидить заміни, необхідні для отримання множини від таких слів як "wolf" (множина "wolves", тут слід змінити літеру "f" на літеру "v", та додати закінчення "es"). Якщо жодне з правил не підходить, слово перевіряється на виключення, і якщо це не виключення, лише тоді просто додається літера "s". Якщо почитати статтю в якій я знайшов правила та код функції.
А на основі вже написаної функції, я зміг написати обернену:
function singular($word) {
$pos = strlen($word)-1;
// Check is word can be in plural form
if ($word{$pos} != 's')
// Check for exception word
switch ($word) {
case 'mice': return 'mouse';
case 'geese': return 'goose';
case 'lise': return 'louse';
case 'feet': return 'foot';
case 'teeth': return 'tooth';
case 'men': return 'man';
case 'women': return 'woman';
case 'children':return 'child';
case 'oxen': return 'ox';
default: return $word;
}
// Check letter before last
switch ($word{$pos-1}) {
// Already singular form
case 's': return $word;
case 'e': // Additinal checking. Needed to find correct forms
switch ($word{$pos-2}) {
case 'h': // Check additional rules
if ($word{$pos-3} != 's' && $word{$pos-3} != 'c')
break;
return substr($word, 0, $pos-1);
case 'i': // Remplace 'i' with 'y' and remove last two chars
return substr($word, 0, $pos-2).'y';
case 'v': // Replace 'v' with 'f'
$word{$pos-2} = 'f';
if ($word == 'knifes' || $word == 'wifes' || $word == 'lifes')
break;
case 'o': // Need to remove last two chars
case 'x': // Remove last two chars
return substr($word, 0, $pos-1);
case 's': // Check for letter before s
if ($word{$pos-3} == 's')
return substr($word, 0, $pos-1);
}
}
// Usual way - remove last 's' letter
return substr($word, 0, $pos);
}
Вона працює в іншому порядку, однак враховуються ті ж самісінькі правила та виключення.
Як я перевіряв ці функції на коректність
Я б справді був гордим, якби весь той код написав з першого разу так, щоб він ідеально працював, не пропустив би жодне правило та ніде не помилився. Однак я такого не можу, та й не знаю жодної людини яка б в сотні рядків коду (разом ці функції з коментарями - 103 рядки :-)) не зробила жодної помилки. Тому потрібно було ці функції перевіряти. Звичайно ж вводити по одному слову, та звіряти зі словником справа нудна та неприємна. Тому вирішив написати тести.
Для того що писати тести було легше, написав допоміжну функцію:
function check($tests, $function) {
$flag = true;
foreach ($tests as $key => $value)
if ($function($key) != $value) {
$flag = false;
echo 'For '.$key.' retult '.($function($key)).' not matchs with '.$value."\n";
}
if ($flag)
echo "Test passed\n";
else
echo "Test failed\n";
}
Першим аргументом передається масив ключі якого є текстовими даними, а значення результатами, не необхідно на цих даних отримувати. Другим же параметром передається функція.
З використанням цієї функції я написав набір тестів, який би відображав всі правила, які я знайшов:
echo "\n'O' ends test: \n";
check(array('tomato' => 'tomatoes', 'potato' => 'potatoes'), plural);
echo "'O' exceptions: \n";
check(array('kilo' => 'kilos', 'photo' => 'photos', 'piano' => 'pianos'), plural);
echo "'S' ends test: \n";
check(array('kilos' => 'kilos', 'checks' => 'checks', 'kiss' => 'kisses', 'rss' => 'rsses'), plural);
echo "'X' ends test: \n";
check(array('box' => 'boxes'), plural);
echo "'H' ends test: \n";
check(array('bush' => 'bushes', 'church' => 'churches'), plural);
echo "'F' and 'E' ends test: \n";
check(array('life' => 'lives', 'wife' => 'wives', 'wolf' => 'wolves'), plural);
echo "Check exceptions: \n";
check(array('foot' => 'feet', 'ox' => 'oxen', 'man' => 'men', 'sheep' => 'sheep'), plural);
echo "'Y' ends test: \n";
check(array('money' => 'moneys', 'baby' => 'babies'), plural);
echo "Normal 's' adding: \n";
check(array('test' => 'tests', 'network' => 'networks'), plural);
// Check all rules for singular
echo "\n'O' ends: \n";
check(array('tomatoes' => 'tomato', 'potatoes' => 'potato', 'kilos' => 'kilo', 'pianos' => 'piano'), singular);
echo "'E' and 'F' ends: \n";
check(array('lives' => 'life', 'wives' => 'wife', 'wolves' => 'wolf', 'knives' => 'knife', 'messages' => 'message'), singular);
echo "'Y' end test: \n";
check(array('moneys' => 'money', 'babies' => 'baby'), singular);
echo "'X' end test: \n";
check(array('boxes' => 'box', 'oxen' => 'ox'), singular);
echo "'H' end test: \n";
check(array('bushes' => 'bush', 'churches' => 'church'), singular);
echo "'S' test ends: \n";
check(array('kisses' => 'kiss', 'tests' => 'test', 'kiss' => 'kiss', 'miss' => 'miss'), singular);
echo "Check for exceptions: \n";
check(array('men' => 'man', 'sheep' => 'sheep', 'women' => 'woman'), singular);
Не з першого разу, однак все ж таки вдалось мені досягти коректного проходження всіх тестів. Ці самі тести дозволили мені зберегти масу часу, хоча на написання самих тестів я витратив не більше години. Та й взагалі я витратив на все близько 5-6 годин (включаючи внесення відповідних змін у framework).
Використання
Така методика перевірки результатів виконання власного програмного коду в той чи інший момент стає в нагоді кожному программісту. Взагалі то є цілий стиль програмування орієнтований на написання тестів, причому ще до початку написання самого коду.
Впевнений, що коду функцій можна знайти не тільки таке використання. Наприклад, якщо комусь треба буде виводити слова на сайт. Зрозуміло ж, що людині буде приємніше читати щось на зразок "You recived one message" та "You recived two messages", замість "Messages: 1". Я раніше використовував інший підхід у таких випадках, а тепер ось отримав непоганий інструмент, який при першій можливості чи необхадності хочу локалізувати. Якщо Ви зробите це раніше, буду вдячний якщо воділитесь результатом. Також буду вдячний, якщо повідомите про помилки в коді. Писати можна в коментарі чи на пошту grandse(at)urk.net
Дякую всім за увагу!
Коментарі:
упс.. Якесь воно більщ ніж "глибоке за філософським змістом" вийшло. А саме над ним більше всього розмірковував. От і накрутив казна що. Дякую, що сказав.
І все одно початок якийсь складний та нецікавий.
Буду вдячний, якщо хтось скаже, чи приходять на пошту листи про коментарі?



