View Javadoc

1   /*
2    * ModeShape (http://www.modeshape.org)
3    * See the COPYRIGHT.txt file distributed with this work for information
4    * regarding copyright ownership.  Some portions may be licensed
5    * to Red Hat, Inc. under one or more contributor license agreements.
6    * See the AUTHORS.txt file in the distribution for a full listing of 
7    * individual contributors. 
8    *
9    * ModeShape is free software. Unless otherwise indicated, all code in ModeShape
10   * is licensed to you under the terms of the GNU Lesser General Public License as
11   * published by the Free Software Foundation; either version 2.1 of
12   * the License, or (at your option) any later version.
13   *
14   * ModeShape is distributed in the hope that it will be useful,
15   * but WITHOUT ANY WARRANTY; without even the implied warranty of
16   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17   * Lesser General Public License for more details.
18   *
19   * You should have received a copy of the GNU Lesser General Public
20   * License along with this software; if not, write to the Free
21   * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
22   * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
23   */
24  package org.modeshape.common.text;
25  
26  import java.util.HashSet;
27  import java.util.LinkedList;
28  import java.util.Set;
29  import java.util.regex.Matcher;
30  import java.util.regex.Pattern;
31  import net.jcip.annotations.ThreadSafe;
32  import org.modeshape.common.util.CheckArg;
33  
34  /**
35   * Transforms words to singular, plural, humanized (human readable), underscore, camel case, or ordinal form. This is inspired by
36   * the <a href="http://api.rubyonrails.org/classes/Inflector.html">Inflector</a> class in <a
37   * href="http://www.rubyonrails.org">Ruby on Rails</a>, which is distributed under the <a
38   * href="http://wiki.rubyonrails.org/rails/pages/License">Rails license</a>.
39   */
40  @ThreadSafe
41  public class Inflector {
42  
43      protected static final Inflector INSTANCE = new Inflector();
44  
45      public static final Inflector getInstance() {
46          return INSTANCE;
47      }
48  
49      protected class Rule {
50  
51          protected final String expression;
52          protected final Pattern expressionPattern;
53          protected final String replacement;
54  
55          protected Rule( String expression,
56                          String replacement ) {
57              this.expression = expression;
58              this.replacement = replacement != null ? replacement : "";
59              this.expressionPattern = Pattern.compile(this.expression, Pattern.CASE_INSENSITIVE);
60          }
61  
62          /**
63           * Apply the rule against the input string, returning the modified string or null if the rule didn't apply (and no
64           * modifications were made)
65           * 
66           * @param input the input string
67           * @return the modified string if this rule applied, or null if the input was not modified by this rule
68           */
69          protected String apply( String input ) {
70              Matcher matcher = this.expressionPattern.matcher(input);
71              if (!matcher.find()) return null;
72              return matcher.replaceAll(this.replacement);
73          }
74  
75          @Override
76          public int hashCode() {
77              return expression.hashCode();
78          }
79  
80          @Override
81          public boolean equals( Object obj ) {
82              if (obj == this) return true;
83              if (obj != null && obj.getClass() == this.getClass()) {
84                  final Rule that = (Rule)obj;
85                  if (this.expression.equalsIgnoreCase(that.expression)) return true;
86              }
87              return false;
88          }
89  
90          @Override
91          public String toString() {
92              return expression + ", " + replacement;
93          }
94      }
95  
96      private LinkedList<Rule> plurals = new LinkedList<Rule>();
97      private LinkedList<Rule> singulars = new LinkedList<Rule>();
98      /**
99       * The lowercase words that are to be excluded and not processed. This map can be modified by the users via
100      * {@link #getUncountables()}.
101      */
102     private final Set<String> uncountables = new HashSet<String>();
103 
104     public Inflector() {
105         initialize();
106     }
107 
108     protected Inflector( Inflector original ) {
109         this.plurals.addAll(original.plurals);
110         this.singulars.addAll(original.singulars);
111         this.uncountables.addAll(original.uncountables);
112     }
113 
114     @Override
115     public Inflector clone() {
116         return new Inflector(this);
117     }
118 
119     // ------------------------------------------------------------------------------------------------
120     // Usage functions
121     // ------------------------------------------------------------------------------------------------
122 
123     /**
124      * Returns the plural form of the word in the string.
125      * <p>
126      * Examples:
127      * 
128      * <pre>
129      *   inflector.pluralize(&quot;post&quot;)               #=&gt; &quot;posts&quot;
130      *   inflector.pluralize(&quot;octopus&quot;)            #=&gt; &quot;octopi&quot;
131      *   inflector.pluralize(&quot;sheep&quot;)              #=&gt; &quot;sheep&quot;
132      *   inflector.pluralize(&quot;words&quot;)              #=&gt; &quot;words&quot;
133      *   inflector.pluralize(&quot;the blue mailman&quot;)   #=&gt; &quot;the blue mailmen&quot;
134      *   inflector.pluralize(&quot;CamelOctopus&quot;)       #=&gt; &quot;CamelOctopi&quot;
135      * </pre>
136      * 
137      * </p>
138      * <p>
139      * Note that if the {@link Object#toString()} is called on the supplied object, so this method works for non-strings, too.
140      * </p>
141      * 
142      * @param word the word that is to be pluralized.
143      * @return the pluralized form of the word, or the word itself if it could not be pluralized
144      * @see #singularize(Object)
145      */
146     public String pluralize( Object word ) {
147         if (word == null) return null;
148         String wordStr = word.toString().trim();
149         if (wordStr.length() == 0) return wordStr;
150         if (isUncountable(wordStr)) return wordStr;
151         for (Rule rule : this.plurals) {
152             String result = rule.apply(wordStr);
153             if (result != null) return result;
154         }
155         return wordStr;
156     }
157 
158     public String pluralize( Object word,
159                              int count ) {
160         if (word == null) return null;
161         if (count == 1 || count == -1) {
162             return word.toString();
163         }
164         return pluralize(word);
165     }
166 
167     /**
168      * Returns the singular form of the word in the string.
169      * <p>
170      * Examples:
171      * 
172      * <pre>
173      *   inflector.singularize(&quot;posts&quot;)             #=&gt; &quot;post&quot;
174      *   inflector.singularize(&quot;octopi&quot;)            #=&gt; &quot;octopus&quot;
175      *   inflector.singularize(&quot;sheep&quot;)             #=&gt; &quot;sheep&quot;
176      *   inflector.singularize(&quot;words&quot;)             #=&gt; &quot;word&quot;
177      *   inflector.singularize(&quot;the blue mailmen&quot;)  #=&gt; &quot;the blue mailman&quot;
178      *   inflector.singularize(&quot;CamelOctopi&quot;)       #=&gt; &quot;CamelOctopus&quot;
179      * </pre>
180      * 
181      * </p>
182      * <p>
183      * Note that if the {@link Object#toString()} is called on the supplied object, so this method works for non-strings, too.
184      * </p>
185      * 
186      * @param word the word that is to be pluralized.
187      * @return the pluralized form of the word, or the word itself if it could not be pluralized
188      * @see #pluralize(Object)
189      */
190     public String singularize( Object word ) {
191         if (word == null) return null;
192         String wordStr = word.toString().trim();
193         if (wordStr.length() == 0) return wordStr;
194         if (isUncountable(wordStr)) return wordStr;
195         for (Rule rule : this.singulars) {
196             String result = rule.apply(wordStr);
197             if (result != null) return result;
198         }
199         return wordStr;
200     }
201 
202     /**
203      * Converts strings to lowerCamelCase. This method will also use any extra delimiter characters to identify word boundaries.
204      * <p>
205      * Examples:
206      * 
207      * <pre>
208      *   inflector.lowerCamelCase(&quot;active_record&quot;)       #=&gt; &quot;activeRecord&quot;
209      *   inflector.lowerCamelCase(&quot;first_name&quot;)          #=&gt; &quot;firstName&quot;
210      *   inflector.lowerCamelCase(&quot;name&quot;)                #=&gt; &quot;name&quot;
211      *   inflector.lowerCamelCase(&quot;the-first_name&quot;,'-')  #=&gt; &quot;theFirstName&quot;
212      * </pre>
213      * 
214      * </p>
215      * 
216      * @param lowerCaseAndUnderscoredWord the word that is to be converted to camel case
217      * @param delimiterChars optional characters that are used to delimit word boundaries
218      * @return the lower camel case version of the word
219      * @see #underscore(String, char[])
220      * @see #camelCase(String, boolean, char[])
221      * @see #upperCamelCase(String, char[])
222      */
223     public String lowerCamelCase( String lowerCaseAndUnderscoredWord,
224                                   char... delimiterChars ) {
225         return camelCase(lowerCaseAndUnderscoredWord, false, delimiterChars);
226     }
227 
228     /**
229      * Converts strings to UpperCamelCase. This method will also use any extra delimiter characters to identify word boundaries.
230      * <p>
231      * Examples:
232      * 
233      * <pre>
234      *   inflector.upperCamelCase(&quot;active_record&quot;)       #=&gt; &quot;SctiveRecord&quot;
235      *   inflector.upperCamelCase(&quot;first_name&quot;)          #=&gt; &quot;FirstName&quot;
236      *   inflector.upperCamelCase(&quot;name&quot;)                #=&gt; &quot;Name&quot;
237      *   inflector.lowerCamelCase(&quot;the-first_name&quot;,'-')  #=&gt; &quot;TheFirstName&quot;
238      * </pre>
239      * 
240      * </p>
241      * 
242      * @param lowerCaseAndUnderscoredWord the word that is to be converted to camel case
243      * @param delimiterChars optional characters that are used to delimit word boundaries
244      * @return the upper camel case version of the word
245      * @see #underscore(String, char[])
246      * @see #camelCase(String, boolean, char[])
247      * @see #lowerCamelCase(String, char[])
248      */
249     public String upperCamelCase( String lowerCaseAndUnderscoredWord,
250                                   char... delimiterChars ) {
251         return camelCase(lowerCaseAndUnderscoredWord, true, delimiterChars);
252     }
253 
254     /**
255      * By default, this method converts strings to UpperCamelCase. If the <code>uppercaseFirstLetter</code> argument to false,
256      * then this method produces lowerCamelCase. This method will also use any extra delimiter characters to identify word
257      * boundaries.
258      * <p>
259      * Examples:
260      * 
261      * <pre>
262      *   inflector.camelCase(&quot;active_record&quot;,false)    #=&gt; &quot;activeRecord&quot;
263      *   inflector.camelCase(&quot;active_record&quot;,true)     #=&gt; &quot;ActiveRecord&quot;
264      *   inflector.camelCase(&quot;first_name&quot;,false)       #=&gt; &quot;firstName&quot;
265      *   inflector.camelCase(&quot;first_name&quot;,true)        #=&gt; &quot;FirstName&quot;
266      *   inflector.camelCase(&quot;name&quot;,false)             #=&gt; &quot;name&quot;
267      *   inflector.camelCase(&quot;name&quot;,true)              #=&gt; &quot;Name&quot;
268      * </pre>
269      * 
270      * </p>
271      * 
272      * @param lowerCaseAndUnderscoredWord the word that is to be converted to camel case
273      * @param uppercaseFirstLetter true if the first character is to be uppercased, or false if the first character is to be
274      *        lowercased
275      * @param delimiterChars optional characters that are used to delimit word boundaries
276      * @return the camel case version of the word
277      * @see #underscore(String, char[])
278      * @see #upperCamelCase(String, char[])
279      * @see #lowerCamelCase(String, char[])
280      */
281     public String camelCase( String lowerCaseAndUnderscoredWord,
282                              boolean uppercaseFirstLetter,
283                              char... delimiterChars ) {
284         if (lowerCaseAndUnderscoredWord == null) return null;
285         lowerCaseAndUnderscoredWord = lowerCaseAndUnderscoredWord.trim();
286         if (lowerCaseAndUnderscoredWord.length() == 0) return "";
287         if (uppercaseFirstLetter) {
288             String result = lowerCaseAndUnderscoredWord;
289             // Replace any extra delimiters with underscores (before the underscores are converted in the next step)...
290             if (delimiterChars != null) {
291                 for (char delimiterChar : delimiterChars) {
292                     result = result.replace(delimiterChar, '_');
293                 }
294             }
295 
296             // Change the case at the beginning at after each underscore ...
297             return replaceAllWithUppercase(result, "(^|_)(.)", 2);
298         }
299         if (lowerCaseAndUnderscoredWord.length() < 2) return lowerCaseAndUnderscoredWord;
300         return "" + Character.toLowerCase(lowerCaseAndUnderscoredWord.charAt(0))
301                + camelCase(lowerCaseAndUnderscoredWord, true, delimiterChars).substring(1);
302     }
303 
304     /**
305      * Makes an underscored form from the expression in the string (the reverse of the {@link #camelCase(String, boolean, char[])
306      * camelCase} method. Also changes any characters that match the supplied delimiters into underscore.
307      * <p>
308      * Examples:
309      * 
310      * <pre>
311      *   inflector.underscore(&quot;activeRecord&quot;)     #=&gt; &quot;active_record&quot;
312      *   inflector.underscore(&quot;ActiveRecord&quot;)     #=&gt; &quot;active_record&quot;
313      *   inflector.underscore(&quot;firstName&quot;)        #=&gt; &quot;first_name&quot;
314      *   inflector.underscore(&quot;FirstName&quot;)        #=&gt; &quot;first_name&quot;
315      *   inflector.underscore(&quot;name&quot;)             #=&gt; &quot;name&quot;
316      *   inflector.underscore(&quot;The.firstName&quot;)    #=&gt; &quot;the_first_name&quot;
317      * </pre>
318      * 
319      * </p>
320      * 
321      * @param camelCaseWord the camel-cased word that is to be converted;
322      * @param delimiterChars optional characters that are used to delimit word boundaries (beyond capitalization)
323      * @return a lower-cased version of the input, with separate words delimited by the underscore character.
324      */
325     public String underscore( String camelCaseWord,
326                               char... delimiterChars ) {
327         if (camelCaseWord == null) return null;
328         String result = camelCaseWord.trim();
329         if (result.length() == 0) return "";
330         result = result.replaceAll("([A-Z]+)([A-Z][a-z])", "$1_$2");
331         result = result.replaceAll("([a-z\\d])([A-Z])", "$1_$2");
332         result = result.replace('-', '_');
333         if (delimiterChars != null) {
334             for (char delimiterChar : delimiterChars) {
335                 result = result.replace(delimiterChar, '_');
336             }
337         }
338         return result.toLowerCase();
339     }
340 
341     /**
342      * Returns a copy of the input with the first character converted to uppercase and the remainder to lowercase.
343      * 
344      * @param words the word to be capitalized
345      * @return the string with the first character capitalized and the remaining characters lowercased
346      */
347     public String capitalize( String words ) {
348         if (words == null) return null;
349         String result = words.trim();
350         if (result.length() == 0) return "";
351         if (result.length() == 1) return result.toUpperCase();
352         return "" + Character.toUpperCase(result.charAt(0)) + result.substring(1).toLowerCase();
353     }
354 
355     /**
356      * Capitalizes the first word and turns underscores into spaces and strips trailing "_id" and any supplied removable tokens.
357      * Like {@link #titleCase(String, String[])}, this is meant for creating pretty output.
358      * <p>
359      * Examples:
360      * 
361      * <pre>
362      *   inflector.humanize(&quot;employee_salary&quot;)       #=&gt; &quot;Employee salary&quot;
363      *   inflector.humanize(&quot;author_id&quot;)             #=&gt; &quot;Author&quot;
364      * </pre>
365      * 
366      * </p>
367      * 
368      * @param lowerCaseAndUnderscoredWords the input to be humanized
369      * @param removableTokens optional array of tokens that are to be removed
370      * @return the humanized string
371      * @see #titleCase(String, String[])
372      */
373     public String humanize( String lowerCaseAndUnderscoredWords,
374                             String... removableTokens ) {
375         if (lowerCaseAndUnderscoredWords == null) return null;
376         String result = lowerCaseAndUnderscoredWords.trim();
377         if (result.length() == 0) return "";
378         // Remove a trailing "_id" token
379         result = result.replaceAll("_id$", "");
380         // Remove all of the tokens that should be removed
381         if (removableTokens != null) {
382             for (String removableToken : removableTokens) {
383                 result = result.replaceAll(removableToken, "");
384             }
385         }
386         result = result.replaceAll("_+", " "); // replace all adjacent underscores with a single space
387         return capitalize(result);
388     }
389 
390     /**
391      * Capitalizes all the words and replaces some characters in the string to create a nicer looking title. Underscores are
392      * changed to spaces, a trailing "_id" is removed, and any of the supplied tokens are removed. Like
393      * {@link #humanize(String, String[])}, this is meant for creating pretty output.
394      * <p>
395      * Examples:
396      * 
397      * <pre>
398      *   inflector.titleCase(&quot;man from the boondocks&quot;)       #=&gt; &quot;Man From The Boondocks&quot;
399      *   inflector.titleCase(&quot;x-men: the last stand&quot;)        #=&gt; &quot;X Men: The Last Stand&quot;
400      * </pre>
401      * 
402      * </p>
403      * 
404      * @param words the input to be turned into title case
405      * @param removableTokens optional array of tokens that are to be removed
406      * @return the title-case version of the supplied words
407      */
408     public String titleCase( String words,
409                              String... removableTokens ) {
410         String result = humanize(words, removableTokens);
411         result = replaceAllWithUppercase(result, "\\b([a-z])", 1); // change first char of each word to uppercase
412         return result;
413     }
414 
415     /**
416      * Turns a non-negative number into an ordinal string used to denote the position in an ordered sequence, such as 1st, 2nd,
417      * 3rd, 4th.
418      * 
419      * @param number the non-negative number
420      * @return the string with the number and ordinal suffix
421      */
422     public String ordinalize( int number ) {
423         int remainder = number % 100;
424         String numberStr = Integer.toString(number);
425         if (11 <= number && number <= 13) return numberStr + "th";
426         remainder = number % 10;
427         if (remainder == 1) return numberStr + "st";
428         if (remainder == 2) return numberStr + "nd";
429         if (remainder == 3) return numberStr + "rd";
430         return numberStr + "th";
431     }
432 
433     // ------------------------------------------------------------------------------------------------
434     // Management methods
435     // ------------------------------------------------------------------------------------------------
436 
437     /**
438      * Determine whether the supplied word is considered uncountable by the {@link #pluralize(Object) pluralize} and
439      * {@link #singularize(Object) singularize} methods.
440      * 
441      * @param word the word
442      * @return true if the plural and singular forms of the word are the same
443      */
444     public boolean isUncountable( String word ) {
445         if (word == null) return false;
446         String trimmedLower = word.trim().toLowerCase();
447         return this.uncountables.contains(trimmedLower);
448     }
449 
450     /**
451      * Get the set of words that are not processed by the Inflector. The resulting map is directly modifiable.
452      * 
453      * @return the set of uncountable words
454      */
455     public Set<String> getUncountables() {
456         return uncountables;
457     }
458 
459     public void addPluralize( String rule,
460                               String replacement ) {
461         final Rule pluralizeRule = new Rule(rule, replacement);
462         this.plurals.addFirst(pluralizeRule);
463     }
464 
465     public void addSingularize( String rule,
466                                 String replacement ) {
467         final Rule singularizeRule = new Rule(rule, replacement);
468         this.singulars.addFirst(singularizeRule);
469     }
470 
471     public void addIrregular( String singular,
472                               String plural ) {
473         CheckArg.isNotEmpty(singular, "singular rule");
474         CheckArg.isNotEmpty(plural, "plural rule");
475         String singularRemainder = singular.length() > 1 ? singular.substring(1) : "";
476         String pluralRemainder = plural.length() > 1 ? plural.substring(1) : "";
477         addPluralize("(" + singular.charAt(0) + ")" + singularRemainder + "$", "$1" + pluralRemainder);
478         addSingularize("(" + plural.charAt(0) + ")" + pluralRemainder + "$", "$1" + singularRemainder);
479     }
480 
481     public void addUncountable( String... words ) {
482         if (words == null || words.length == 0) return;
483         for (String word : words) {
484             if (word != null) uncountables.add(word.trim().toLowerCase());
485         }
486     }
487 
488     /**
489      * Utility method to replace all occurrences given by the specific backreference with its uppercased form, and remove all
490      * other backreferences.
491      * <p>
492      * The Java {@link Pattern regular expression processing} does not use the preprocessing directives <code>\l</code>,
493      * <code>&#92;u</code>, <code>\L</code>, and <code>\U</code>. If so, such directives could be used in the replacement string
494      * to uppercase or lowercase the backreferences. For example, <code>\L1</code> would lowercase the first backreference, and
495      * <code>&#92;u3</code> would uppercase the 3rd backreference.
496      * </p>
497      * 
498      * @param input
499      * @param regex
500      * @param groupNumberToUppercase
501      * @return the input string with the appropriate characters converted to upper-case
502      */
503     protected static String replaceAllWithUppercase( String input,
504                                                      String regex,
505                                                      int groupNumberToUppercase ) {
506         Pattern underscoreAndDotPattern = Pattern.compile(regex);
507         Matcher matcher = underscoreAndDotPattern.matcher(input);
508         StringBuffer sb = new StringBuffer();
509         while (matcher.find()) {
510             matcher.appendReplacement(sb, matcher.group(groupNumberToUppercase).toUpperCase());
511         }
512         matcher.appendTail(sb);
513         return sb.toString();
514     }
515 
516     /**
517      * Completely remove all rules within this inflector.
518      */
519     public void clear() {
520         this.uncountables.clear();
521         this.plurals.clear();
522         this.singulars.clear();
523     }
524 
525     protected void initialize() {
526         Inflector inflect = this;
527         inflect.addPluralize("$", "s");
528         inflect.addPluralize("s$", "s");
529         inflect.addPluralize("(ax|test)is$", "$1es");
530         inflect.addPluralize("(octop|vir)us$", "$1i");
531         inflect.addPluralize("(octop|vir)i$", "$1i"); // already plural
532         inflect.addPluralize("(alias|status)$", "$1es");
533         inflect.addPluralize("(bu)s$", "$1ses");
534         inflect.addPluralize("(buffal|tomat)o$", "$1oes");
535         inflect.addPluralize("([ti])um$", "$1a");
536         inflect.addPluralize("([ti])a$", "$1a"); // already plural
537         inflect.addPluralize("sis$", "ses");
538         inflect.addPluralize("(?:([^f])fe|([lr])f)$", "$1$2ves");
539         inflect.addPluralize("(hive)$", "$1s");
540         inflect.addPluralize("([^aeiouy]|qu)y$", "$1ies");
541         inflect.addPluralize("(x|ch|ss|sh)$", "$1es");
542         inflect.addPluralize("(matr|vert|ind)ix|ex$", "$1ices");
543         inflect.addPluralize("([m|l])ouse$", "$1ice");
544         inflect.addPluralize("([m|l])ice$", "$1ice");
545         inflect.addPluralize("^(ox)$", "$1en");
546         inflect.addPluralize("(quiz)$", "$1zes");
547         // Need to check for the following words that are already pluralized:
548         inflect.addPluralize("(people|men|children|sexes|moves|stadiums)$", "$1"); // irregulars
549         inflect.addPluralize("(oxen|octopi|viri|aliases|quizzes)$", "$1"); // special rules
550 
551         inflect.addSingularize("s$", "");
552         inflect.addSingularize("(s|si|u)s$", "$1s"); // '-us' and '-ss' are already singular
553         inflect.addSingularize("(n)ews$", "$1ews");
554         inflect.addSingularize("([ti])a$", "$1um");
555         inflect.addSingularize("((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$", "$1$2sis");
556         inflect.addSingularize("(^analy)ses$", "$1sis");
557         inflect.addSingularize("(^analy)sis$", "$1sis"); // already singular, but ends in 's'
558         inflect.addSingularize("([^f])ves$", "$1fe");
559         inflect.addSingularize("(hive)s$", "$1");
560         inflect.addSingularize("(tive)s$", "$1");
561         inflect.addSingularize("([lr])ves$", "$1f");
562         inflect.addSingularize("([^aeiouy]|qu)ies$", "$1y");
563         inflect.addSingularize("(s)eries$", "$1eries");
564         inflect.addSingularize("(m)ovies$", "$1ovie");
565         inflect.addSingularize("(x|ch|ss|sh)es$", "$1");
566         inflect.addSingularize("([m|l])ice$", "$1ouse");
567         inflect.addSingularize("(bus)es$", "$1");
568         inflect.addSingularize("(o)es$", "$1");
569         inflect.addSingularize("(shoe)s$", "$1");
570         inflect.addSingularize("(cris|ax|test)is$", "$1is"); // already singular, but ends in 's'
571         inflect.addSingularize("(cris|ax|test)es$", "$1is");
572         inflect.addSingularize("(octop|vir)i$", "$1us");
573         inflect.addSingularize("(octop|vir)us$", "$1us"); // already singular, but ends in 's'
574         inflect.addSingularize("(alias|status)es$", "$1");
575         inflect.addSingularize("(alias|status)$", "$1"); // already singular, but ends in 's'
576         inflect.addSingularize("^(ox)en", "$1");
577         inflect.addSingularize("(vert|ind)ices$", "$1ex");
578         inflect.addSingularize("(matr)ices$", "$1ix");
579         inflect.addSingularize("(quiz)zes$", "$1");
580 
581         inflect.addIrregular("person", "people");
582         inflect.addIrregular("man", "men");
583         inflect.addIrregular("child", "children");
584         inflect.addIrregular("sex", "sexes");
585         inflect.addIrregular("move", "moves");
586         inflect.addIrregular("stadium", "stadiums");
587 
588         inflect.addUncountable("equipment", "information", "rice", "money", "species", "series", "fish", "sheep");
589     }
590 
591 }