root/tags/release_day_23/lib/PorterStemmer.class.php

Revision 58, 13.7 kB (checked in by fabien, 3 years ago)

day 21 modifications

  • Property svn:mime-type set to text/x-php
  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
Line 
1 <?php
2     /**
3     * Copyright (c) 2005 Richard Heyes (http://www.phpguru.org/)
4     *
5     * All rights reserved.
6     *
7     * This script is free software; you can redistribute it and/or modify
8     * it under the terms of the GNU General Public License as published by
9     * the Free Software Foundation; either version 2 of the License, or
10     * (at your option) any later version.
11     *
12     * The GNU General Public License can be found at
13     * http://www.gnu.org/copyleft/gpl.html.
14     *
15     * This script is distributed in the hope that it will be useful,
16     * but WITHOUT ANY WARRANTY; without even the implied warranty of
17     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18     * GNU General Public License for more details.
19     */
20     
21     /**
22     * PHP5 Implementation of the Porter Stemmer algorithm. Certain elements
23     * were borrowed from the (broken) implementation by Jon Abernathy.
24     *
25     * Usage:
26     *
27     *  $stem = PorterStemmer::Stem($word);
28     *
29     * How easy is that?
30     */
31     
32     class PorterStemmer
33     {
34         /**
35         * Regex for matching a consonant
36         * @var string
37         */
38         private static $regex_consonant = '(?:[bcdfghjklmnpqrstvwxz]|(?<=[aeiou])y|^y)';
39     
40     
41         /**
42         * Regex for matching a vowel
43         * @var string
44         */
45         private static $regex_vowel = '(?:[aeiou]|(?<![aeiou])y)';
46
47
48         /**
49         * Cache for mass stemmings. Do not use for mass stemmings of *different* words. Use for
50         * stemming words from documents for example, where the same word may crop up multiple times.
51         * @var array
52         */
53         private static $cache = array();
54
55
56         /**
57         * Stems a word. Simple huh?
58         *
59         * @param  string $word  Word to stem
60         * @param  bool   $cache Whether to use caching
61         * @return string        Stemmed word
62         */
63         public static function Stem($word, $cache = false)
64         {
65             if (strlen($word) <= 2) {
66                 return $word;
67             }
68
69             // Check cache
70             if ($cache AND !empty(self::$cache[$word])) {
71                 return self::$cache[$word];
72             }
73             
74             /**
75             * Remove: 've, n't, 'd
76             */
77             $word = preg_replace("/('ve|n't|'d)$/", '', $word);
78
79             $stem = self::step1ab($word);
80             $stem = self::step1c($stem);
81             $stem = self::step2($stem);
82             $stem = self::step3($stem);
83             $stem = self::step4($stem);
84             $stem = self::step5($stem);
85
86             // Store in cache
87             if ($cache) {
88                 self::$cache[$word] = $stem;
89             }
90
91             return $stem;
92         }
93     
94     
95         /**
96         * Step 1
97         */
98         private static function step1ab($word)
99         {
100             // Part a
101             if (substr($word, -1) == 's') {
102
103                    self::replace($word, 'sses', 'ss')
104                 OR self::replace($word, 'ies', 'i')
105                 OR self::replace($word, 'ss', 'ss')
106                 OR self::replace($word, 's', '');
107             }
108
109             // Part b
110             if (substr($word, -2, 1) != 'e' OR !self::replace($word, 'eed', 'ee', 0)) { // First rule
111                 $v = self::$regex_vowel;
112
113                 // ing and ed
114                 if (   preg_match("#$v+#", substr($word, 0, -3)) && self::replace($word, 'ing', '')
115                     OR preg_match("#$v+#", substr($word, 0, -2)) && self::replace($word, 'ed', '')) { // Note use of && and OR, for precedence reasons
116
117                     // If one of above two test successful
118                     if (    !self::replace($word, 'at', 'ate')
119                         AND !self::replace($word, 'bl', 'ble')
120                         AND !self::replace($word, 'iz', 'ize')) {
121
122                         // Double consonant ending
123                         if (    self::doubleConsonant($word)
124                             AND substr($word, -2) != 'll'
125                             AND substr($word, -2) != 'ss'
126                             AND substr($word, -2) != 'zz') {
127                             
128                             $word = substr($word, 0, -1);
129                         
130                         } else if (self::m($word) == 1 AND self::cvc($word)) {
131                             $word .= 'e';
132                         }
133                     }
134                 }
135             }
136
137             return $word;
138         }
139     
140     
141         /**
142         * Step 1c
143         *
144         * @param string $word Word to stem
145         */
146         private static function step1c($word)
147         {
148             $v = self::$regex_vowel;
149
150             if (substr($word, -1) == 'y' && preg_match("#$v+#", substr($word, 0, -1))) {
151                 self::replace($word, 'y', 'i');
152             }
153
154             return $word;
155         }
156
157
158         /**
159         * Step 2
160         *
161         * @param string $word Word to stem
162         */
163         private static function step2($word)
164         {
165             switch (substr($word, -2, 1)) {
166                 case 'a':
167                        self::replace($word, 'ational', 'ate', 0)
168                     OR self::replace($word, 'tional', 'tion', 0);
169                     break;
170
171                 case 'c':
172                        self::replace($word, 'enci', 'ence', 0)
173                     OR self::replace($word, 'anci', 'ance', 0);
174                     break;
175
176                 case 'e':
177                     self::replace($word, 'izer', 'ize', 0);
178                     break;
179
180                 case 'g':
181                     self::replace($word, 'logi', 'log', 0);
182                     break;
183
184                 case 'l':
185                        self::replace($word, 'entli', 'ent', 0)
186                     OR self::replace($word, 'ousli', 'ous', 0)
187                     OR self::replace($word, 'alli', 'al', 0)
188                     OR self::replace($word, 'bli', 'ble', 0)
189                     OR self::replace($word, 'eli', 'e', 0);
190                     break;
191
192                 case 'o':
193                        self::replace($word, 'ization', 'ize', 0)
194                     OR self::replace($word, 'ation', 'ate', 0)
195                     OR self::replace($word, 'ator', 'ate', 0);
196                     break;
197
198                 case 's':
199                        self::replace($word, 'iveness', 'ive', 0)
200                     OR self::replace($word, 'fulness', 'ful', 0)
201                     OR self::replace($word, 'ousness', 'ous', 0)
202                     OR self::replace($word, 'alism', 'al', 0);
203                     break;
204
205                 case 't':
206                        self::replace($word, 'biliti', 'ble', 0)
207                     OR self::replace($word, 'aliti', 'al', 0)
208                     OR self::replace($word, 'iviti', 'ive', 0);
209                     break;
210             }
211
212             return $word;
213         }
214
215     
216         /**
217         * Step 3
218         *
219         * @param string $word String to stem
220         */
221         private static function step3($word)
222         {
223             switch (substr($word, -2, 1)) {
224                 case 'a':
225                     self::replace($word, 'ical', 'ic', 0);
226                     break;
227                     
228                 case 's':
229                        self::replace($word, 'alise', 'al', 0)
230                     OR self::replace($word, 'ness', '', 0);
231                     break;
232                     
233                 case 't':
234                        self::replace($word, 'icate', 'ic', 0)
235                     OR self::replace($word, 'iciti', 'ic', 0);
236                     break;
237                     
238                 case 'u':
239                     self::replace($word, 'ful', '', 0);
240                     break;
241                     
242                 case 'v':
243                     self::replace($word, 'ative', '', 0);
244                     break;
245                     
246                 case 'z':
247                     self::replace($word, 'alize', 'al', 0);
248                     break;
249             }
250             
251             return $word;
252         }
253     
254     
255         /**
256         * Step 4
257         *
258         * @param string $word Word to stem
259         */
260         private static function step4($word)
261         {
262             switch (substr($word, -2, 1)) {
263                 case 'a':
264                     self::replace($word, 'al', '', 1);
265                     break;
266
267                 case 'c':
268                        self::replace($word, 'ance', '', 1)
269                     OR self::replace($word, 'ence', '', 1);
270                     break;
271
272                 case 'e':
273                     self::replace($word, 'er', '', 1);
274                     break;
275
276                 case 'i':
277                     self::replace($word, 'ic', '', 1);
278                     break;
279
280                 case 'l':
281                        self::replace($word, 'able', '', 1)
282                     OR self::replace($word, 'ible', '', 1);
283                     break;
284
285                 case 'n':
286                        self::replace($word, 'ant', '', 1)
287                     OR self::replace($word, 'ement', '', 1)
288                     OR self::replace($word, 'ment', '', 1)
289                     OR self::replace($word, 'ent', '', 1);
290                     break;
291
292                 case 'o':
293                     if (substr($word, -4) == 'tion' OR substr($word, -4) == 'sion') {
294                        self::replace($word, 'ion', '', 1);
295                     } else {
296                         self::replace($word, 'ou', '', 1);
297                     }
298                     break;
299
300                 case 's':
301                     self::replace($word, 'ism', '', 1);
302                     break;
303
304                 case 't':
305                        self::replace($word, 'ate', '', 1)
306                     OR self::replace($word, 'iti', '', 1);
307                     break;
308
309                 case 'u':
310                     self::replace($word, 'ous', '', 1);
311                     break;
312
313                 case 'v':
314                     self::replace($word, 'ive', '', 1);
315                     break;
316
317                 case 'z':
318                     self::replace($word, 'ize', '', 1);
319                     break;
320             }
321             
322             return $word;
323         }
324
325     
326         /**
327         * Step 5
328         *
329         * @param string $word Word to stem
330         */
331         private static function step5($word)
332         {
333             // Part a
334             if (substr($word, -1) == 'e') {
335                 if (self::m(substr($word, 0, -1)) > 1) {
336                     self::replace($word, 'e', '');
337     
338                 } else if (self::m(substr($word, 0, -1)) == 1) {
339
340                     if (!self::cvc(substr($word, 0, -1))) {
341                         self::replace($word, 'e', '');
342                     }
343                 }
344             }
345
346             // Part b
347             if (self::m($word) > 1 AND self::doubleConsonant($word) AND substr($word, -1) == 'l') {
348                 $word = substr($word, 0, -1);
349             }
350
351             return $word;
352         }
353     
354     
355         /**
356         * Replaces the first string with the second, at the end of the string. If third
357         * arg is given, then the preceding string must match that m count at least.
358         *
359         * @param  string $str   String to check
360         * @param  string $check Ending to check for
361         * @param  string $repl  Replacement string
362         * @param  int    $m     Optional minimum number of m() to meet
363         * @return bool          Whether the $check string was at the end
364         *                       of the $str string. True does not necessarily mean
365         *                       that it was replaced.
366         */
367         private static function replace(&$str, $check, $repl, $m = null)
368         {
369             $len = 0 - strlen($check);
370     
371             if (substr($str, $len) == $check) {
372                 $substr = substr($str, 0, $len);
373                 if (is_null($m) OR self::m($substr) > $m) {
374                     $str = $substr . $repl;
375                 }
376
377                 return true;
378             }
379     
380             return false;
381         }
382     
383     
384         /**
385         * What, you mean it's not obvious from the name?
386         *
387         * m() measures the number of consonant sequences in $str. if c is
388         * a consonant sequence and v a vowel sequence, and <..> indicates arbitrary
389         * presence,
390         *
391         * <c><v>       gives 0
392         * <c>vc<v>     gives 1
393         * <c>vcvc<v>   gives 2
394         * <c>vcvcvc<v> gives 3
395         *
396         * @param  string $str The string to return the m count for
397         * @return int         The m count
398         */
399         private static function m($str)
400         {
401             $c = self::$regex_consonant;
402             $v = self::$regex_vowel;
403
404             $str = preg_replace("#^$c+#", '', $str);
405             $str = preg_replace("#$v+$#", '', $str);
406     
407             preg_match_all("#($v+$c+)#", $str, $matches);
408
409             return count($matches[1]);
410         }
411
412     
413         /**
414         * Returns true/false as to whether the given string contains two
415         * of the same consonant next to each other at the end of the string.
416         *
417         * @param  string $str String to check
418         * @return bool        Result
419         */
420         private static function doubleConsonant($str)
421         {
422             $c = self::$regex_consonant;
423             
424             return preg_match("#$c{2}$#", $str, $matches) AND $matches[0]{0} == $matches[0]{1};
425         }
426
427
428         /**
429         * Checks for ending CVC sequence where second C is not W, X or Y
430         *
431         * @param  string $str String to check
432         * @return bool        Result
433         */
434         private static function cvc($str)
435         {
436             $c = self::$regex_consonant;
437             $v = self::$regex_vowel;
438
439             return     preg_match("#($c$v$c)$#", $str, $matches)
440                    AND strlen($matches[1]) == 3
441                    AND $matches[1]{2} != 'w'
442                    AND $matches[1]{2} != 'x'
443                    AND $matches[1]{2} != 'y';
444         }
445     }
446 ?>
Note: See TracBrowser for help on using the browser.