root/fGrammar.php

Revision 747, 20.1 kB (checked in by wbond, 1 week ago)

Fixed ticket #374 - added missing fGrammar::compose()

LineHide Line Numbers
1 <?php
2 /**
3  * Provides english word inflection, notation conversion, grammar helpers and internationlization support
4  *
5  * @copyright  Copyright (c) 2007-2010 Will Bond
6  * @author     Will Bond [wb] <will@flourishlib.com>
7  * @license    http://flourishlib.com/license
8  *
9  * @package    Flourish
10  * @link       http://flourishlib.com/fGrammar
11  *
12  * @version    1.0.0b6
13  * @changes    1.0.0b6  Added missing ::compose() method [wb, 2010-03-03]
14  * @changes    1.0.0b5  Fixed ::reset() to properly reset the singularization and pluralization rules [wb, 2009-10-28]
15  * @changes    1.0.0b4  Added caching for various methods - provided significant performance boost to ORM [wb, 2009-06-15]
16  * @changes    1.0.0b3  Changed replacement values in preg_replace() calls to be properly escaped [wb, 2009-06-11]
17  * @changes    1.0.0b2  Fixed a bug where some words would lose capitalization with ::pluralize() and ::singularize() [wb, 2009-01-25]
18  * @changes    1.0.0b   The initial implementation [wb, 2007-09-25]
19  */
20 class fGrammar
21 {
22     // The following constants allow for nice looking callbacks to static methods
23     const addCamelUnderscoreRule    = 'fGrammar::addCamelUnderscoreRule';
24     const addHumanizeRule           = 'fGrammar::addHumanizeRule';
25     const addSingularPluralRule     = 'fGrammar::addSingularPluralRule';
26     const camelize                  = 'fGrammar::camelize';
27     const humanize                  = 'fGrammar::humanize';
28     const inflectOnQuantity         = 'fGrammar::inflectOnQuantity';
29     const joinArray                 = 'fGrammar::joinArray';
30     const pluralize                 = 'fGrammar::pluralize';
31     const registerJoinArrayCallback = 'fGrammar::registerJoinArrayCallback';
32     const reset                     = 'fGrammar::reset';
33     const singularize               = 'fGrammar::singularize';
34     const underscorize              = 'fGrammar::underscorize';
35    
36    
37     /**
38     * Cache for plural <-> singular and underscore <-> camelcase
39     *
40     * @var array
41     */
42     static private $cache = array(
43         'camelize'     => array(0 => array(), 1 => array()),
44         'humanize'     => array(),
45         'pluralize'    => array(),
46         'singularize'  => array(),
47         'underscorize' => array()
48     );
49    
50     /**
51     * Custom rules for camelizing a string
52     *
53     * @var array
54     */
55     static private $camelize_rules = array();
56    
57     /**
58     * Custom rules for humanizing a string
59     *
60     * @var array
61     */
62     static private $humanize_rules = array();
63    
64     /**
65     * The callback to replace ::joinArray()
66     *
67     * @var callback
68     */
69     static private $join_array_callback = NULL;
70    
71     /**
72     * Rules for plural to singular inflection of nouns
73     *
74     * @var array
75     */
76     static private $plural_to_singular_rules = array(
77         '([ml])ice'                    => '\1ouse',
78         '(media|info(rmation)?|news)$' => '\1',
79         '(q)uizzes$'                   => '\1uiz',
80         '(c)hildren$'                  => '\1hild',
81         '(p)eople$'                    => '\1erson',
82         '(m)en$'                       => '\1an',
83         '((?!sh).)oes$'                => '\1o',
84         '((?<!o)[ieu]s|[ieuo]x)es$'    => '\1',
85         '([cs]h)es$'                   => '\1',
86         '(ss)es$'                      => '\1',
87         '([aeo]l)ves$'                 => '\1f',
88         '([^d]ea)ves$'                 => '\1f',
89         '(ar)ves$'                     => '\1f',
90         '([nlw]i)ves$'                 => '\1fe',
91         '([aeiou]y)s$'                 => '\1',
92         '([^aeiou])ies$'               => '\1y',
93         '(la)ses$'                     => '\1s',
94         '(.)s$'                        => '\1'
95     );
96    
97     /**
98     * Rules for singular to plural inflection of nouns
99     *
100     * @var array
101     */
102     static private $singular_to_plural_rules = array(
103         '([ml])ouse$'                  => '\1ice',
104         '(media|info(rmation)?|news)$' => '\1',
105         '(phot|log)o$'                 => '\1os',
106         '^(q)uiz$'                     => '\1uizzes',
107         '(c)hild$'                     => '\1hildren',
108         '(p)erson$'                    => '\1eople',
109         '(m)an$'                       => '\1en',
110         '([ieu]s|[ieuo]x)$'            => '\1es',
111         '([cs]h)$'                     => '\1es',
112         '(ss)$'                        => '\1es',
113         '([aeo]l)f$'                   => '\1ves',
114         '([^d]ea)f$'                   => '\1ves',
115         '(ar)f$'                       => '\1ves',
116         '([nlw]i)fe$'                  => '\1ves',
117         '([aeiou]y)$'                  => '\1s',
118         '([^aeiou])y$'                 => '\1ies',
119         '([^o])o$'                     => '\1oes',
120         's$'                           => 'ses',
121         '(.)$'                         => '\1s'
122     );
123    
124     /**
125     * Custom rules for underscorizing a string
126     *
127     * @var array
128     */
129     static private $underscorize_rules = array();
130    
131    
132     /**
133     * Adds a custom mapping of a non-humanized string to a humanized string for ::humanize()
134     *
135     * @param  string $non_humanized_string  The non-humanized string
136     * @param  string $humanized_string      The humanized string
137     * @return void
138     */
139     static public function addHumanizeRule($non_humanized_string, $humanized_string)
140     {
141         self::$humanize_rules[$non_humanized_string] = $humanized_string;
142        
143         self::$cache['humanize'] = array();
144     }
145    
146    
147     /**
148     * Adds a custom `camelCase` to `underscore_notation` and `underscore_notation` to `camelCase` rule
149     *
150     * @param  string $camel_case           The lower `camelCase` version of the string
151     * @param  string $underscore_notation  The `underscore_notation` version of the string
152     * @return void
153     */
154     static public function addCamelUnderscoreRule($camel_case, $underscore_notation)
155     {
156         $camel_case = strtolower($camel_case[0]) . substr($camel_case, 1);
157         self::$underscorize_rules[$camel_case] = $underscore_notation;
158         self::$camelize_rules[$underscore_notation] = $camel_case;
159        
160         self::$cache['camelize']     = array(0 => array(), 1 => array());
161         self::$cache['underscorize'] = array();
162     }
163    
164    
165     /**
166     * Adds a custom singular to plural and plural to singular rule for ::pluralize() and ::singularize()
167     *
168     * @param  string $singular  The singular version of the noun
169     * @param  string $plural    The plural version of the noun
170     * @return void
171     */
172     static public function addSingularPluralRule($singular, $plural)
173     {
174         self::$singular_to_plural_rules = array_merge(
175             array(
176                 '^(' . preg_quote($singular[0], '#') . ')' . preg_quote(substr($singular, 1), '#') . '$' =>
177                     '\1' . strtr(substr($plural, 1), array('\\' => '\\\\', '$' => '\\$'))
178             ),
179             self::$singular_to_plural_rules
180         );
181         self::$plural_to_singular_rules = array_merge(
182             array(
183                 '^(' . preg_quote($plural[0], '#') . ')' . preg_quote(substr($plural, 1), '#') . '$' =>
184                     '\1' . strtr(substr($singular, 1), array('\\' => '\\\\', '$' => '\\$'))
185             ),
186             self::$plural_to_singular_rules
187         );
188        
189         self::$cache['pluralize']   = array();
190         self::$cache['singularize'] = array();
191     }
192    
193    
194     /**
195     * Converts an `underscore_notation`, human-friendly or `camelCase` string to `camelCase`
196     *
197     * @param  string  $string  The string to convert
198     * @param  boolean $upper   If the camel case should be `UpperCamelCase`
199     * @return string  The converted string
200     */
201     static public function camelize($string, $upper)
202     {
203         $upper = (int) $upper;
204         if (isset(self::$cache['camelize'][$upper][$string])) {
205             return self::$cache['camelize'][$upper][$string];       
206         }
207        
208         $original = $string;
209        
210         // Handle custom rules
211         if (isset(self::$camelize_rules[$string])) {
212             $string = self::$camelize_rules[$string];
213             if ($upper) {
214                 $string = strtoupper($camel[0]) . substr($camel, 1);
215             }
216        
217         // Make a humanized string like underscore notation
218         } elseif (strpos($string, ' ') !== FALSE) {
219             $string = strtolower(preg_replace('#\s+#', '_', $string));
220        
221         // Check to make sure this is not already camel case
222         } elseif (strpos($string, '_') === FALSE) {
223             if ($upper) {
224                 $string = strtoupper($string[0]) . substr($string, 1);
225             }
226            
227         // Handle underscore notation
228         } else {
229             $string = strtolower($string);
230             if ($upper) {
231                 $string = strtoupper($string[0]) . substr($string, 1);
232             }
233             $string = preg_replace('/(_([a-z0-9]))/e', 'strtoupper("\2")', $string);       
234         }
235        
236         self::$cache['camelize'][$upper][$original] = $string;
237         return $string;
238     }
239    
240    
241     /**
242     * Composes text using fText if loaded
243     *
244     * @param  string  $message    The message to compose
245     * @param  mixed   $component  A string or number to insert into the message
246     * @param  mixed   ...
247     * @return string  The composed and possible translated message
248     */
249     static protected function compose($message)
250     {
251         $args = array_slice(func_get_args(), 1);
252        
253         if (class_exists('fText', FALSE)) {
254             return call_user_func_array(
255                 array('fText', 'compose'),
256                 array($message, $args)
257             );
258         } else {
259             return vsprintf($message, $args);
260         }
261     }
262    
263    
264     /**
265     * Makes an `underscore_notation`, `camelCase`, or human-friendly string into a human-friendly string
266     *
267     * @param  string $string  The string to humanize
268     * @return string  The converted string
269     */
270     static public function humanize($string)
271     {
272         if (isset(self::$cache['humanize'][$string])) {
273             return self::$cache['humanize'][$string];
274         }
275        
276         $original = $string;
277        
278         if (isset(self::$humanize_rules[$string])) {
279             $string = self::$humanize_rules[$string];   
280        
281         // If there is no space, it isn't already humanized
282         } elseif (strpos($string, ' ') === FALSE) {
283            
284             // If we don't have an underscore we probably have camelCase
285             if (strpos($string, '_') === FALSE) {
286                 $string = self::underscorize($string);
287             }
288            
289             $string = preg_replace(
290                 '/(\b(api|css|gif|html|id|jpg|js|mp3|pdf|php|png|sql|swf|url|xhtml|xml)\b|\b\w)/e',
291                 'strtoupper("\1")',
292                 str_replace('_', ' ', $string)
293             );
294         }
295        
296         self::$cache['humanize'][$original] = $string;
297        
298         return $string;
299     }
300    
301    
302     /**
303     * Returns the singular or plural form of the word or based on the quantity specified
304     *
305     * @param  mixed   $quantity                     The quantity (integer) or an array of objects to count
306     * @param  string  $singular_form                The string to be returned for when `$quantity = 1`
307     * @param  string  $plural_form                  The string to be returned for when `$quantity != 1`, use `%d` to place the quantity in the string
308     * @param  boolean $use_words_for_single_digits  If the numbers 0 to 9 should be written out as words
309     * @return string
310     */
311     static public function inflectOnQuantity($quantity, $singular_form, $plural_form=NULL, $use_words_for_single_digits=FALSE)
312     {
313         if ($plural_form === NULL) {
314             $plural_form = self::pluralize($singular_form);
315         }
316        
317         if (is_array($quantity)) {
318             $quantity = sizeof($quantity);
319         }
320        
321         if ($quantity == 1) {
322             return $singular_form;
323            
324         } else {
325             $output = $plural_form;
326            
327             // Handle placement of the quantity into the output
328             if (strpos($output, '%d') !== FALSE) {
329                
330                 if ($use_words_for_single_digits && $quantity < 10) {
331                     static $replacements = array();
332                     if (!$replacements) {
333                         $replacements = array(
334                             0 => self::compose('zero'),
335                             1 => self::compose('one'),
336                             2 => self::compose('two'),
337                             3 => self::compose('three'),
338                             4 => self::compose('four'),
339                             5 => self::compose('five'),
340                             6 => self::compose('six'),
341                             7 => self::compose('seven'),
342                             8 => self::compose('eight'),
343                             9 => self::compose('nine')
344                         );
345                     }
346                     $quantity = $replacements[$quantity];
347                 }
348                
349                 $output = str_replace('%d', $quantity, $output);
350             }
351            
352             return $output;
353         }
354     }
355    
356    
357     /**
358     * Returns the passed terms joined together using rule 2 from Strunk & White's 'The Elements of Style'
359     *
360     * @param  array  $strings  An array of strings to be joined together
361     * @param  string $type     The type of join to perform, `'and'` or `'or'`
362     * @return string  The terms joined together
363     */
364     static public function joinArray($strings, $type)
365     {
366         $valid_types = array('and', 'or');
367         if (!in_array($type, $valid_types)) {
368             throw new fProgrammerException(
369                 'The type specified, %1$s, is invalid. Must be one of: %2$s.',
370                 $type,
371                 join(', ', $valid_types)
372             );
373         }
374        
375         if (self::$join_array_callback) {
376             return call_user_func(self::$join_array_callback, $strings, $type);
377         }
378        
379         settype($strings, 'array');
380         $strings = array_values($strings);
381        
382         switch (sizeof($strings)) {
383             case 0:
384                 return '';
385                 break;
386            
387             case 1:
388                 return $strings[0];
389                 break;
390            
391             case 2:
392                 return $strings[0] . ' ' . $type . ' ' . $strings[1];
393                 break;
394                
395             default:
396                 $last_string = array_pop($strings);
397                 return join(', ', $strings) . ' ' . $type . ' ' . $last_string;
398                 break;
399         }
400     }
401    
402    
403     /**
404     * Returns the plural version of a singular noun
405     *
406     * @param  string $singular_noun  The singular noun to pluralize
407     * @return string  The pluralized noun
408     */
409     static public function pluralize($singular_noun)
410     {
411         if (isset(self::$cache['pluralize'][$singular_noun])) {
412             return self::$cache['pluralize'][$singular_noun];       
413         }
414        
415         $original    = $singular_noun;
416         $plural_noun = NULL;
417        
418         list ($beginning, $singular_noun) = self::splitLastWord($singular_noun);
419         foreach (self::$singular_to_plural_rules as $from => $to) {
420             if (preg_match('#' . $from . '#iD', $singular_noun)) {
421                 $plural_noun = $beginning . preg_replace('#' . $from . '#iD', $to, $singular_noun);
422                 break;
423             }
424         }
425        
426         if (!$plural_noun) {
427             throw new fProgrammerException('The noun specified could not be pluralized');
428         }
429        
430         self::$cache['pluralize'][$original] = $plural_noun;
431        
432         return $plural_noun;
433     }
434    
435    
436     /**
437     * Allows replacing the ::joinArray() function with a user defined function
438     *
439     * This would be most useful for changing ::joinArray() to work with
440     * languages other than English.
441     *
442     * @param  callback $callback  The function to replace ::joinArray() with - should accept the same parameters and return the same type
443     * @return void
444     */
445     static public function registerJoinArrayCallback($callback)
446     {
447         if (is_string($callback) && strpos($callback, '::') !== FALSE) {
448             $callback = explode('::', $callback);   
449         }
450         self::$join_array_callback = $callback;
451     }
452    
453    
454     /**
455     * Resets the configuration of the class
456     *
457     * @internal
458      *
459     * @return void
460     */
461     static public function reset()
462     {
463         self::$cache                    = array(
464             'camelize'     => array(0 => array(), 1 => array()),
465             'humanize'     => array(),
466             'pluralize'    => array(),
467             'singularize'  => array(),
468             'underscorize' => array()
469         );
470         self::$camelize_rules           = array();
471         self::$humanize_rules           = array();
472         self::$join_array_callback      = NULL;
473         self::$plural_to_singular_rules = array(
474             '([ml])ice'                    => '\1ouse',
475             '(media|info(rmation)?|news)$' => '\1',
476             '(q)uizzes$'                   => '\1uiz',
477             '(c)hildren$'                  => '\1hild',
478             '(p)eople$'                    => '\1erson',
479             '(m)en$'                       => '\1an',
480             '((?!sh).)oes$'                => '\1o',
481             '((?<!o)[ieu]s|[ieuo]x)es$'    => '\1',
482             '([cs]h)es$'                   => '\1',
483             '(ss)es$'                      => '\1',
484             '([aeo]l)ves$'                 => '\1f',
485             '([^d]ea)ves$'                 => '\1f',
486             '(ar)ves$'                     => '\1f',
487             '([nlw]i)ves$'                 => '\1fe',
488             '([aeiou]y)s$'                 => '\1',
489             '([^aeiou])ies$'               => '\1y',
490             '(la)ses$'                     => '\1s',
491             '(.)s$'                        => '\1'
492         );
493         self::$singular_to_plural_rules = array(
494             '([ml])ouse$'                  => '\1ice',
495             '(media|info(rmation)?|news)$' => '\1',
496             '(phot|log)o$'                 => '\1os',
497             '^(q)uiz$'                     => '\1uizzes',
498             '(c)hild$'                     => '\1hildren',
499             '(p)erson$'                    => '\1eople',
500             '(m)an$'                       => '\1en',
501             '([ieu]s|[ieuo]x)$'            => '\1es',
502             '([cs]h)$'                     => '\1es',
503             '(ss)$'                        => '\1es',
504             '([aeo]l)f$'                   => '\1ves',
505             '([^d]ea)f$'                   => '\1ves',
506             '(ar)f$'                       => '\1ves',
507             '([nlw]i)fe$'                  => '\1ves',
508             '([aeiou]y)$'                  => '\1s',
509             '([^aeiou])y$'                 => '\1ies',
510             '([^o])o$'                     => '\1oes',
511             's$'                           => 'ses',
512             '(.)$'                         => '\1s'
513         );   
514     }
515    
516    
517     /**
518     * Returns the singular version of a plural noun
519     *
520     * @param  string $plural_noun  The plural noun to singularize
521     * @return string  The singularized noun
522     */
523     static public function singularize($plural_noun)
524     {
525         if (isset(self::$cache['singularize'][$plural_noun])) {
526             return self::$cache['singularize'][$plural_noun];       
527         }
528        
529         $original      = $plural_noun;
530         $singular_noun = NULL;
531        
532         list ($beginning, $plural_noun) = self::splitLastWord($plural_noun);
533         foreach (self::$plural_to_singular_rules as $from => $to) {
534             if (preg_match('#' . $from . '#iD', $plural_noun)) {
535                 $singular_noun = $beginning . preg_replace('#' . $from . '#iD', $to, $plural_noun);
536                 break;
537             }
538         }
539        
540         if (!$singular_noun) {
541             throw new fProgrammerException('The noun specified could not be singularized');
542         }
543        
544         self::$cache['singularize'][$original] = $singular_noun;
545        
546         return $singular_noun;
547     }
548    
549    
550     /**
551     * Splits the last word off of a `camelCase` or `underscore_notation` string
552     *
553     * @param  string $string  The string to split the word from
554     * @return array  The first element is the beginning part of the string, the second element is the last word
555     */
556     static private function splitLastWord($string)
557     {
558         // Handle strings with spaces in them
559         if (strpos($string, ' ') !== FALSE) {
560             return array(substr($string, 0, strrpos($string, ' ')+1), substr($string, strrpos($string, ' ')+1));
561         }
562        
563         // Handle underscore notation
564         if ($string == self::underscorize($string)) {
565             if (strpos($string, '_') === FALSE) { return array('', $string); }
566             return array(substr($string, 0, strrpos($string, '_')+1), substr($string, strrpos($string, '_')+1));
567         }
568        
569         // Handle camel case
570         if (preg_match('#(.*)((?<=[a-zA-Z]|^)(?:[0-9]+|[A-Z][a-z]*)|(?<=[0-9A-Z]|^)(?:[A-Z][a-z]*))$#D', $string, $match)) {
571             return array($match[1], $match[2]);
572         }
573        
574         return array('', $string);
575     }
576    
577    
578     /**
579     * Converts a `camelCase`, human-friendly or `underscore_notation` string to `underscore_notation`
580     *
581     * @param  string $string  The string to convert
582     * @return string  The converted string
583     */
584     static public function underscorize($string)
585     {
586         if (isset(self::$cache['underscorize'][$string])) {
587             return self::$cache['underscorize'][$string];       
588         }
589        
590         $original = $string;
591         $string = strtolower($string[0]) . substr($string, 1);
592        
593         // Handle custom rules
594         if (isset(self::$underscorize_rules[$string])) {
595             $string = self::$underscorize_rules[$string];
596        
597         // If the string is already underscore notation then leave it
598         } elseif (strpos($string, '_') !== FALSE) {
599        
600         // Allow humanized string to be passed in
601         } elseif (strpos($string, ' ') !== FALSE) {
602             $string = strtolower(preg_replace('#\s+#', '_', $string));
603        
604         } else {
605             do {
606                 $old_string = $string;
607                 $string = preg_replace('/([a-zA-Z])([0-9])/', '\1_\2', $string);
608                 $string = preg_replace('/([a-z0-9A-Z])([A-Z])/', '\1_\2', $string);
609             } while ($old_string != $string);
610            
611             $string = strtolower($string);
612         }
613        
614         self::$cache['underscorize'][$original] = $string;
615        
616         return $string;
617     }
618    
619    
620     /**
621     * Forces use as a static class
622     *
623     * @return fGrammar
624     */
625     private function __construct() { }
626 }
627  
628  
629  
630 /**
631  * Copyright (c) 2007-2010 Will Bond <will@flourishlib.com>
632  *
633  * Permission is hereby granted, free of charge, to any person obtaining a copy
634  * of this software and associated documentation files (the "Software"), to deal
635  * in the Software without restriction, including without limitation the rights
636  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
637  * copies of the Software, and to permit persons to whom the Software is
638  * furnished to do so, subject to the following conditions:
639  *
640  * The above copyright notice and this permission notice shall be included in
641  * all copies or substantial portions of the Software.
642  *
643  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
644  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
645  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
646  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
647  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
648  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
649  * THE SOFTWARE.
650  */