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("post") #=> "posts" 158 * inflector.pluralize("octopus") #=> "octopi" 159 * inflector.pluralize("sheep") #=> "sheep" 160 * inflector.pluralize("words") #=> "words" 161 * inflector.pluralize("the blue mailman") #=> "the blue mailmen" 162 * inflector.pluralize("CamelOctopus") #=> "CamelOctopi" 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("posts") #=> "post" 202 * inflector.singularize("octopi") #=> "octopus" 203 * inflector.singularize("sheep") #=> "sheep" 204 * inflector.singularize("words") #=> "word" 205 * inflector.singularize("the blue mailmen") #=> "the blue mailman" 206 * inflector.singularize("CamelOctopi") #=> "CamelOctopus" 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("active_record") #=> "activeRecord" 237 * inflector.lowerCamelCase("first_name") #=> "firstName" 238 * inflector.lowerCamelCase("name") #=> "name" 239 * inflector.lowerCamelCase("the-first_name",'-') #=> "theFirstName" 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("active_record") #=> "SctiveRecord" 263 * inflector.upperCamelCase("first_name") #=> "FirstName" 264 * inflector.upperCamelCase("name") #=> "Name" 265 * inflector.lowerCamelCase("the-first_name",'-') #=> "TheFirstName" 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("active_record",false) #=> "activeRecord" 291 * inflector.camelCase("active_record",true) #=> "ActiveRecord" 292 * inflector.camelCase("first_name",false) #=> "firstName" 293 * inflector.camelCase("first_name",true) #=> "FirstName" 294 * inflector.camelCase("name",false) #=> "name" 295 * inflector.camelCase("name",true) #=> "Name" 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("activeRecord") #=> "active_record" 340 * inflector.underscore("ActiveRecord") #=> "active_record" 341 * inflector.underscore("firstName") #=> "first_name" 342 * inflector.underscore("FirstName") #=> "first_name" 343 * inflector.underscore("name") #=> "name" 344 * inflector.underscore("The.firstName") #=> "the_first_name" 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("employee_salary") #=> "Employee salary" 391 * inflector.humanize("author_id") #=> "Author" 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("man from the boondocks") #=> "Man From The Boondocks" 427 * inflector.titleCase("x-men: the last stand") #=> "X Men: The Last Stand" 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>\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>\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}