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("post") #=> "posts" 179 * inflector.pluralize("octopus") #=> "octopi" 180 * inflector.pluralize("sheep") #=> "sheep" 181 * inflector.pluralize("words") #=> "words" 182 * inflector.pluralize("the blue mailman") #=> "the blue mailmen" 183 * inflector.pluralize("CamelOctopus") #=> "CamelOctopi" 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("posts") #=> "post" 223 * inflector.singularize("octopi") #=> "octopus" 224 * inflector.singularize("sheep") #=> "sheep" 225 * inflector.singularize("words") #=> "word" 226 * inflector.singularize("the blue mailmen") #=> "the blue mailman" 227 * inflector.singularize("CamelOctopi") #=> "CamelOctopus" 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("active_record") #=> "activeRecord" 258 * inflector.lowerCamelCase("first_name") #=> "firstName" 259 * inflector.lowerCamelCase("name") #=> "name" 260 * inflector.lowerCamelCase("the-first_name",'-') #=> "theFirstName" 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("active_record") #=> "SctiveRecord" 284 * inflector.upperCamelCase("first_name") #=> "FirstName" 285 * inflector.upperCamelCase("name") #=> "Name" 286 * inflector.lowerCamelCase("the-first_name",'-') #=> "TheFirstName" 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("active_record",false) #=> "activeRecord" 312 * inflector.camelCase("active_record",true) #=> "ActiveRecord" 313 * inflector.camelCase("first_name",false) #=> "firstName" 314 * inflector.camelCase("first_name",true) #=> "FirstName" 315 * inflector.camelCase("name",false) #=> "name" 316 * inflector.camelCase("name",true) #=> "Name" 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("activeRecord") #=> "active_record" 361 * inflector.underscore("ActiveRecord") #=> "active_record" 362 * inflector.underscore("firstName") #=> "first_name" 363 * inflector.underscore("FirstName") #=> "first_name" 364 * inflector.underscore("name") #=> "name" 365 * inflector.underscore("The.firstName") #=> "the_first_name" 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("employee_salary") #=> "Employee salary" 412 * inflector.humanize("author_id") #=> "Author" 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("man from the boondocks") #=> "Man From The Boondocks" 448 * inflector.titleCase("x-men: the last stand") #=> "X Men: The Last Stand" 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>\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>\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}