Added the Porter Stemmer module to improve searches. This doesn't deal with some...
[yaffs-website] / web / modules / contrib / porterstemmer / src / Porter2.php
1 <?php
2
3 namespace Drupal\porterstemmer;
4
5 /**
6  * PHP Implementation of the Porter2 Stemming Algorithm.
7  *
8  * See http://snowball.tartarus.org/algorithms/english/stemmer.html .
9  */
10 class Porter2 {
11
12   /**
13    * Computes the stem of the word.
14    *
15    * @return string
16    *   The word's stem.
17    */
18   public static function stem($word) {
19
20     $exceptions = array(
21       'skis' => 'ski',
22       'skies' => 'sky',
23       'dying' => 'die',
24       'lying' => 'lie',
25       'tying' => 'tie',
26       'idly' => 'idl',
27       'gently' => 'gentl',
28       'ugly' => 'ugli',
29       'early' => 'earli',
30       'only' => 'onli',
31       'singly' => 'singl',
32       'sky' => 'sky',
33       'news' => 'news',
34       'howe' => 'howe',
35       'atlas' => 'atlas',
36       'cosmos' => 'cosmos',
37       'bias' => 'bias',
38       'andes' => 'andes',
39     );
40
41     // Process exceptions.
42     if (isset($exceptions[$word])) {
43       $word = $exceptions[$word];
44     }
45     elseif (strlen($word) > 2) {
46       // Only execute algorithm on words that are longer than two letters.
47       $word = self::prepare($word);
48       $word = self::step0($word);
49       $word = self::step1a($word);
50       $word = self::step1b($word);
51       $word = self::step1c($word);
52       $word = self::step2($word);
53       $word = self::step3($word);
54       $word = self::step4($word);
55       $word = self::step5($word);
56     }
57     return strtolower($word);
58   }
59
60   /**
61    * Set initial y, or y after a vowel, to Y.
62    *
63    * @param string $word
64    *   The word to stem.
65    *
66    * @return string $word
67    *   The prepared word.
68    */
69   protected static function prepare($word) {
70     $inc = 0;
71     if (strpos($word, "'") === 0) {
72       $word = substr($word, 1);
73     }
74     while ($inc <= strlen($word)) {
75       if (substr($word, $inc, 1) === 'y' && ($inc == 0 || self::isVowel($inc - 1, $word))) {
76         $word = substr_replace($word, 'Y', $inc, 1);
77       }
78       $inc++;
79     }
80     return $word;
81   }
82
83   /**
84    * Search for the longest among the "s" suffixes and removes it.
85    *
86    * @param string $word
87    *   The word to stem.
88    *
89    * @return string $word
90    *   The modified word.
91    */
92   protected static function step0($word) {
93     $found = FALSE;
94     $checks = array("'s'", "'s", "'");
95     foreach ($checks as $check) {
96       if (!$found && self::hasEnding($word, $check)) {
97         $word = self::removeEnding($word, $check);
98         $found = TRUE;
99       }
100     }
101     return $word;
102   }
103
104   /**
105    * Handles various suffixes, of which the longest is replaced.
106    *
107    * @param string $word
108    *   The word to stem.
109    *
110    * @return string $word
111    *   The modified word.
112    */
113   protected static function step1a($word) {
114     $found = FALSE;
115     if (self::hasEnding($word, 'sses')) {
116       $word = self::removeEnding($word, 'sses') . 'ss';
117       $found = TRUE;
118     }
119     $checks = array('ied', 'ies');
120     foreach ($checks as $check) {
121       if (!$found && self::hasEnding($word, $check)) {
122         // @todo: check order here.
123         $length = strlen($word);
124         $word = self::removeEnding($word, $check);
125         if ($length > 4) {
126           $word .= 'i';
127         }
128         else {
129           $word .= 'ie';
130         }
131         $found = TRUE;
132       }
133     }
134     if (self::hasEnding($word, 'us') || self::hasEnding($word, 'ss')) {
135       $found = TRUE;
136     }
137     // Delete if preceding word part has a vowel not immediately before the s.
138     if (!$found && self::hasEnding($word, 's') && self::containsVowel(substr($word, 0, -2))) {
139       $word = self::removeEnding($word, 's');
140     }
141     return $word;
142   }
143
144   /**
145    * Handles various suffixes, of which the longest is replaced.
146    *
147    * @param string $word
148    *   The word to stem.
149    *
150    * @return string $word
151    *   The modified word.
152    */
153   protected static function step1b($word) {
154     $exceptions = array(
155       'inning',
156       'outing',
157       'canning',
158       'herring',
159       'earring',
160       'proceed',
161       'exceed',
162       'succeed',
163     );
164     if (in_array($word, $exceptions)) {
165       return $word;
166     }
167     $checks = array('eedly', 'eed');
168     foreach ($checks as $check) {
169       if (self::hasEnding($word, $check)) {
170         if (self::r($word, 1) !== strlen($word)) {
171           $word = self::removeEnding($word, $check) . 'ee';
172         }
173         return $word;
174       }
175     }
176     $checks = array('ingly', 'edly', 'ing', 'ed');
177     $second_endings = array('at', 'bl', 'iz');
178     foreach ($checks as $check) {
179       // If the ending is present and the previous part contains a vowel.
180       if (self::hasEnding($word, $check) && self::containsVowel(substr($word, 0, -strlen($check)))) {
181         $word = self::removeEnding($word, $check);
182         foreach ($second_endings as $ending) {
183           if (self::hasEnding($word, $ending)) {
184             return $word . 'e';
185           }
186         }
187         // If the word ends with a double, remove the last letter.
188         $double_removed = self::removeDoubles($word);
189         if ($double_removed != $word) {
190           $word = $double_removed;
191         }
192         elseif (self::isShort($word)) {
193           // If the word is short, add e (so hop -> hope).
194           $word .= 'e';
195         }
196         return $word;
197       }
198     }
199     return $word;
200   }
201
202   /**
203    * Replaces suffix y or Y with i if after non-vowel not @ word begin.
204    *
205    * @param string $word
206    *   The word to stem.
207    *
208    * @return string $word
209    *   The modified word.
210    */
211   protected static function step1c($word) {
212     if ((self::hasEnding($word, 'y') || self::hasEnding($word, 'Y')) && strlen($word) > 2 && !(self::isVowel(strlen($word) - 2, $word))) {
213       $word = self::removeEnding($word, 'y');
214       $word .= 'i';
215     }
216     return $word;
217   }
218
219   /**
220    * Implements step 2 of the Porter2 algorithm.
221    *
222    * @param string $word
223    *   The word to stem.
224    *
225    * @return string $word
226    *   The modified word.
227    */
228   protected static function step2($word) {
229     $checks = array(
230       "ization" => "ize",
231       "iveness" => "ive",
232       "fulness" => "ful",
233       "ational" => "ate",
234       "ousness" => "ous",
235       "biliti" => "ble",
236       "tional" => "tion",
237       "lessli" => "less",
238       "fulli" => "ful",
239       "entli" => "ent",
240       "ation" => "ate",
241       "aliti" => "al",
242       "iviti" => "ive",
243       "ousli" => "ous",
244       "alism" => "al",
245       "abli" => "able",
246       "anci" => "ance",
247       "alli" => "al",
248       "izer" => "ize",
249       "enci" => "ence",
250       "ator" => "ate",
251       "bli" => "ble",
252       "ogi" => "og",
253     );
254     foreach ($checks as $find => $replace) {
255       if (self::hasEnding($word, $find)) {
256         if (self::inR1($word, $find)) {
257           $word = self::removeEnding($word, $find) . $replace;
258         }
259         return $word;
260       }
261     }
262     if (self::hasEnding($word, 'li')) {
263       if (strlen($word) > 4 && self::validLi(self::charAt(-3, $word))) {
264         $word = self::removeEnding($word, 'li');
265       }
266     }
267     return $word;
268   }
269
270   /**
271    * Implements step 3 of the Porter2 algorithm.
272    *
273    * @param string $word
274    *   The word to stem.
275    *
276    * @return string $word
277    *   The modified word.
278    */
279   protected static function step3($word) {
280     $checks = array(
281       'ational' => 'ate',
282       'tional' => 'tion',
283       'alize' => 'al',
284       'icate' => 'ic',
285       'iciti' => 'ic',
286       'ical' => 'ic',
287       'ness' => '',
288       'ful' => '',
289     );
290     foreach ($checks as $find => $replace) {
291       if (self::hasEnding($word, $find)) {
292         if (self::inR1($word, $find)) {
293           $word = self::removeEnding($word, $find) . $replace;
294         }
295         return $word;
296       }
297     }
298     if (self::hasEnding($word, 'ative')) {
299       if (self::inR2($word, 'ative')) {
300         $word = self::removeEnding($word, 'ative');
301       }
302     }
303     return $word;
304   }
305
306   /**
307    * Implements step 4 of the Porter2 algorithm.
308    *
309    * @param string $word
310    *   The word to stem.
311    *
312    * @return string $word
313    *   The modified word.
314    */
315   protected static function step4($word) {
316     $checks = array(
317       'ement',
318       'ment',
319       'ance',
320       'ence',
321       'able',
322       'ible',
323       'ant',
324       'ent',
325       'ion',
326       'ism',
327       'ate',
328       'iti',
329       'ous',
330       'ive',
331       'ize',
332       'al',
333       'er',
334       'ic',
335     );
336     foreach ($checks as $check) {
337       // Among the suffixes, if found and in R2, delete.
338       if (self::hasEnding($word, $check)) {
339         if (self::inR2($word, $check)) {
340           if ($check !== 'ion' || in_array(self::charAt(-4, $word), array('s', 't'))) {
341             $word = self::removeEnding($word, $check);
342           }
343         }
344         return $word;
345       }
346     }
347     return $word;
348   }
349
350   /**
351    * Implements step 5 of the Porter2 algorithm.
352    *
353    * @param string $word
354    *   The word to stem.
355    *
356    * @return string $word
357    *   The modified word.
358    */
359   protected static function step5($word) {
360     if (self::hasEnding($word, 'e')) {
361       // Delete if in R2, or in R1 and not preceded by a short syllable.
362       if (self::inR2($word, 'e') || (self::inR1($word, 'e') && !self::isShortSyllable($word, strlen($word) - 3))) {
363         $word = self::removeEnding($word, 'e');
364       }
365       return $word;
366     }
367     if (self::hasEnding($word, 'l')) {
368       // Delete if in R2 and preceded by l.
369       if (self::inR2($word, 'l') && self::charAt(-2, $word) == 'l') {
370         $word = self::removeEnding($word, 'l');
371       }
372     }
373     return $word;
374   }
375
376   /**
377    * Removes certain double consonants from the word's end.
378    *
379    * @param string $word
380    *   The word to stem.
381    *
382    * @return string $word
383    *   The modified word.
384    */
385   protected static function removeDoubles($word) {
386     $doubles = array('bb', 'dd', 'ff', 'gg', 'mm', 'nn', 'pp', 'rr', 'tt');
387     foreach ($doubles as $double) {
388       if (substr($word, -2) == $double) {
389         $word = substr($word, 0, -1);
390         break;
391       }
392     }
393     return $word;
394   }
395
396   /**
397    * Checks whether a character is a vowel.
398    *
399    * @param int $position
400    *   The character's position.
401    * @param string $word
402    *   The word in which to check.
403    * @param string[] $additional
404    *   (optional) Additional characters that should count as vowels.
405    *
406    * @return bool
407    *   TRUE if the character is a vowel, FALSE otherwise.
408    */
409   protected static function isVowel($position, $word, $additional = array()) {
410     $vowels = array_merge(array('a', 'e', 'i', 'o', 'u', 'y'), $additional);
411     return in_array(self::charAt($position, $word), $vowels);
412   }
413
414   /**
415    * Retrieves the character at the given position.
416    *
417    * @param int $position
418    *   The 0-based index of the character. If a negative number is given, the
419    *   position is counted from the end of the string.
420    * @param string $word
421    *   The word from which to retrieve the character.
422    *
423    * @return string
424    *   The character at the given position, or an empty string if the given
425    *   position was illegal.
426    */
427   protected static function charAt($position, $word) {
428     $length = strlen($word);
429     if (abs($position) >= $length) {
430       return '';
431     }
432     if ($position < 0) {
433       $position += $length;
434     }
435     return $word[$position];
436   }
437
438   /**
439    * Determines whether the word ends in a "vowel-consonant" suffix.
440    *
441    * Unless the word is only two characters long, it also checks that the
442    * third-last character is neither "w", "x" nor "Y".
443    *
444    * @param int|null $position
445    *   (optional) If given, do not check the end of the word, but the character
446    *   at the given position, and the next one.
447    *
448    * @return bool
449    *   TRUE if the word has the described suffix, FALSE otherwise.
450    */
451   protected static function isShortSyllable($word, $position = NULL) {
452     if ($position === NULL) {
453       $position = strlen($word) - 2;
454     }
455     // A vowel at the beginning of the word followed by a non-vowel.
456     if ($position === 0) {
457       return self::isVowel(0, $word) && !self::isVowel(1, $word);
458     }
459     // Vowel followed by non-vowel other than w, x, Y and preceded by
460     // non-vowel.
461     $additional = array('w', 'x', 'Y');
462     return !self::isVowel($position - 1, $word) && self::isVowel($position, $word) && !self::isVowel($position + 1, $word, $additional);
463   }
464
465   /**
466    * Determines whether the word is short.
467    *
468    * A word is called short if it ends in a short syllable and if R1 is null.
469    *
470    * @return bool
471    *   TRUE if the word is short, FALSE otherwise.
472    */
473   protected static function isShort($word) {
474     return self::isShortSyllable($word) && self::r($word, 1) == strlen($word);
475   }
476
477   /**
478    * Determines the start of a certain "R" region.
479    *
480    * R is a region after the first non-vowel following a vowel, or end of word.
481    *
482    * @param int $type
483    *   (optional) 1 or 2. If 2, then calculate the R after the R1.
484    *
485    * @return int
486    *   The R position.
487    */
488   protected static function r($word, $type = 1) {
489     $inc = 1;
490     if ($type === 2) {
491       $inc = self::r($word, 1);
492     }
493     elseif (strlen($word) > 5) {
494       $prefix_5 = substr($word, 0, 5);
495       if ($prefix_5 === 'gener' || $prefix_5 === 'arsen') {
496         return 5;
497       }
498       if (strlen($word) > 5 && substr($word, 0, 6) === 'commun') {
499         return 6;
500       }
501     }
502
503     while ($inc <= strlen($word)) {
504       if (!self::isVowel($inc, $word) && self::isVowel($inc - 1, $word)) {
505         $position = $inc;
506         break;
507       }
508       $inc++;
509     }
510     if (!isset($position)) {
511       $position = strlen($word);
512     }
513     else {
514       // We add one, as this is the position AFTER the first non-vowel.
515       $position++;
516     }
517     return $position;
518   }
519
520   /**
521    * Checks whether the given string is contained in R1.
522    *
523    * @param string $string
524    *   The string.
525    *
526    * @return bool
527    *   TRUE if the string is in R1, FALSE otherwise.
528    */
529   protected static function inR1($word, $string) {
530     $r1 = substr($word, self::r($word, 1));
531     return strpos($r1, $string) !== FALSE;
532   }
533
534   /**
535    * Checks whether the given string is contained in R2.
536    *
537    * @param string $string
538    *   The string.
539    *
540    * @return bool
541    *   TRUE if the string is in R2, FALSE otherwise.
542    */
543   protected static function inR2($word, $string) {
544     $r2 = substr($word, self::r($word, 2));
545     return strpos($r2, $string) !== FALSE;
546   }
547
548   /**
549    * Checks whether the word ends with the given string.
550    *
551    * @param string $string
552    *   The string.
553    *
554    * @return bool
555    *   TRUE if the word ends with the given string, FALSE otherwise.
556    */
557   protected static function hasEnding($word, $string) {
558     $length = strlen($string);
559     if ($length > strlen($word)) {
560       return FALSE;
561     }
562     return (substr_compare($word, $string, -1 * $length, $length) === 0);
563   }
564
565   /**
566    * Removes a given string from the end of the current word.
567    *
568    * Does not check whether the ending is actually there.
569    *
570    * @param string $string
571    *   The ending to remove.
572    */
573   protected static function removeEnding($word, $string) {
574     return substr($word, 0, -strlen($string));
575   }
576
577   /**
578    * Checks whether the given string contains a vowel.
579    *
580    * @param string $string
581    *   The string to check.
582    *
583    * @return bool
584    *   TRUE if the string contains a vowel, FALSE otherwise.
585    */
586   protected static function containsVowel($string) {
587     $inc = 0;
588     $return = FALSE;
589     while ($inc < strlen($string)) {
590       if (self::isVowel($inc, $string)) {
591         $return = TRUE;
592         break;
593       }
594       $inc++;
595     }
596     return $return;
597   }
598
599   /**
600    * Checks whether the given string is a valid -li prefix.
601    *
602    * @param string $string
603    *   The string to check.
604    *
605    * @return bool
606    *   TRUE if the given string is a valid -li prefix, FALSE otherwise.
607    */
608   protected static function validLi($string) {
609     return in_array($string, array(
610       'c',
611       'd',
612       'e',
613       'g',
614       'h',
615       'k',
616       'm',
617       'n',
618       'r',
619       't',
620     ));
621   }
622
623 }