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("post") #=> "posts" 161 * inflector.pluralize("octopus") #=> "octopi" 162 * inflector.pluralize("sheep") #=> "sheep" 163 * inflector.pluralize("words") #=> "words" 164 * inflector.pluralize("the blue mailman") #=> "the blue mailmen" 165 * inflector.pluralize("CamelOctopus") #=> "CamelOctopi" 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("posts") #=> "post" 205 * inflector.singularize("octopi") #=> "octopus" 206 * inflector.singularize("sheep") #=> "sheep" 207 * inflector.singularize("words") #=> "word" 208 * inflector.singularize("the blue mailmen") #=> "the blue mailman" 209 * inflector.singularize("CamelOctopi") #=> "CamelOctopus" 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("active_record") #=> "activeRecord" 240 * inflector.lowerCamelCase("first_name") #=> "firstName" 241 * inflector.lowerCamelCase("name") #=> "name" 242 * inflector.lowerCamelCase("the-first_name",'-') #=> "theFirstName" 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("active_record") #=> "SctiveRecord" 266 * inflector.upperCamelCase("first_name") #=> "FirstName" 267 * inflector.upperCamelCase("name") #=> "Name" 268 * inflector.lowerCamelCase("the-first_name",'-') #=> "TheFirstName" 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("active_record",false) #=> "activeRecord" 294 * inflector.camelCase("active_record",true) #=> "ActiveRecord" 295 * inflector.camelCase("first_name",false) #=> "firstName" 296 * inflector.camelCase("first_name",true) #=> "FirstName" 297 * inflector.camelCase("name",false) #=> "name" 298 * inflector.camelCase("name",true) #=> "Name" 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("activeRecord") #=> "active_record" 343 * inflector.underscore("ActiveRecord") #=> "active_record" 344 * inflector.underscore("firstName") #=> "first_name" 345 * inflector.underscore("FirstName") #=> "first_name" 346 * inflector.underscore("name") #=> "name" 347 * inflector.underscore("The.firstName") #=> "the_first_name" 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("employee_salary") #=> "Employee salary" 394 * inflector.humanize("author_id") #=> "Author" 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("man from the boondocks") #=> "Man From The Boondocks" 430 * inflector.titleCase("x-men: the last stand") #=> "X Men: The Last Stand" 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>\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>\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}