/* ******************************************************************************* * Copyright (C) 2007-2010, International Business Machines Corporation and * * others. All Rights Reserved. * ******************************************************************************* */ package com.ibm.icu.text; import java.text.FieldPosition; import java.text.ParsePosition; import java.util.HashMap; import java.util.Locale; import java.util.Map; import java.util.Set; import com.ibm.icu.impl.UCharacterProperty; import com.ibm.icu.util.ULocale; /** *

* PluralFormat supports the creation of internationalized * messages with plural inflection. It is based on plural * selection, i.e. the caller specifies messages for each * plural case that can appear in the users language and the * PluralFormat selects the appropriate message based on * the number. *

*

The Problem of Plural Forms in Internationalized Messages

*

* Different languages have different ways to inflect * plurals. Creating internationalized messages that include plural * forms is only feasible when the framework is able to handle plural * forms of all languages correctly. ChoiceFormat * doesn't handle this well, because it attaches a number interval to * each message and selects the message whose interval contains a * given number. This can only handle a finite number of * intervals. But in some languages, like Polish, one plural case * applies to infinitely many intervals (e.g., paucal applies to * numbers ending with 2, 3, or 4 except those ending with 12, 13, or * 14). Thus ChoiceFormat is not adequate. *

* PluralFormat deals with this by breaking the problem * into two parts: *

*

*

Usage of PluralFormat

*

* This discussion assumes that you use PluralFormat with * a predefined set of plural rules. You can create one using one of * the constructors that takes a ULocale object. To * specify the message pattern, you can either pass it to the * constructor or set it explicitly using the * applyPattern() method. The format() * method takes a number object and selects the message of the * matching plural case. This message will be returned. *

*
Patterns and Their Interpretation
*

* The pattern text defines the message output for each plural case of the * used locale. The pattern is a sequence of * caseKeyword{message} clauses, separated by white * space characters. Each clause assigns the message message * to the plural case identified by caseKeyword. *

* You always have to define a message text for the default plural case * "other" which is contained in every rule set. If the plural * rules of the PluralFormat object do not contain a plural case * identified by caseKeyword, an * IllegalArgumentException is thrown. * If you do not specify a message text for a particular plural case, the * message text of the plural case "other" gets assigned to this * plural case. If you specify more than one message for the same plural case, * an IllegalArgumentException is thrown. *
* Spaces between caseKeyword and * message will be ignored; spaces within * message will be preserved. *

* The message text for a particular plural case may contain other message * format patterns. PluralFormat preserves these so that you * can use the strings produced by PluralFormat with other * formatters. If you are using PluralFormat inside a * MessageFormat pattern, MessageFormat will * automatically evaluate the resulting format pattern.
* Thus, curly braces ({, }) are only allowed * in message texts to define a nested format pattern.
* The pound sign (#) will be interpreted as the number placeholder * in the message text, if it is not contained in curly braces (to preserve * NumberFormat patterns). PluralFormat will * replace each of those pound signs by the number passed to the * format() method. It will be formatted using a * NumberFormat for the PluralFormat's locale. If you * need special number formatting, you have to explicitly specify a * NumberFormat for the PluralFormat to use. *

* Example *
 * MessageFormat msgFmt = new MessageFormat("{0, plural, " +
 *     "one{{0, number, C''''est #,##0.0#  fichier}} " +
 *     "other {Ce sont # fichiers}} dans la liste.",
 *     new ULocale("fr"));
 * Object args[] = {new Long(0)};
 * System.out.println(msgFmt.format(args));
 * args = {new Long(3)};
 * System.out.println(msgFmt.format(args));
 * 
* Produces the output:
* C'est 0,0 fichier dans la liste.
* Ce sont 3 fichiers dans la liste." *

* Note:
* Currently PluralFormat * does not make use of quotes like MessageFormat. * If you use plural format strings with MessageFormat and want * to use a quote sign "'", you have to write "''". * MessageFormat unquotes this pattern and passes the unquoted * pattern to PluralFormat. It's a bit trickier if you use * nested formats that do quoting. In the example above, we wanted to insert * "'" in the number format pattern. Since * NumberFormat supports quotes, we had to insert * "''". But since MessageFormat unquotes the * pattern before it gets passed to PluralFormat, we have to * double these quotes, i.e. write "''''". *

*

Defining Custom Plural Rules

*

If you need to use PluralFormat with custom rules, you can * create a PluralRules object and pass it to * PluralFormat's constructor. If you also specify a locale in this * constructor, this locale will be used to format the number in the message * texts. *

* For more information about PluralRules, see * {@link PluralRules}. *

* * @author tschumann (Tim Schumann) * @stable ICU 3.8 */ public class PluralFormat extends UFormat { private static final long serialVersionUID = 1L; /* * The locale used for standard number formatting and getting the predefined * plural rules (if they were not defined explicitely). */ private ULocale ulocale = null; /* * The plural rules used for plural selection. */ private PluralRules pluralRules = null; /* * The applied pattern string. */ private String pattern = null; /* * The format messages for each plural case. It is a mapping: * String(plural case keyword) --> String * (message for this plural case). */ private Map parsedValues = null; /* * This NumberFormat is used for the standard formatting of * the number inserted into the message. */ private NumberFormat numberFormat = null; /** * Creates a new PluralFormat for the default locale. * This locale will be used to get the set of plural rules and for standard * number formatting. * @stable ICU 3.8 */ public PluralFormat() { init(null, ULocale.getDefault()); } /** * Creates a new PluralFormat for a given locale. * @param ulocale the PluralFormat will be configured with * rules for this locale. This locale will also be used for standard * number formatting. * @stable ICU 3.8 */ public PluralFormat(ULocale ulocale) { init(null, ulocale); } /** * Creates a new PluralFormat for a given set of rules. * The standard number formatting will be done using the default locale. * @param rules defines the behavior of the PluralFormat * object. * @stable ICU 3.8 */ public PluralFormat(PluralRules rules) { init(rules, ULocale.getDefault()); } /** * Creates a new PluralFormat for a given set of rules. * The standard number formatting will be done using the given locale. * @param ulocale the default number formatting will be done using this * locale. * @param rules defines the behavior of the PluralFormat * object. * @stable ICU 3.8 */ public PluralFormat(ULocale ulocale, PluralRules rules) { init(rules, ulocale); } /** * Creates a new PluralFormat for a given pattern string. * The default locale will be used to get the set of plural rules and for * standard number formatting. * @param pattern the pattern for this PluralFormat. * @throws IllegalArgumentException if the pattern is invalid. * @stable ICU 3.8 */ public PluralFormat(String pattern) { init(null, ULocale.getDefault()); applyPattern(pattern); } /** * Creates a new PluralFormat for a given pattern string and * locale. * The locale will be used to get the set of plural rules and for * standard number formatting. * @param ulocale the PluralFormat will be configured with * rules for this locale. This locale will also be used for standard * number formatting. * @param pattern the pattern for this PluralFormat. * @throws IllegalArgumentException if the pattern is invalid. * @stable ICU 3.8 */ public PluralFormat(ULocale ulocale, String pattern) { init(null, ulocale); applyPattern(pattern); } /** * Creates a new PluralFormat for a given set of rules and a * pattern. * The standard number formatting will be done using the default locale. * @param rules defines the behavior of the PluralFormat * object. * @param pattern the pattern for this PluralFormat. * @throws IllegalArgumentException if the pattern is invalid. * @stable ICU 3.8 */ public PluralFormat(PluralRules rules, String pattern) { init(rules, ULocale.getDefault()); applyPattern(pattern); } /** * Creates a new PluralFormat for a given set of rules, a * pattern and a locale. * @param ulocale the PluralFormat will be configured with * rules for this locale. This locale will also be used for standard * number formatting. * @param rules defines the behavior of the PluralFormat * object. * @param pattern the pattern for this PluralFormat. * @throws IllegalArgumentException if the pattern is invalid. * @stable ICU 3.8 */ public PluralFormat(ULocale ulocale, PluralRules rules, String pattern) { init(rules, ulocale); applyPattern(pattern); } /* * Initializes the PluralRules object. * Postcondition:
* ulocale : is locale
* pluralRules: if rules != null * it's set to rules, otherwise it is the * predefined plural rule set for the locale * ulocale.
* parsedValues: is null
* pattern: is null
* numberFormat: a NumberFormat for the locale * ulocale. */ private void init(PluralRules rules, ULocale locale) { ulocale = locale; pluralRules = (rules == null) ? PluralRules.forLocale(ulocale) : rules; parsedValues = null; pattern = null; numberFormat = NumberFormat.getInstance(ulocale); } /** * Sets the pattern used by this plural format. * The method parses the pattern and creates a map of format strings * for the plural rules. * Patterns and their interpretation are specified in the class description. * * @param pttrn the pattern for this plural format. * @throws IllegalArgumentException if the pattern is invalid. * @stable ICU 3.8 */ public void applyPattern(String pttrn) { pttrn = pttrn.trim(); this.pattern = pttrn; int braceStack = 0; Set ruleNames = pluralRules.getKeywords(); parsedValues = new HashMap(); // Format string has to include keywords. // states: // 0: Reading keyword. // 1: Reading value for preceding keyword. int state = 0; StringBuilder token = new StringBuilder(); String currentKeyword = null; boolean readSpaceAfterKeyword = false; for (int i = 0; i < pttrn.length(); ++i) { char ch = pttrn.charAt(i); switch (state) { case 0: // Reading value. if (token.length() == 0) { readSpaceAfterKeyword = false; } if (UCharacterProperty.isRuleWhiteSpace(ch)) { if (token.length() > 0) { readSpaceAfterKeyword = true; } // Skip leading and trailing whitespaces. break; } if (ch == '{') { // End of keyword definition reached. currentKeyword = token.toString().toLowerCase( Locale.ENGLISH); if (!ruleNames.contains(currentKeyword)) { parsingFailure("Malformed formatting expression. " + "Unknown keyword \"" + currentKeyword + "\" at position " + i + "."); } if (parsedValues.get(currentKeyword) != null) { parsingFailure("Malformed formatting expression. " + "Text for case \"" + currentKeyword + "\" at position " + i + " already defined!"); } token.delete(0, token.length()); braceStack++; state = 1; break; } if (readSpaceAfterKeyword) { parsingFailure("Malformed formatting expression. " + "Invalid keyword definition. Character \"" + ch + "\" at position " + i + " not expected!"); } token.append(ch); break; case 1: // Reading value. switch (ch) { case '{': braceStack++; token.append(ch); break; case '}': braceStack--; if (braceStack == 0) { // End of value reached. parsedValues.put(currentKeyword, token.toString()); token.delete(0, token.length()); state = 0; } else if (braceStack < 0) { parsingFailure("Malformed formatting expression. " + "Braces do not match."); } else { // braceStack > 0 token.append(ch); } break; default: token.append(ch); } break; } // switch state } // for loop. if (braceStack != 0) { parsingFailure( "Malformed formatting expression. Braces do not match."); } checkSufficientDefinition(); } /** * Returns the pattern for this PluralFormat. * * @return the pattern string * @stable ICU 4.2 */ public String toPattern() { return pattern; } /** * Formats a plural message for a given number. * * @param number a number for which the plural message should be formatted. * If no pattern has been applied to this * PluralFormat object yet, the formatted number will * be returned. * @return the string containing the formatted plural message. * @stable ICU 4.0 */ public final String format(double number) { // If no pattern was applied, return the formatted number. if (parsedValues == null) { return numberFormat.format(number); } // Get appropriate format pattern. String selectedRule = pluralRules.select(number); String selectedPattern = parsedValues.get(selectedRule); if (selectedPattern == null) { // Fallback to others. selectedPattern = parsedValues.get(PluralRules.KEYWORD_OTHER); } // Get formatted number and insert it into String. // Will replace all '#' which are not inside curly braces by the // formatted number. return insertFormattedNumber(number, selectedPattern); } /** * Formats a plural message for a given number and appends the formatted * message to the given StringBuffer. * @param number a number object (instance of Number for which * the plural message should be formatted. If no pattern has been * applied to this PluralFormat object yet, the * formatted number will be returned. * Note: If this object is not an instance of Number, * the toAppendTo will not be modified. * @param toAppendTo the formatted message will be appended to this * StringBuffer. * @param pos will be ignored by this method. * @return the string buffer passed in as toAppendTo, with formatted text * appended. * @throws IllegalArgumentException if number is not an instance of Number * @stable ICU 3.8 */ public StringBuffer format(Object number, StringBuffer toAppendTo, FieldPosition pos) { if (number instanceof Number) { toAppendTo.append(format(((Number) number).doubleValue())); return toAppendTo; } throw new IllegalArgumentException("'" + number + "' is not a Number"); } /** * This method is not yet supported by PluralFormat. * @param text the string to be parsed. * @param parsePosition defines the position where parsing is to begin, * and upon return, the position where parsing left off. If the position * has not changed upon return, then parsing failed. * @return nothing because this method is not yet implemented. * @throws UnsupportedOperationException will always be thrown by this method. * @stable ICU 3.8 */ public Number parse(String text, ParsePosition parsePosition) { throw new UnsupportedOperationException(); } /** * This method is not yet supported by PluralFormat. * @param source the string to be parsed. * @param pos defines the position where parsing is to begin, * and upon return, the position where parsing left off. If the position * has not changed upon return, then parsing failed. * @return nothing because this method is not yet implemented. * @throws UnsupportedOperationException will always be thrown by this method. * @stable ICU 3.8 */ public Object parseObject(String source, ParsePosition pos) { throw new UnsupportedOperationException(); } /** * Sets the locale used by this PluraFormat object. * Note: Calling this method resets this PluraFormat object, * i.e., a pattern that was applied previously will be removed, * and the NumberFormat is set to the default number format for * the locale. The resulting format behaves the same as one * constructed from {@link #PluralFormat(ULocale)}. * @param ulocale the ULocale used to configure the * formatter. If ulocale is null, the * default locale will be used. * @stable ICU 3.8 */ public void setLocale(ULocale ulocale) { if (ulocale == null) { ulocale = ULocale.getDefault(); } init(null, ulocale); } /** * Sets the number format used by this formatter. You only need to * call this if you want a different number format than the default * formatter for the locale. * @param format the number format to use. * @stable ICU 3.8 */ public void setNumberFormat(NumberFormat format) { numberFormat = format; } /* * Checks if the applied pattern provided enough information, * i.e., if the attribute parsedValues stores enough * information for plural formatting. * Will be called at the end of pattern parsing. * @throws IllegalArgumentException if there's not sufficient information * provided. */ private void checkSufficientDefinition() { // Check that at least the default rule is defined. if (parsedValues.get(PluralRules.KEYWORD_OTHER) == null) { parsingFailure("Malformed formatting expression.\n" + "Value for case \"" + PluralRules.KEYWORD_OTHER + "\" was not defined."); } } /* * Helper method that resets the PluralFormat object and throws * an IllegalArgumentException with a given error text. * @param errorText the error text of the exception message. * @throws IllegalArgumentException will always be thrown by this method. */ private void parsingFailure(String errorText) { // Set PluralFormat to a valid state. init(null, ULocale.getDefault()); throw new IllegalArgumentException(errorText); } /* * Helper method that is called during formatting. * It replaces the character '#' by the number used for plural selection in * a message text. Only '#' are replaced, that are not written inside curly * braces. This allows the use of nested number formats. * The number will be formatted using the attribute * numberformat. * @param number the number used for plural selection. * @param message is the text in which '#' will be replaced. * @return the text with inserted numbers. */ private String insertFormattedNumber(double number, String message) { if (message == null) { return ""; } String formattedNumber = numberFormat.format(number); StringBuilder result = new StringBuilder(); int braceStack = 0; int startIndex = 0; for (int i = 0; i < message.length(); ++i) { switch (message.charAt(i)) { case '{': ++braceStack; break; case '}': --braceStack; break; case '#': if (braceStack == 0) { result.append(message.substring(startIndex,i)); startIndex = i + 1; result.append(formattedNumber); } break; } } if (startIndex < message.length()) { result.append(message.substring(startIndex, message.length())); } return result.toString(); } /** * {@inheritDoc} * @stable ICU 3.8 */ public boolean equals(Object rhs) { return rhs instanceof PluralFormat && equals((PluralFormat) rhs); } /** * Returns true if this equals the provided PluralFormat. * @param rhs the PluralFormat to compare against * @return true if this equals rhs * @stable ICU 3.8 */ public boolean equals(PluralFormat rhs) { return pluralRules.equals(rhs.pluralRules) && parsedValues.equals(rhs.parsedValues) && numberFormat.equals(rhs.numberFormat); } /** * {@inheritDoc} * @stable ICU 3.8 */ public int hashCode() { return pluralRules.hashCode() ^ parsedValues.hashCode(); } /** * For debugging purposes only * @return a text representation of the format data. * @stable ICU 3.8 */ public String toString() { StringBuilder buf = new StringBuilder(); buf.append("locale=" + ulocale); buf.append(", rules='" + pluralRules + "'"); buf.append(", pattern='" + pattern + "'"); buf.append(", parsedValues='" + parsedValues + "'"); buf.append(", format='" + numberFormat + "'"); return buf.toString(); } }