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