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