001/*
002Copyright (c) 2011+, HL7, Inc
003All rights reserved.
004
005Redistribution and use in source and binary forms, with or without modification, 
006are permitted provided that the following conditions are met:
007
008 * Redistributions of source code must retain the above copyright notice, this 
009   list of conditions and the following disclaimer.
010 * Redistributions in binary form must reproduce the above copyright notice, 
011   this list of conditions and the following disclaimer in the documentation 
012   and/or other materials provided with the distribution.
013 * Neither the name of HL7 nor the names of its contributors may be used to 
014   endorse or promote products derived from this software without specific 
015   prior written permission.
016
017THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 
018ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 
019WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 
020IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 
021INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 
022NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 
023PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 
024WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
025ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
026POSSIBILITY OF SUCH DAMAGE.
027
028*/
029package org.hl7.fhir.utilities;
030
031/*
032 * JBoss DNA (http://www.jboss.org/dna)
033 * See the COPYRIGHT.txt file distributed with this work for information
034 * regarding copyright ownership.  Some portions may be licensed
035 * to Red Hat, Inc. under one or more contributor license agreements.
036 * See the AUTHORS.txt file in the distribution for a full listing of 
037 * individual contributors. 
038 *
039 * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
040 * is licensed to you under the terms of the GNU Lesser General Public License as
041 * published by the Free Software Foundation; either version 2.1 of
042 * the License, or (at your option) any later version.
043 *
044 * JBoss DNA is distributed in the hope that it will be useful,
045 * but WITHOUT ANY WARRANTY; without even the implied warranty of
046 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
047 * Lesser General Public License for more details.
048 *
049 * You should have received a copy of the GNU Lesser General Public
050 * License along with this software; if not, write to the Free
051 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
052 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
053 */
054
055import java.util.HashSet;
056import java.util.LinkedList;
057import java.util.Set;
058import java.util.regex.Matcher;
059import java.util.regex.Pattern;
060
061/**
062 * Transforms words to singular, plural, humanized (human readable), underscore, camel case, or ordinal form. This is inspired by
063 * the <a href="http://api.rubyonrails.org/classes/Inflector.html">Inflector</a> class in <a
064 * href="http://www.rubyonrails.org">Ruby on Rails</a>, which is distributed under the <a
065 * href="http://wiki.rubyonrails.org/rails/pages/License">Rails license</a>.
066 * 
067 * @author Randall Hauch
068 */
069public class Inflector {
070
071    protected static final Inflector INSTANCE = new Inflector();
072
073    public static final Inflector getInstance() {
074        return INSTANCE;
075    }
076
077    protected class Rule {
078
079        protected final String expression;
080        protected final Pattern expressionPattern;
081        protected final String replacement;
082
083        protected Rule( String expression,
084                        String replacement ) {
085            this.expression = expression;
086            this.replacement = replacement != null ? replacement : "";
087            this.expressionPattern = Pattern.compile(this.expression, Pattern.CASE_INSENSITIVE);
088        }
089
090        /**
091         * Apply the rule against the input string, returning the modified string or null if the rule didn't apply (and no
092         * modifications were made)
093         * 
094         * @param input the input string
095         * @return the modified string if this rule applied, or null if the input was not modified by this rule
096         */
097        protected String apply( String input ) {
098            Matcher matcher = this.expressionPattern.matcher(input);
099            if (!matcher.find()) return null;
100            return matcher.replaceAll(this.replacement);
101        }
102
103        @Override
104        public int hashCode() {
105            return expression.hashCode();
106        }
107
108        @Override
109        public boolean equals( Object obj ) {
110            if (obj == this) return true;
111            if (obj != null && obj.getClass() == this.getClass()) {
112                final Rule that = (Rule)obj;
113                if (this.expression.equalsIgnoreCase(that.expression)) return true;
114            }
115            return false;
116        }
117
118        @Override
119        public String toString() {
120            return expression + ", " + replacement;
121        }
122    }
123
124    private LinkedList<Rule> plurals = new LinkedList<Rule>();
125    private LinkedList<Rule> singulars = new LinkedList<Rule>();
126    /**
127     * The lowercase words that are to be excluded and not processed. This map can be modified by the users via
128     * {@link #getUncountables()}.
129     */
130    private final Set<String> uncountables = new HashSet<String>();
131
132    public Inflector() {
133        initialize();
134    }
135
136    protected Inflector( Inflector original ) {
137        this.plurals.addAll(original.plurals);
138        this.singulars.addAll(original.singulars);
139        this.uncountables.addAll(original.uncountables);
140    }
141
142    @Override
143    public Inflector clone() {
144        return new Inflector(this);
145    }
146
147    // ------------------------------------------------------------------------------------------------
148    // Usage functions
149    // ------------------------------------------------------------------------------------------------
150
151    /**
152     * Returns the plural form of the word in the string.
153     * 
154     * Examples:
155     * 
156     * <pre>
157     *   inflector.pluralize(&quot;post&quot;)               #=&gt; &quot;posts&quot;
158     *   inflector.pluralize(&quot;octopus&quot;)            #=&gt; &quot;octopi&quot;
159     *   inflector.pluralize(&quot;sheep&quot;)              #=&gt; &quot;sheep&quot;
160     *   inflector.pluralize(&quot;words&quot;)              #=&gt; &quot;words&quot;
161     *   inflector.pluralize(&quot;the blue mailman&quot;)   #=&gt; &quot;the blue mailmen&quot;
162     *   inflector.pluralize(&quot;CamelOctopus&quot;)       #=&gt; &quot;CamelOctopi&quot;
163     * </pre>
164     * 
165     * 
166     * 
167     * Note that if the {@link Object#toString()} is called on the supplied object, so this method works for non-strings, too.
168     * 
169     * 
170     * @param word the word that is to be pluralized.
171     * @return the pluralized form of the word, or the word itself if it could not be pluralized
172     * @see #singularize(Object)
173     */
174    public String pluralize( Object word ) {
175        if (word == null) return null;
176        String wordStr = word.toString().trim();
177        if (wordStr.length() == 0) return wordStr;
178        if (isUncountable(wordStr)) return wordStr;
179        for (Rule rule : this.plurals) {
180            String result = rule.apply(wordStr);
181            if (result != null) return result;
182        }
183        return wordStr;
184    }
185
186    public String pluralize( Object word,
187                             int count ) {
188        if (word == null) return null;
189        if (count == 1 || count == -1) {
190            return word.toString();
191        }
192        return pluralize(word);
193    }
194
195    /**
196     * Returns the singular form of the word in the string.
197     * 
198     * Examples:
199     * 
200     * <pre>
201     *   inflector.singularize(&quot;posts&quot;)             #=&gt; &quot;post&quot;
202     *   inflector.singularize(&quot;octopi&quot;)            #=&gt; &quot;octopus&quot;
203     *   inflector.singularize(&quot;sheep&quot;)             #=&gt; &quot;sheep&quot;
204     *   inflector.singularize(&quot;words&quot;)             #=&gt; &quot;word&quot;
205     *   inflector.singularize(&quot;the blue mailmen&quot;)  #=&gt; &quot;the blue mailman&quot;
206     *   inflector.singularize(&quot;CamelOctopi&quot;)       #=&gt; &quot;CamelOctopus&quot;
207     * </pre>
208     * 
209     * 
210     * 
211     * Note that if the {@link Object#toString()} is called on the supplied object, so this method works for non-strings, too.
212     * 
213     * 
214     * @param word the word that is to be pluralized.
215     * @return the pluralized form of the word, or the word itself if it could not be pluralized
216     * @see #pluralize(Object)
217     */
218    public String singularize( Object word ) {
219        if (word == null) return null;
220        String wordStr = word.toString().trim();
221        if (wordStr.length() == 0) return wordStr;
222        if (isUncountable(wordStr)) return wordStr;
223        for (Rule rule : this.singulars) {
224            String result = rule.apply(wordStr);
225            if (result != null) return result;
226        }
227        return wordStr;
228    }
229
230    /**
231     * Converts strings to lowerCamelCase. This method will also use any extra delimiter characters to identify word boundaries.
232     * 
233     * Examples:
234     * 
235     * <pre>
236     *   inflector.lowerCamelCase(&quot;active_record&quot;)       #=&gt; &quot;activeRecord&quot;
237     *   inflector.lowerCamelCase(&quot;first_name&quot;)          #=&gt; &quot;firstName&quot;
238     *   inflector.lowerCamelCase(&quot;name&quot;)                #=&gt; &quot;name&quot;
239     *   inflector.lowerCamelCase(&quot;the-first_name&quot;,'-')  #=&gt; &quot;theFirstName&quot;
240     * </pre>
241     * 
242     * 
243     * 
244     * @param lowerCaseAndUnderscoredWord the word that is to be converted to camel case
245     * @param delimiterChars optional characters that are used to delimit word boundaries
246     * @return the lower camel case version of the word
247     * @see #underscore(String, char[])
248     * @see #camelCase(String, boolean, char[])
249     * @see #upperCamelCase(String, char[])
250     */
251    public String lowerCamelCase( String lowerCaseAndUnderscoredWord,
252                                  char... delimiterChars ) {
253        return camelCase(lowerCaseAndUnderscoredWord, false, delimiterChars);
254    }
255
256    /**
257     * Converts strings to UpperCamelCase. This method will also use any extra delimiter characters to identify word boundaries.
258     * 
259     * Examples:
260     * 
261     * <pre>
262     *   inflector.upperCamelCase(&quot;active_record&quot;)       #=&gt; &quot;SctiveRecord&quot;
263     *   inflector.upperCamelCase(&quot;first_name&quot;)          #=&gt; &quot;FirstName&quot;
264     *   inflector.upperCamelCase(&quot;name&quot;)                #=&gt; &quot;Name&quot;
265     *   inflector.lowerCamelCase(&quot;the-first_name&quot;,'-')  #=&gt; &quot;TheFirstName&quot;
266     * </pre>
267     * 
268     * 
269     * 
270     * @param lowerCaseAndUnderscoredWord the word that is to be converted to camel case
271     * @param delimiterChars optional characters that are used to delimit word boundaries
272     * @return the upper camel case version of the word
273     * @see #underscore(String, char[])
274     * @see #camelCase(String, boolean, char[])
275     * @see #lowerCamelCase(String, char[])
276     */
277    public String upperCamelCase( String lowerCaseAndUnderscoredWord,
278                                  char... delimiterChars ) {
279        return camelCase(lowerCaseAndUnderscoredWord, true, delimiterChars);
280    }
281
282    /**
283     * By default, this method converts strings to UpperCamelCase. If the <code>uppercaseFirstLetter</code> argument to false,
284     * then this method produces lowerCamelCase. This method will also use any extra delimiter characters to identify word
285     * boundaries.
286     * 
287     * Examples:
288     * 
289     * <pre>
290     *   inflector.camelCase(&quot;active_record&quot;,false)    #=&gt; &quot;activeRecord&quot;
291     *   inflector.camelCase(&quot;active_record&quot;,true)     #=&gt; &quot;ActiveRecord&quot;
292     *   inflector.camelCase(&quot;first_name&quot;,false)       #=&gt; &quot;firstName&quot;
293     *   inflector.camelCase(&quot;first_name&quot;,true)        #=&gt; &quot;FirstName&quot;
294     *   inflector.camelCase(&quot;name&quot;,false)             #=&gt; &quot;name&quot;
295     *   inflector.camelCase(&quot;name&quot;,true)              #=&gt; &quot;Name&quot;
296     * </pre>
297     * 
298     * 
299     * 
300     * @param lowerCaseAndUnderscoredWord the word that is to be converted to camel case
301     * @param uppercaseFirstLetter true if the first character is to be uppercased, or false if the first character is to be
302     *        lowercased
303     * @param delimiterChars optional characters that are used to delimit word boundaries
304     * @return the camel case version of the word
305     * @see #underscore(String, char[])
306     * @see #upperCamelCase(String, char[])
307     * @see #lowerCamelCase(String, char[])
308     */
309    public String camelCase( String lowerCaseAndUnderscoredWord,
310                             boolean uppercaseFirstLetter,
311                             char... delimiterChars ) {
312        if (lowerCaseAndUnderscoredWord == null) return null;
313        lowerCaseAndUnderscoredWord = lowerCaseAndUnderscoredWord.trim();
314        if (lowerCaseAndUnderscoredWord.length() == 0) return "";
315        if (uppercaseFirstLetter) {
316            String result = lowerCaseAndUnderscoredWord;
317            // Replace any extra delimiters with underscores (before the underscores are converted in the next step)...
318            if (delimiterChars != null) {
319                for (char delimiterChar : delimiterChars) {
320                    result = result.replace(delimiterChar, '_');
321                }
322            }
323
324            // Change the case at the beginning at after each underscore ...
325            return replaceAllWithUppercase(result, "(^|_)(.)", 2);
326        }
327        if (lowerCaseAndUnderscoredWord.length() < 2) return lowerCaseAndUnderscoredWord;
328        return "" + Character.toLowerCase(lowerCaseAndUnderscoredWord.charAt(0))
329               + camelCase(lowerCaseAndUnderscoredWord, true, delimiterChars).substring(1);
330    }
331
332    /**
333     * Makes an underscored form from the expression in the string (the reverse of the {@link #camelCase(String, boolean, char[])
334     * camelCase} method. Also changes any characters that match the supplied delimiters into underscore.
335     * 
336     * Examples:
337     * 
338     * <pre>
339     *   inflector.underscore(&quot;activeRecord&quot;)     #=&gt; &quot;active_record&quot;
340     *   inflector.underscore(&quot;ActiveRecord&quot;)     #=&gt; &quot;active_record&quot;
341     *   inflector.underscore(&quot;firstName&quot;)        #=&gt; &quot;first_name&quot;
342     *   inflector.underscore(&quot;FirstName&quot;)        #=&gt; &quot;first_name&quot;
343     *   inflector.underscore(&quot;name&quot;)             #=&gt; &quot;name&quot;
344     *   inflector.underscore(&quot;The.firstName&quot;)    #=&gt; &quot;the_first_name&quot;
345     * </pre>
346     * 
347     * 
348     * 
349     * @param camelCaseWord the camel-cased word that is to be converted;
350     * @param delimiterChars optional characters that are used to delimit word boundaries (beyond capitalization)
351     * @return a lower-cased version of the input, with separate words delimited by the underscore character.
352     */
353    public String underscore( String camelCaseWord,
354                              char... delimiterChars ) {
355        if (camelCaseWord == null) return null;
356        String result = camelCaseWord.trim();
357        if (result.length() == 0) return "";
358        result = result.replaceAll("([A-Z]+)([A-Z][a-z])", "$1_$2");
359        result = result.replaceAll("([a-z\\d])([A-Z])", "$1_$2");
360        result = result.replace('-', '_');
361        if (delimiterChars != null) {
362            for (char delimiterChar : delimiterChars) {
363                result = result.replace(delimiterChar, '_');
364            }
365        }
366        return result.toLowerCase();
367    }
368
369    /**
370     * Returns a copy of the input with the first character converted to uppercase and the remainder to lowercase.
371     * 
372     * @param words the word to be capitalized
373     * @return the string with the first character capitalized and the remaining characters lowercased
374     */
375    public String capitalize( String words ) {
376        if (words == null) return null;
377        String result = words.trim();
378        if (result.length() == 0) return "";
379        if (result.length() == 1) return result.toUpperCase();
380        return "" + Character.toUpperCase(result.charAt(0)) + result.substring(1).toLowerCase();
381    }
382
383    /**
384     * Capitalizes the first word and turns underscores into spaces and strips trailing "_id" and any supplied removable tokens.
385     * Like {@link #titleCase(String, String[])}, this is meant for creating pretty output.
386     * 
387     * Examples:
388     * 
389     * <pre>
390     *   inflector.humanize(&quot;employee_salary&quot;)       #=&gt; &quot;Employee salary&quot;
391     *   inflector.humanize(&quot;author_id&quot;)             #=&gt; &quot;Author&quot;
392     * </pre>
393     * 
394     * 
395     * 
396     * @param lowerCaseAndUnderscoredWords the input to be humanized
397     * @param removableTokens optional array of tokens that are to be removed
398     * @return the humanized string
399     * @see #titleCase(String, String[])
400     */
401    public String humanize( String lowerCaseAndUnderscoredWords,
402                            String... removableTokens ) {
403        if (lowerCaseAndUnderscoredWords == null) return null;
404        String result = lowerCaseAndUnderscoredWords.trim();
405        if (result.length() == 0) return "";
406        // Remove a trailing "_id" token
407        result = result.replaceAll("_id$", "");
408        // Remove all of the tokens that should be removed
409        if (removableTokens != null) {
410            for (String removableToken : removableTokens) {
411                result = result.replaceAll(removableToken, "");
412            }
413        }
414        result = result.replaceAll("_+", " "); // replace all adjacent underscores with a single space
415        return capitalize(result);
416    }
417
418    /**
419     * Capitalizes all the words and replaces some characters in the string to create a nicer looking title. Underscores are
420     * changed to spaces, a trailing "_id" is removed, and any of the supplied tokens are removed. Like
421     * {@link #humanize(String, String[])}, this is meant for creating pretty output.
422     * 
423     * Examples:
424     * 
425     * <pre>
426     *   inflector.titleCase(&quot;man from the boondocks&quot;)       #=&gt; &quot;Man From The Boondocks&quot;
427     *   inflector.titleCase(&quot;x-men: the last stand&quot;)        #=&gt; &quot;X Men: The Last Stand&quot;
428     * </pre>
429     * 
430     * 
431     * 
432     * @param words the input to be turned into title case
433     * @param removableTokens optional array of tokens that are to be removed
434     * @return the title-case version of the supplied words
435     */
436    public String titleCase( String words,
437                             String... removableTokens ) {
438        String result = humanize(words, removableTokens);
439        result = replaceAllWithUppercase(result, "\\b([a-z])", 1); // change first char of each word to uppercase
440        return result;
441    }
442
443    /**
444     * Turns a non-negative number into an ordinal string used to denote the position in an ordered sequence, such as 1st, 2nd,
445     * 3rd, 4th.
446     * 
447     * @param number the non-negative number
448     * @return the string with the number and ordinal suffix
449     */
450    public String ordinalize( int number ) {
451        int remainder = number % 100;
452        String numberStr = Integer.toString(number);
453        if (11 <= number && number <= 13) return numberStr + "th";
454        remainder = number % 10;
455        if (remainder == 1) return numberStr + "st";
456        if (remainder == 2) return numberStr + "nd";
457        if (remainder == 3) return numberStr + "rd";
458        return numberStr + "th";
459    }
460
461    // ------------------------------------------------------------------------------------------------
462    // Management methods
463    // ------------------------------------------------------------------------------------------------
464
465    /**
466     * Determine whether the supplied word is considered uncountable by the {@link #pluralize(Object) pluralize} and
467     * {@link #singularize(Object) singularize} methods.
468     * 
469     * @param word the word
470     * @return true if the plural and singular forms of the word are the same
471     */
472    public boolean isUncountable( String word ) {
473        if (word == null) return false;
474        String trimmedLower = word.trim().toLowerCase();
475        return this.uncountables.contains(trimmedLower);
476    }
477
478    /**
479     * Get the set of words that are not processed by the Inflector. The resulting map is directly modifiable.
480     * 
481     * @return the set of uncountable words
482     */
483    public Set<String> getUncountables() {
484        return uncountables;
485    }
486
487    public void addPluralize( String rule,
488                              String replacement ) {
489        final Rule pluralizeRule = new Rule(rule, replacement);
490        this.plurals.addFirst(pluralizeRule);
491    }
492
493    public void addSingularize( String rule,
494                                String replacement ) {
495        final Rule singularizeRule = new Rule(rule, replacement);
496        this.singulars.addFirst(singularizeRule);
497    }
498
499    public void addIrregular( String singular,
500                              String plural ) {
501        //CheckArg.isNotEmpty(singular, "singular rule");
502        //CheckArg.isNotEmpty(plural, "plural rule");
503        String singularRemainder = singular.length() > 1 ? singular.substring(1) : "";
504        String pluralRemainder = plural.length() > 1 ? plural.substring(1) : "";
505        addPluralize("(" + singular.charAt(0) + ")" + singularRemainder + "$", "$1" + pluralRemainder);
506        addSingularize("(" + plural.charAt(0) + ")" + pluralRemainder + "$", "$1" + singularRemainder);
507    }
508
509    public void addUncountable( String... words ) {
510        if (words == null || words.length == 0) return;
511        for (String word : words) {
512            if (word != null) uncountables.add(word.trim().toLowerCase());
513        }
514    }
515
516    /**
517     * Utility method to replace all occurrences given by the specific backreference with its uppercased form, and remove all
518     * other backreferences.
519     * 
520     * The Java {@link Pattern regular expression processing} does not use the preprocessing directives <code>\l</code>,
521     * <code>&#92;u</code>, <code>\L</code>, and <code>\U</code>. If so, such directives could be used in the replacement string
522     * to uppercase or lowercase the backreferences. For example, <code>\L1</code> would lowercase the first backreference, and
523     * <code>&#92;u3</code> would uppercase the 3rd backreference.
524     * 
525     * 
526     * @param input
527     * @param regex
528     * @param groupNumberToUppercase
529     * @return the input string with the appropriate characters converted to upper-case
530     */
531    protected static String replaceAllWithUppercase( String input,
532                                                     String regex,
533                                                     int groupNumberToUppercase ) {
534        Pattern underscoreAndDotPattern = Pattern.compile(regex);
535        Matcher matcher = underscoreAndDotPattern.matcher(input);
536        StringBuffer sb = new StringBuffer();
537        while (matcher.find()) {
538            matcher.appendReplacement(sb, matcher.group(groupNumberToUppercase).toUpperCase());
539        }
540        matcher.appendTail(sb);
541        return sb.toString();
542    }
543
544    /**
545     * Completely remove all rules within this inflector.
546     */
547    public void clear() {
548        this.uncountables.clear();
549        this.plurals.clear();
550        this.singulars.clear();
551    }
552
553    protected void initialize() {
554        Inflector inflect = this;
555        inflect.addPluralize("$", "s");
556        inflect.addPluralize("s$", "s");
557        inflect.addPluralize("(ax|test)is$", "$1es");
558        inflect.addPluralize("(octop|vir)us$", "$1i");
559        inflect.addPluralize("(octop|vir)i$", "$1i"); // already plural
560        inflect.addPluralize("(alias|status)$", "$1es");
561        inflect.addPluralize("(bu)s$", "$1ses");
562        inflect.addPluralize("(buffal|tomat)o$", "$1oes");
563        inflect.addPluralize("([ti])um$", "$1a");
564        inflect.addPluralize("([ti])a$", "$1a"); // already plural
565        inflect.addPluralize("sis$", "ses");
566        inflect.addPluralize("(?:([^f])fe|([lr])f)$", "$1$2ves");
567        inflect.addPluralize("(hive)$", "$1s");
568        inflect.addPluralize("([^aeiouy]|qu)y$", "$1ies");
569        inflect.addPluralize("(x|ch|ss|sh)$", "$1es");
570        inflect.addPluralize("(matr|vert|ind)ix|ex$", "$1ices");
571        inflect.addPluralize("([m|l])ouse$", "$1ice");
572        inflect.addPluralize("([m|l])ice$", "$1ice");
573        inflect.addPluralize("^(ox)$", "$1en");
574        inflect.addPluralize("(quiz)$", "$1zes");
575        // Need to check for the following words that are already pluralized:
576        inflect.addPluralize("(people|men|children|sexes|moves|stadiums)$", "$1"); // irregulars
577        inflect.addPluralize("(oxen|octopi|viri|aliases|quizzes)$", "$1"); // special rules
578
579        inflect.addSingularize("s$", "");
580        inflect.addSingularize("(s|si|u)s$", "$1s"); // '-us' and '-ss' are already singular
581        inflect.addSingularize("(n)ews$", "$1ews");
582        inflect.addSingularize("([ti])a$", "$1um");
583        inflect.addSingularize("((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$", "$1$2sis");
584        inflect.addSingularize("(^analy)ses$", "$1sis");
585        inflect.addSingularize("(^analy)sis$", "$1sis"); // already singular, but ends in 's'
586        inflect.addSingularize("([^f])ves$", "$1fe");
587        inflect.addSingularize("(hive)s$", "$1");
588        inflect.addSingularize("(tive)s$", "$1");
589        inflect.addSingularize("([lr])ves$", "$1f");
590        inflect.addSingularize("([^aeiouy]|qu)ies$", "$1y");
591        inflect.addSingularize("(s)eries$", "$1eries");
592        inflect.addSingularize("(m)ovies$", "$1ovie");
593        inflect.addSingularize("(x|ch|ss|sh)es$", "$1");
594        inflect.addSingularize("([m|l])ice$", "$1ouse");
595        inflect.addSingularize("(bus)es$", "$1");
596        inflect.addSingularize("(o)es$", "$1");
597        inflect.addSingularize("(shoe)s$", "$1");
598        inflect.addSingularize("(cris|ax|test)is$", "$1is"); // already singular, but ends in 's'
599        inflect.addSingularize("(cris|ax|test)es$", "$1is");
600        inflect.addSingularize("(octop|vir)i$", "$1us");
601        inflect.addSingularize("(octop|vir)us$", "$1us"); // already singular, but ends in 's'
602        inflect.addSingularize("(alias|status)es$", "$1");
603        inflect.addSingularize("(alias|status)$", "$1"); // already singular, but ends in 's'
604        inflect.addSingularize("^(ox)en", "$1");
605        inflect.addSingularize("(vert|ind)ices$", "$1ex");
606        inflect.addSingularize("(matr)ices$", "$1ix");
607        inflect.addSingularize("(quiz)zes$", "$1");
608
609        inflect.addIrregular("person", "people");
610        inflect.addIrregular("man", "men");
611        inflect.addIrregular("child", "children");
612        inflect.addIrregular("sex", "sexes");
613        inflect.addIrregular("move", "moves");
614        inflect.addIrregular("stadium", "stadiums");
615
616        inflect.addUncountable("equipment", "information", "rice", "money", "species", "series", "fish", "sheep");
617    }
618
619}