1 <?php
2 /**
3 * Part of the Joomla Framework String Package
4 *
5 * @copyright Copyright (C) 2005 - 2015 Open Source Matters, Inc. All rights reserved.
6 * @license GNU General Public License version 2 or later; see LICENSE
7 */
8
9 namespace Joomla\String;
10
11 use InvalidArgumentException;
12
13 /**
14 * Joomla Framework String Inflector Class
15 *
16 * The Inflector transforms words
17 *
18 * @since 1.0
19 */
20 class Inflector
21 {
22 /**
23 * The singleton instance.
24 *
25 * @var Inflector
26 * @since 1.0
27 */
28 private static $instance;
29
30 /**
31 * The inflector rules for singularisation, pluralisation and countability.
32 *
33 * @var array
34 * @since 1.0
35 */
36 private $rules = array(
37 'singular' => array(
38 '/(matr)ices$/i' => '\1ix',
39 '/(vert|ind)ices$/i' => '\1ex',
40 '/(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|viri?)i$/i' => '\1us',
41 '/([ftw]ax)es/i' => '\1',
42 '/(cris|ax|test)es$/i' => '\1is',
43 '/(shoe|slave)s$/i' => '\1',
44 '/(o)es$/i' => '\1',
45 '/([^aeiouy]|qu)ies$/i' => '\1y',
46 '/$1ses$/i' => '\s',
47 '/ses$/i' => '\s',
48 '/eaus$/' => 'eau',
49 '/^(.*us)$/' => '\\1',
50 '/s$/i' => '',
51 ),
52 'plural' => array(
53 '/([m|l])ouse$/i' => '\1ice',
54 '/(matr|vert|ind)(ix|ex)$/i' => '\1ices',
55 '/(x|ch|ss|sh)$/i' => '\1es',
56 '/([^aeiouy]|qu)y$/i' => '\1ies',
57 '/([^aeiouy]|qu)ies$/i' => '\1y',
58 '/(?:([^f])fe|([lr])f)$/i' => '\1\2ves',
59 '/sis$/i' => 'ses',
60 '/([ti])um$/i' => '\1a',
61 '/(buffal|tomat)o$/i' => '\1\2oes',
62 '/(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|vir)us$/i' => '\1i',
63 '/us$/i' => 'uses',
64 '/(ax|cris|test)is$/i' => '\1es',
65 '/s$/i' => 's',
66 '/$/' => 's',
67 ),
68 'countable' => array(
69 'id',
70 'hits',
71 'clicks',
72 ),
73 );
74
75 /**
76 * Cached inflections.
77 *
78 * The array is in the form [singular => plural]
79 *
80 * @var array
81 * @since 1.0
82 */
83 private $cache = array();
84
85 /**
86 * Protected constructor.
87 *
88 * @since 1.0
89 */
90 protected function __construct()
91 {
92 // Pre=populate the irregual singular/plural.
93 $this
94 ->addWord('deer')
95 ->addWord('moose')
96 ->addWord('sheep')
97 ->addWord('bison')
98 ->addWord('salmon')
99 ->addWord('pike')
100 ->addWord('trout')
101 ->addWord('fish')
102 ->addWord('swine')
103
104 ->addWord('alias', 'aliases')
105 ->addWord('bus', 'buses')
106 ->addWord('foot', 'feet')
107 ->addWord('goose', 'geese')
108 ->addWord('hive', 'hives')
109 ->addWord('louse', 'lice')
110 ->addWord('man', 'men')
111 ->addWord('mouse', 'mice')
112 ->addWord('ox', 'oxen')
113 ->addWord('quiz', 'quizes')
114 ->addWord('status', 'statuses')
115 ->addWord('tooth', 'teeth')
116 ->addWord('woman', 'women');
117 }
118
119 /**
120 * Adds inflection regex rules to the inflector.
121 *
122 * @param mixed $data A string or an array of strings or regex rules to add.
123 * @param string $ruleType The rule type: singular | plural | countable
124 *
125 * @return void
126 *
127 * @since 1.0
128 * @throws InvalidArgumentException
129 */
130 private function addRule($data, $ruleType)
131 {
132 if (is_string($data))
133 {
134 $data = array($data);
135 }
136 elseif (!is_array($data))
137 {
138 // Do not translate.
139 throw new InvalidArgumentException('Invalid inflector rule data.');
140 }
141
142 foreach ($data as $rule)
143 {
144 // Ensure a string is pushed.
145 array_push($this->rules[$ruleType], (string) $rule);
146 }
147 }
148
149 /**
150 * Gets an inflected word from the cache where the singular form is supplied.
151 *
152 * @param string $singular A singular form of a word.
153 *
154 * @return mixed The cached inflection or false if none found.
155 *
156 * @since 1.0
157 */
158 private function getCachedPlural($singular)
159 {
160 $singular = StringHelper::strtolower($singular);
161
162 // Check if the word is in cache.
163 if (isset($this->cache[$singular]))
164 {
165 return $this->cache[$singular];
166 }
167
168 return false;
169 }
170
171 /**
172 * Gets an inflected word from the cache where the plural form is supplied.
173 *
174 * @param string $plural A plural form of a word.
175 *
176 * @return mixed The cached inflection or false if none found.
177 *
178 * @since 1.0
179 */
180 private function getCachedSingular($plural)
181 {
182 $plural = StringHelper::strtolower($plural);
183
184 return array_search($plural, $this->cache);
185 }
186
187 /**
188 * Execute a regex from rules.
189 *
190 * The 'plural' rule type expects a singular word.
191 * The 'singular' rule type expects a plural word.
192 *
193 * @param string $word The string input.
194 * @param string $ruleType String (eg, singular|plural)
195 *
196 * @return mixed An inflected string, or false if no rule could be applied.
197 *
198 * @since 1.0
199 */
200 private function matchRegexRule($word, $ruleType)
201 {
202 // Cycle through the regex rules.
203 foreach ($this->rules[$ruleType] as $regex => $replacement)
204 {
205 $matches = 0;
206 $matchedWord = preg_replace($regex, $replacement, $word, -1, $matches);
207
208 if ($matches > 0)
209 {
210 return $matchedWord;
211 }
212 }
213
214 return false;
215 }
216
217 /**
218 * Sets an inflected word in the cache.
219 *
220 * @param string $singular The singular form of the word.
221 * @param string $plural The plural form of the word. If omitted, it is assumed the singular and plural are identical.
222 *
223 * @return void
224 *
225 * @since 1.0
226 */
227 private function setCache($singular, $plural = null)
228 {
229 $singular = StringHelper::strtolower($singular);
230
231 if ($plural === null)
232 {
233 $plural = $singular;
234 }
235 else
236 {
237 $plural = StringHelper::strtolower($plural);
238 }
239
240 $this->cache[$singular] = $plural;
241 }
242
243 /**
244 * Adds a countable word.
245 *
246 * @param mixed $data A string or an array of strings to add.
247 *
248 * @return Inflector Returns this object to support chaining.
249 *
250 * @since 1.0
251 */
252 public function addCountableRule($data)
253 {
254 $this->addRule($data, 'countable');
255
256 return $this;
257 }
258
259 /**
260 * Adds a specific singular-plural pair for a word.
261 *
262 * @param string $singular The singular form of the word.
263 * @param string $plural The plural form of the word. If omitted, it is assumed the singular and plural are identical.
264 *
265 * @return Inflector Returns this object to support chaining.
266 *
267 * @since 1.0
268 */
269 public function addWord($singular, $plural =null)
270 {
271 $this->setCache($singular, $plural);
272
273 return $this;
274 }
275
276 /**
277 * Adds a pluralisation rule.
278 *
279 * @param mixed $data A string or an array of regex rules to add.
280 *
281 * @return Inflector Returns this object to support chaining.
282 *
283 * @since 1.0
284 */
285 public function addPluraliseRule($data)
286 {
287 $this->addRule($data, 'plural');
288
289 return $this;
290 }
291
292 /**
293 * Adds a singularisation rule.
294 *
295 * @param mixed $data A string or an array of regex rules to add.
296 *
297 * @return Inflector Returns this object to support chaining.
298 *
299 * @since 1.0
300 */
301 public function addSingulariseRule($data)
302 {
303 $this->addRule($data, 'singular');
304
305 return $this;
306 }
307
308 /**
309 * Gets an instance of the JStringInflector singleton.
310 *
311 * @param boolean $new If true (default is false), returns a new instance regardless if one exists.
312 * This argument is mainly used for testing.
313 *
314 * @return Inflector
315 *
316 * @since 1.0
317 */
318 public static function getInstance($new = false)
319 {
320 if ($new)
321 {
322 return new static;
323 }
324 elseif (!is_object(self::$instance))
325 {
326 self::$instance = new static;
327 }
328
329 return self::$instance;
330 }
331
332 /**
333 * Checks if a word is countable.
334 *
335 * @param string $word The string input.
336 *
337 * @return boolean True if word is countable, false otherwise.
338 *
339 * @since 1.0
340 */
341 public function isCountable($word)
342 {
343 return (boolean) in_array($word, $this->rules['countable']);
344 }
345
346 /**
347 * Checks if a word is in a plural form.
348 *
349 * @param string $word The string input.
350 *
351 * @return boolean True if word is plural, false if not.
352 *
353 * @since 1.0
354 */
355 public function isPlural($word)
356 {
357 // Try the cache for an known inflection.
358 $inflection = $this->getCachedSingular($word);
359
360 if ($inflection !== false)
361 {
362 return true;
363 }
364
365 $singularWord = $this->toSingular($word);
366
367 if ($singularWord === false)
368 {
369 return false;
370 }
371
372 // Compute the inflection to cache the values, and compare.
373 return $this->toPlural($singularWord) == $word;
374 }
375
376 /**
377 * Checks if a word is in a singular form.
378 *
379 * @param string $word The string input.
380 *
381 * @return boolean True if word is singular, false if not.
382 *
383 * @since 1.0
384 */
385 public function isSingular($word)
386 {
387 // Try the cache for an known inflection.
388 $inflection = $this->getCachedPlural($word);
389
390 if ($inflection !== false)
391 {
392 return true;
393 }
394
395 $pluralWord = $this->toPlural($word);
396
397 if ($pluralWord === false)
398 {
399 return false;
400 }
401
402 // Compute the inflection to cache the values, and compare.
403 return $this->toSingular($pluralWord) == $word;
404 }
405
406 /**
407 * Converts a word into its plural form.
408 *
409 * @param string $word The singular word to pluralise.
410 *
411 * @return mixed An inflected string, or false if no rule could be applied.
412 *
413 * @since 1.0
414 */
415 public function toPlural($word)
416 {
417 // Try to get the cached plural form from the singular.
418 $cache = $this->getCachedPlural($word);
419
420 if ($cache !== false)
421 {
422 return $cache;
423 }
424
425 // Check if the word is a known singular.
426 if ($this->getCachedSingular($word))
427 {
428 return false;
429 }
430
431 // Compute the inflection.
432 $inflected = $this->matchRegexRule($word, 'plural');
433
434 if ($inflected !== false)
435 {
436 $this->setCache($word, $inflected);
437
438 return $inflected;
439 }
440
441 // Dead code
442 return false;
443 }
444
445 /**
446 * Converts a word into its singular form.
447 *
448 * @param string $word The plural word to singularise.
449 *
450 * @return mixed An inflected string, or false if no rule could be applied.
451 *
452 * @since 1.0
453 */
454 public function toSingular($word)
455 {
456 // Try to get the cached singular form from the plural.
457 $cache = $this->getCachedSingular($word);
458
459 if ($cache !== false)
460 {
461 return $cache;
462 }
463
464 // Check if the word is a known plural.
465 if ($this->getCachedPlural($word))
466 {
467 return false;
468 }
469
470 // Compute the inflection.
471 $inflected = $this->matchRegexRule($word, 'singular');
472
473 if ($inflected !== false)
474 {
475 $this->setCache($inflected, $word);
476
477 return $inflected;
478 }
479
480 return false;
481 }
482 }
483