Mega Code Archive

 
Categories / Java / Data Type
 

Transforms words to singular, plural, humanized (human readable), underscore, camel case, or ordinal form

/*  * JBoss DNA (http://www.jboss.org/dna)  * See the COPYRIGHT.txt file distributed with this work for information  * regarding copyright ownership.  Some portions may be licensed  * to Red Hat, Inc. under one or more contributor license agreements.  * See the AUTHORS.txt file in the distribution for a full listing of   * individual contributors.   *  * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA  * is licensed to you under the terms of the GNU Lesser General Public License as  * published by the Free Software Foundation; either version 2.1 of  * the License, or (at your option) any later version.  *  * JBoss DNA is distributed in the hope that it will be useful,  * but WITHOUT ANY WARRANTY; without even the implied warranty of  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU  * Lesser General Public License for more details.  *  * You should have received a copy of the GNU Lesser General Public  * License along with this software; if not, write to the Free  * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  * 02110-1301 USA, or see the FSF site: http://www.fsf.org.  */ import java.util.HashSet; import java.util.LinkedList; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; /**  * Transforms words to singular, plural, humanized (human readable), underscore, camel case, or ordinal form. This is inspired by  * the <a href="http://api.rubyonrails.org/classes/Inflector.html">Inflector</a> class in <a  * href="http://www.rubyonrails.org">Ruby on Rails</a>, which is distributed under the <a  * href="http://wiki.rubyonrails.org/rails/pages/License">Rails license</a>.  *   * @author Randall Hauch  */ public class Inflector {     protected static final Inflector INSTANCE = new Inflector();     public static final Inflector getInstance() {         return INSTANCE;     }     protected class Rule {         protected final String expression;         protected final Pattern expressionPattern;         protected final String replacement;         protected Rule( String expression,                         String replacement ) {             this.expression = expression;             this.replacement = replacement != null ? replacement : "";             this.expressionPattern = Pattern.compile(this.expression, Pattern.CASE_INSENSITIVE);         }         /**          * Apply the rule against the input string, returning the modified string or null if the rule didn't apply (and no          * modifications were made)          *           * @param input the input string          * @return the modified string if this rule applied, or null if the input was not modified by this rule          */         protected String apply( String input ) {             Matcher matcher = this.expressionPattern.matcher(input);             if (!matcher.find()) return null;             return matcher.replaceAll(this.replacement);         }         @Override         public int hashCode() {             return expression.hashCode();         }         @Override         public boolean equals( Object obj ) {             if (obj == this) return true;             if (obj != null && obj.getClass() == this.getClass()) {                 final Rule that = (Rule)obj;                 if (this.expression.equalsIgnoreCase(that.expression)) return true;             }             return false;         }         @Override         public String toString() {             return expression + ", " + replacement;         }     }     private LinkedList<Rule> plurals = new LinkedList<Rule>();     private LinkedList<Rule> singulars = new LinkedList<Rule>();     /**      * The lowercase words that are to be excluded and not processed. This map can be modified by the users via      * {@link #getUncountables()}.      */     private final Set<String> uncountables = new HashSet<String>();     public Inflector() {         initialize();     }     protected Inflector( Inflector original ) {         this.plurals.addAll(original.plurals);         this.singulars.addAll(original.singulars);         this.uncountables.addAll(original.uncountables);     }     @Override     public Inflector clone() {         return new Inflector(this);     }     // ------------------------------------------------------------------------------------------------     // Usage functions     // ------------------------------------------------------------------------------------------------     /**      * Returns the plural form of the word in the string.      * <p>      * Examples:      *       * <pre>      *   inflector.pluralize(&quot;post&quot;)               #=&gt; &quot;posts&quot;      *   inflector.pluralize(&quot;octopus&quot;)            #=&gt; &quot;octopi&quot;      *   inflector.pluralize(&quot;sheep&quot;)              #=&gt; &quot;sheep&quot;      *   inflector.pluralize(&quot;words&quot;)              #=&gt; &quot;words&quot;      *   inflector.pluralize(&quot;the blue mailman&quot;)   #=&gt; &quot;the blue mailmen&quot;      *   inflector.pluralize(&quot;CamelOctopus&quot;)       #=&gt; &quot;CamelOctopi&quot;      * </pre>      *       * </p>      * <p>      * Note that if the {@link Object#toString()} is called on the supplied object, so this method works for non-strings, too.      * </p>      *       * @param word the word that is to be pluralized.      * @return the pluralized form of the word, or the word itself if it could not be pluralized      * @see #singularize(Object)      */     public String pluralize( Object word ) {         if (word == null) return null;         String wordStr = word.toString().trim();         if (wordStr.length() == 0) return wordStr;         if (isUncountable(wordStr)) return wordStr;         for (Rule rule : this.plurals) {             String result = rule.apply(wordStr);             if (result != null) return result;         }         return wordStr;     }     public String pluralize( Object word,                              int count ) {         if (word == null) return null;         if (count == 1 || count == -1) {             return word.toString();         }         return pluralize(word);     }     /**      * Returns the singular form of the word in the string.      * <p>      * Examples:      *       * <pre>      *   inflector.singularize(&quot;posts&quot;)             #=&gt; &quot;post&quot;      *   inflector.singularize(&quot;octopi&quot;)            #=&gt; &quot;octopus&quot;      *   inflector.singularize(&quot;sheep&quot;)             #=&gt; &quot;sheep&quot;      *   inflector.singularize(&quot;words&quot;)             #=&gt; &quot;word&quot;      *   inflector.singularize(&quot;the blue mailmen&quot;)  #=&gt; &quot;the blue mailman&quot;      *   inflector.singularize(&quot;CamelOctopi&quot;)       #=&gt; &quot;CamelOctopus&quot;      * </pre>      *       * </p>      * <p>      * Note that if the {@link Object#toString()} is called on the supplied object, so this method works for non-strings, too.      * </p>      *       * @param word the word that is to be pluralized.      * @return the pluralized form of the word, or the word itself if it could not be pluralized      * @see #pluralize(Object)      */     public String singularize( Object word ) {         if (word == null) return null;         String wordStr = word.toString().trim();         if (wordStr.length() == 0) return wordStr;         if (isUncountable(wordStr)) return wordStr;         for (Rule rule : this.singulars) {             String result = rule.apply(wordStr);             if (result != null) return result;         }         return wordStr;     }     /**      * Converts strings to lowerCamelCase. This method will also use any extra delimiter characters to identify word boundaries.      * <p>      * Examples:      *       * <pre>      *   inflector.lowerCamelCase(&quot;active_record&quot;)       #=&gt; &quot;activeRecord&quot;      *   inflector.lowerCamelCase(&quot;first_name&quot;)          #=&gt; &quot;firstName&quot;      *   inflector.lowerCamelCase(&quot;name&quot;)                #=&gt; &quot;name&quot;      *   inflector.lowerCamelCase(&quot;the-first_name&quot;,'-')  #=&gt; &quot;theFirstName&quot;      * </pre>      *       * </p>      *       * @param lowerCaseAndUnderscoredWord the word that is to be converted to camel case      * @param delimiterChars optional characters that are used to delimit word boundaries      * @return the lower camel case version of the word      * @see #underscore(String, char[])      * @see #camelCase(String, boolean, char[])      * @see #upperCamelCase(String, char[])      */     public String lowerCamelCase( String lowerCaseAndUnderscoredWord,                                   char... delimiterChars ) {         return camelCase(lowerCaseAndUnderscoredWord, false, delimiterChars);     }     /**      * Converts strings to UpperCamelCase. This method will also use any extra delimiter characters to identify word boundaries.      * <p>      * Examples:      *       * <pre>      *   inflector.upperCamelCase(&quot;active_record&quot;)       #=&gt; &quot;SctiveRecord&quot;      *   inflector.upperCamelCase(&quot;first_name&quot;)          #=&gt; &quot;FirstName&quot;      *   inflector.upperCamelCase(&quot;name&quot;)                #=&gt; &quot;Name&quot;      *   inflector.lowerCamelCase(&quot;the-first_name&quot;,'-')  #=&gt; &quot;TheFirstName&quot;      * </pre>      *       * </p>      *       * @param lowerCaseAndUnderscoredWord the word that is to be converted to camel case      * @param delimiterChars optional characters that are used to delimit word boundaries      * @return the upper camel case version of the word      * @see #underscore(String, char[])      * @see #camelCase(String, boolean, char[])      * @see #lowerCamelCase(String, char[])      */     public String upperCamelCase( String lowerCaseAndUnderscoredWord,                                   char... delimiterChars ) {         return camelCase(lowerCaseAndUnderscoredWord, true, delimiterChars);     }     /**      * By default, this method converts strings to UpperCamelCase. If the <code>uppercaseFirstLetter</code> argument to false,      * then this method produces lowerCamelCase. This method will also use any extra delimiter characters to identify word      * boundaries.      * <p>      * Examples:      *       * <pre>      *   inflector.camelCase(&quot;active_record&quot;,false)    #=&gt; &quot;activeRecord&quot;      *   inflector.camelCase(&quot;active_record&quot;,true)     #=&gt; &quot;ActiveRecord&quot;      *   inflector.camelCase(&quot;first_name&quot;,false)       #=&gt; &quot;firstName&quot;      *   inflector.camelCase(&quot;first_name&quot;,true)        #=&gt; &quot;FirstName&quot;      *   inflector.camelCase(&quot;name&quot;,false)             #=&gt; &quot;name&quot;      *   inflector.camelCase(&quot;name&quot;,true)              #=&gt; &quot;Name&quot;      * </pre>      *       * </p>      *       * @param lowerCaseAndUnderscoredWord the word that is to be converted to camel case      * @param uppercaseFirstLetter true if the first character is to be uppercased, or false if the first character is to be      *        lowercased      * @param delimiterChars optional characters that are used to delimit word boundaries      * @return the camel case version of the word      * @see #underscore(String, char[])      * @see #upperCamelCase(String, char[])      * @see #lowerCamelCase(String, char[])      */     public String camelCase( String lowerCaseAndUnderscoredWord,                              boolean uppercaseFirstLetter,                              char... delimiterChars ) {         if (lowerCaseAndUnderscoredWord == null) return null;         lowerCaseAndUnderscoredWord = lowerCaseAndUnderscoredWord.trim();         if (lowerCaseAndUnderscoredWord.length() == 0) return "";         if (uppercaseFirstLetter) {             String result = lowerCaseAndUnderscoredWord;             // Replace any extra delimiters with underscores (before the underscores are converted in the next step)...             if (delimiterChars != null) {                 for (char delimiterChar : delimiterChars) {                     result = result.replace(delimiterChar, '_');                 }             }             // Change the case at the beginning at after each underscore ...             return replaceAllWithUppercase(result, "(^|_)(.)", 2);         }         if (lowerCaseAndUnderscoredWord.length() < 2) return lowerCaseAndUnderscoredWord;         return "" + Character.toLowerCase(lowerCaseAndUnderscoredWord.charAt(0))                + camelCase(lowerCaseAndUnderscoredWord, true, delimiterChars).substring(1);     }     /**      * Makes an underscored form from the expression in the string (the reverse of the {@link #camelCase(String, boolean, char[])      * camelCase} method. Also changes any characters that match the supplied delimiters into underscore.      * <p>      * Examples:      *       * <pre>      *   inflector.underscore(&quot;activeRecord&quot;)     #=&gt; &quot;active_record&quot;      *   inflector.underscore(&quot;ActiveRecord&quot;)     #=&gt; &quot;active_record&quot;      *   inflector.underscore(&quot;firstName&quot;)        #=&gt; &quot;first_name&quot;      *   inflector.underscore(&quot;FirstName&quot;)        #=&gt; &quot;first_name&quot;      *   inflector.underscore(&quot;name&quot;)             #=&gt; &quot;name&quot;      *   inflector.underscore(&quot;The.firstName&quot;)    #=&gt; &quot;the_first_name&quot;      * </pre>      *       * </p>      *       * @param camelCaseWord the camel-cased word that is to be converted;      * @param delimiterChars optional characters that are used to delimit word boundaries (beyond capitalization)      * @return a lower-cased version of the input, with separate words delimited by the underscore character.      */     public String underscore( String camelCaseWord,                               char... delimiterChars ) {         if (camelCaseWord == null) return null;         String result = camelCaseWord.trim();         if (result.length() == 0) return "";         result = result.replaceAll("([A-Z]+)([A-Z][a-z])", "$1_$2");         result = result.replaceAll("([a-z\\d])([A-Z])", "$1_$2");         result = result.replace('-', '_');         if (delimiterChars != null) {             for (char delimiterChar : delimiterChars) {                 result = result.replace(delimiterChar, '_');             }         }         return result.toLowerCase();     }     /**      * Returns a copy of the input with the first character converted to uppercase and the remainder to lowercase.      *       * @param words the word to be capitalized      * @return the string with the first character capitalized and the remaining characters lowercased      */     public String capitalize( String words ) {         if (words == null) return null;         String result = words.trim();         if (result.length() == 0) return "";         if (result.length() == 1) return result.toUpperCase();         return "" + Character.toUpperCase(result.charAt(0)) + result.substring(1).toLowerCase();     }     /**      * Capitalizes the first word and turns underscores into spaces and strips trailing "_id" and any supplied removable tokens.      * Like {@link #titleCase(String, String[])}, this is meant for creating pretty output.      * <p>      * Examples:      *       * <pre>      *   inflector.humanize(&quot;employee_salary&quot;)       #=&gt; &quot;Employee salary&quot;      *   inflector.humanize(&quot;author_id&quot;)             #=&gt; &quot;Author&quot;      * </pre>      *       * </p>      *       * @param lowerCaseAndUnderscoredWords the input to be humanized      * @param removableTokens optional array of tokens that are to be removed      * @return the humanized string      * @see #titleCase(String, String[])      */     public String humanize( String lowerCaseAndUnderscoredWords,                             String... removableTokens ) {         if (lowerCaseAndUnderscoredWords == null) return null;         String result = lowerCaseAndUnderscoredWords.trim();         if (result.length() == 0) return "";         // Remove a trailing "_id" token         result = result.replaceAll("_id$", "");         // Remove all of the tokens that should be removed         if (removableTokens != null) {             for (String removableToken : removableTokens) {                 result = result.replaceAll(removableToken, "");             }         }         result = result.replaceAll("_+", " "); // replace all adjacent underscores with a single space         return capitalize(result);     }     /**      * Capitalizes all the words and replaces some characters in the string to create a nicer looking title. Underscores are      * changed to spaces, a trailing "_id" is removed, and any of the supplied tokens are removed. Like      * {@link #humanize(String, String[])}, this is meant for creating pretty output.      * <p>      * Examples:      *       * <pre>      *   inflector.titleCase(&quot;man from the boondocks&quot;)       #=&gt; &quot;Man From The Boondocks&quot;      *   inflector.titleCase(&quot;x-men: the last stand&quot;)        #=&gt; &quot;X Men: The Last Stand&quot;      * </pre>      *       * </p>      *       * @param words the input to be turned into title case      * @param removableTokens optional array of tokens that are to be removed      * @return the title-case version of the supplied words      */     public String titleCase( String words,                              String... removableTokens ) {         String result = humanize(words, removableTokens);         result = replaceAllWithUppercase(result, "\\b([a-z])", 1); // change first char of each word to uppercase         return result;     }     /**      * Turns a non-negative number into an ordinal string used to denote the position in an ordered sequence, such as 1st, 2nd,      * 3rd, 4th.      *       * @param number the non-negative number      * @return the string with the number and ordinal suffix      */     public String ordinalize( int number ) {         int remainder = number % 100;         String numberStr = Integer.toString(number);         if (11 <= number && number <= 13) return numberStr + "th";         remainder = number % 10;         if (remainder == 1) return numberStr + "st";         if (remainder == 2) return numberStr + "nd";         if (remainder == 3) return numberStr + "rd";         return numberStr + "th";     }     // ------------------------------------------------------------------------------------------------     // Management methods     // ------------------------------------------------------------------------------------------------     /**      * Determine whether the supplied word is considered uncountable by the {@link #pluralize(Object) pluralize} and      * {@link #singularize(Object) singularize} methods.      *       * @param word the word      * @return true if the plural and singular forms of the word are the same      */     public boolean isUncountable( String word ) {         if (word == null) return false;         String trimmedLower = word.trim().toLowerCase();         return this.uncountables.contains(trimmedLower);     }     /**      * Get the set of words that are not processed by the Inflector. The resulting map is directly modifiable.      *       * @return the set of uncountable words      */     public Set<String> getUncountables() {         return uncountables;     }     public void addPluralize( String rule,                               String replacement ) {         final Rule pluralizeRule = new Rule(rule, replacement);         this.plurals.addFirst(pluralizeRule);     }     public void addSingularize( String rule,                                 String replacement ) {         final Rule singularizeRule = new Rule(rule, replacement);         this.singulars.addFirst(singularizeRule);     }     public void addIrregular( String singular,                               String plural ) {         //CheckArg.isNotEmpty(singular, "singular rule");         //CheckArg.isNotEmpty(plural, "plural rule");         String singularRemainder = singular.length() > 1 ? singular.substring(1) : "";         String pluralRemainder = plural.length() > 1 ? plural.substring(1) : "";         addPluralize("(" + singular.charAt(0) + ")" + singularRemainder + "$", "$1" + pluralRemainder);         addSingularize("(" + plural.charAt(0) + ")" + pluralRemainder + "$", "$1" + singularRemainder);     }     public void addUncountable( String... words ) {         if (words == null || words.length == 0) return;         for (String word : words) {             if (word != null) uncountables.add(word.trim().toLowerCase());         }     }     /**      * Utility method to replace all occurrences given by the specific backreference with its uppercased form, and remove all      * other backreferences.      * <p>      * The Java {@link Pattern regular expression processing} does not use the preprocessing directives <code>\l</code>,      * <code>&#92;u</code>, <code>\L</code>, and <code>\U</code>. If so, such directives could be used in the replacement string      * to uppercase or lowercase the backreferences. For example, <code>\L1</code> would lowercase the first backreference, and      * <code>&#92;u3</code> would uppercase the 3rd backreference.      * </p>      *       * @param input      * @param regex      * @param groupNumberToUppercase      * @return the input string with the appropriate characters converted to upper-case      */     protected static String replaceAllWithUppercase( String input,                                                      String regex,                                                      int groupNumberToUppercase ) {         Pattern underscoreAndDotPattern = Pattern.compile(regex);         Matcher matcher = underscoreAndDotPattern.matcher(input);         StringBuffer sb = new StringBuffer();         while (matcher.find()) {             matcher.appendReplacement(sb, matcher.group(groupNumberToUppercase).toUpperCase());         }         matcher.appendTail(sb);         return sb.toString();     }     /**      * Completely remove all rules within this inflector.      */     public void clear() {         this.uncountables.clear();         this.plurals.clear();         this.singulars.clear();     }     protected void initialize() {         Inflector inflect = this;         inflect.addPluralize("$", "s");         inflect.addPluralize("s$", "s");         inflect.addPluralize("(ax|test)is$", "$1es");         inflect.addPluralize("(octop|vir)us$", "$1i");         inflect.addPluralize("(octop|vir)i$", "$1i"); // already plural         inflect.addPluralize("(alias|status)$", "$1es");         inflect.addPluralize("(bu)s$", "$1ses");         inflect.addPluralize("(buffal|tomat)o$", "$1oes");         inflect.addPluralize("([ti])um$", "$1a");         inflect.addPluralize("([ti])a$", "$1a"); // already plural         inflect.addPluralize("sis$", "ses");         inflect.addPluralize("(?:([^f])fe|([lr])f)$", "$1$2ves");         inflect.addPluralize("(hive)$", "$1s");         inflect.addPluralize("([^aeiouy]|qu)y$", "$1ies");         inflect.addPluralize("(x|ch|ss|sh)$", "$1es");         inflect.addPluralize("(matr|vert|ind)ix|ex$", "$1ices");         inflect.addPluralize("([m|l])ouse$", "$1ice");         inflect.addPluralize("([m|l])ice$", "$1ice");         inflect.addPluralize("^(ox)$", "$1en");         inflect.addPluralize("(quiz)$", "$1zes");         // Need to check for the following words that are already pluralized:         inflect.addPluralize("(people|men|children|sexes|moves|stadiums)$", "$1"); // irregulars         inflect.addPluralize("(oxen|octopi|viri|aliases|quizzes)$", "$1"); // special rules         inflect.addSingularize("s$", "");         inflect.addSingularize("(s|si|u)s$", "$1s"); // '-us' and '-ss' are already singular         inflect.addSingularize("(n)ews$", "$1ews");         inflect.addSingularize("([ti])a$", "$1um");         inflect.addSingularize("((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$", "$1$2sis");         inflect.addSingularize("(^analy)ses$", "$1sis");         inflect.addSingularize("(^analy)sis$", "$1sis"); // already singular, but ends in 's'         inflect.addSingularize("([^f])ves$", "$1fe");         inflect.addSingularize("(hive)s$", "$1");         inflect.addSingularize("(tive)s$", "$1");         inflect.addSingularize("([lr])ves$", "$1f");         inflect.addSingularize("([^aeiouy]|qu)ies$", "$1y");         inflect.addSingularize("(s)eries$", "$1eries");         inflect.addSingularize("(m)ovies$", "$1ovie");         inflect.addSingularize("(x|ch|ss|sh)es$", "$1");         inflect.addSingularize("([m|l])ice$", "$1ouse");         inflect.addSingularize("(bus)es$", "$1");         inflect.addSingularize("(o)es$", "$1");         inflect.addSingularize("(shoe)s$", "$1");         inflect.addSingularize("(cris|ax|test)is$", "$1is"); // already singular, but ends in 's'         inflect.addSingularize("(cris|ax|test)es$", "$1is");         inflect.addSingularize("(octop|vir)i$", "$1us");         inflect.addSingularize("(octop|vir)us$", "$1us"); // already singular, but ends in 's'         inflect.addSingularize("(alias|status)es$", "$1");         inflect.addSingularize("(alias|status)$", "$1"); // already singular, but ends in 's'         inflect.addSingularize("^(ox)en", "$1");         inflect.addSingularize("(vert|ind)ices$", "$1ex");         inflect.addSingularize("(matr)ices$", "$1ix");         inflect.addSingularize("(quiz)zes$", "$1");         inflect.addIrregular("person", "people");         inflect.addIrregular("man", "men");         inflect.addIrregular("child", "children");         inflect.addIrregular("sex", "sexes");         inflect.addIrregular("move", "moves");         inflect.addIrregular("stadium", "stadiums");         inflect.addUncountable("equipment", "information", "rice", "money", "species", "series", "fish", "sheep");     } }