All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.ibm.icu.text.PluralFormat Maven / Gradle / Ivy

Go to download

International Component for Unicode for Java (ICU4J) is a mature, widely used Java library providing Unicode and Globalization support

There is a newer version: 76.1
Show newest version
/*
 *******************************************************************************
 * Copyright (C) 2007-2012, International Business Machines Corporation and
 * others. All Rights Reserved.
 *******************************************************************************
 */

package com.ibm.icu.text;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.text.FieldPosition;
import java.text.ParsePosition;
import java.util.Map;

import com.ibm.icu.impl.Utility;
import com.ibm.icu.text.PluralRules.PluralType;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.ULocale.Category;

/**
 * 

* 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 user's 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., the paucal case 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: *

    *
  • It uses PluralRules that can define more complex * conditions for a plural case than just a single interval. These plural * rules define both what plural cases exist in a language, and to * which numbers these cases apply. *
  • It provides predefined plural rules for many languages. Thus, the programmer * need not worry about the plural cases of a language and * does not have to define the plural cases; they can simply * use the predefined keywords. The whole plural formatting of messages can * be done using localized patterns from resource bundles. For predefined plural * rules, see the CLDR Language Plural Rules page at * http://unicode.org/repos/cldr-tmp/trunk/diff/supplemental/language_plural_rules.html *
*

*

Usage of PluralFormat

*

Note: Typically, plural formatting is done via MessageFormat * with a plural argument type, * rather than using a stand-alone 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 * specified locale. Syntax: *

 * pluralStyle = [offsetValue] (selector '{' message '}')+
 * offsetValue = "offset:" number
 * selector = explicitValue | keyword
 * explicitValue = '=' number  // adjacent, no white space in between
 * keyword = [^[[:Pattern_Syntax:][:Pattern_White_Space:]]]+
 * message: see {@link MessageFormat}
 * 
* Pattern_White_Space between syntax elements is ignored, except * between the {curly braces} and their sub-message, * and between the '=' and the number of an explicitValue. * *

* There are 6 predefined case keywords in CLDR/ICU - 'zero', 'one', 'two', 'few', 'many' and * 'other'. You always have to define a message text for the default plural case * "other" which is contained in every rule set. * 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. *

* When formatting, the input number is first matched against the explicitValue clauses. * If there is no exact-number match, then a keyword is selected by calling * the PluralRules with the input number minus the offset. * (The offset defaults to 0 if it is omitted from the pattern string.) * If there is no clause with that keyword, then the "other" clauses is returned. *

* An unquoted pound sign (#) in the selected sub-message * itself (i.e., outside of arguments nested in the sub-message) * is replaced by the input number minus the offset. * The number-minus-offset value is formatted using a * NumberFormat for the PluralFormat's locale. If you * need special number formatting, you have to use a MessageFormat * and explicitly specify a NumberFormat argument. * Note: That argument is formatting without subtracting the offset! * If you need a custom format and have a non-zero offset, then you need to pass the * number-minus-offset value as a separate parameter. *

* For a usage example, see the {@link MessageFormat} class documentation. * *

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). * @serial */ private ULocale ulocale = null; /** * The plural rules used for plural selection. * @serial */ private PluralRules pluralRules = null; /** * The applied pattern string. * @serial */ private String pattern = null; /** * The MessagePattern which contains the parsed structure of the pattern string. */ transient private MessagePattern msgPattern; /** * Obsolete with use of MessagePattern since ICU 4.8. Used to be: * The format messages for each plural case. It is a mapping: * String(plural case keyword) --> String * (message for this plural case). * @serial */ private Map parsedValues = null; /** * This NumberFormat is used for the standard formatting of * the number inserted into the message. * @serial */ private NumberFormat numberFormat = null; /** * The offset to subtract before invoking plural rules. */ transient private double offset = 0; /** * Creates a new cardinal-number PluralFormat for the default FORMAT locale. * This locale will be used to get the set of plural rules and for standard * number formatting. * @see Category#FORMAT * @stable ICU 3.8 */ public PluralFormat() { init(null, PluralType.CARDINAL, ULocale.getDefault(Category.FORMAT)); } /** * Creates a new cardinal-number 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, PluralType.CARDINAL, ulocale); } /** * Creates a new cardinal-number PluralFormat for a given set of rules. * The standard number formatting will be done using the default FORMAT locale. * @param rules defines the behavior of the PluralFormat * object. * @see Category#FORMAT * @stable ICU 3.8 */ public PluralFormat(PluralRules rules) { init(rules, PluralType.CARDINAL, ULocale.getDefault(Category.FORMAT)); } /** * Creates a new cardinal-number 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, PluralType.CARDINAL, ulocale); } /** * Creates a new PluralFormat for the plural type. * The standard number formatting will be done using the given locale. * @param ulocale the default number formatting will be done using this * locale. * @param type The plural type (e.g., cardinal or ordinal). * @draft ICU 50 * @provisional This API might change or be removed in a future release. */ public PluralFormat(ULocale ulocale, PluralType type) { init(null, type, ulocale); } /** * Creates a new cardinal-number PluralFormat for a given pattern string. * The default FORMAT 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. * @see Category#FORMAT * @stable ICU 3.8 */ public PluralFormat(String pattern) { init(null, PluralType.CARDINAL, ULocale.getDefault(Category.FORMAT)); applyPattern(pattern); } /** * Creates a new cardinal-number 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, PluralType.CARDINAL, ulocale); applyPattern(pattern); } /** * Creates a new cardinal-number PluralFormat for a given set of rules and a * pattern. * The standard number formatting will be done using the default FORMAT locale. * @param rules defines the behavior of the PluralFormat * object. * @param pattern the pattern for this PluralFormat. * @throws IllegalArgumentException if the pattern is invalid. * @see Category#FORMAT * @stable ICU 3.8 */ public PluralFormat(PluralRules rules, String pattern) { init(rules, PluralType.CARDINAL, ULocale.getDefault(Category.FORMAT)); applyPattern(pattern); } /** * Creates a new cardinal-number 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, PluralType.CARDINAL, ulocale); applyPattern(pattern); } /** * Creates a new PluralFormat for a plural type, 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 type The plural type (e.g., cardinal or ordinal). * @param pattern the pattern for this PluralFormat. * @throws IllegalArgumentException if the pattern is invalid. * @draft ICU 50 * @provisional This API might change or be removed in a future release. */ public PluralFormat(ULocale ulocale, PluralType type, String pattern) { init(null, type, 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, PluralType type, ULocale locale) { ulocale = locale; pluralRules = (rules == null) ? PluralRules.forLocale(ulocale, type) : rules; resetPattern(); numberFormat = NumberFormat.getInstance(ulocale); } private void resetPattern() { pattern = null; if(msgPattern != null) { msgPattern.clear(); } offset = 0; } /** * 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 pattern the pattern for this plural format. * @throws IllegalArgumentException if the pattern is invalid. * @stable ICU 3.8 */ public void applyPattern(String pattern) { this.pattern = pattern; if (msgPattern == null) { msgPattern = new MessagePattern(); } try { msgPattern.parsePluralStyle(pattern); offset = msgPattern.getPluralOffset(0); } catch(RuntimeException e) { resetPattern(); throw e; } } /** * Returns the pattern for this PluralFormat. * * @return the pattern string * @stable ICU 4.2 */ public String toPattern() { return pattern; } /** * Finds the PluralFormat sub-message for the given number, or the "other" sub-message. * @param pattern A MessagePattern. * @param partIndex the index of the first PluralFormat argument style part. * @param selector the PluralSelector for mapping the number (minus offset) to a keyword. * @param number a number to be matched to one of the PluralFormat argument's explicit values, * or mapped via the PluralSelector. * @return the sub-message start part index. */ /*package*/ static int findSubMessage( MessagePattern pattern, int partIndex, PluralSelector selector, double number) { int count=pattern.countParts(); double offset; MessagePattern.Part part=pattern.getPart(partIndex); if(part.getType().hasNumericValue()) { offset=pattern.getNumericValue(part); ++partIndex; } else { offset=0; } // The keyword is null until we need to match against non-explicit, not-"other" value. // Then we get the keyword from the selector. // (In other words, we never call the selector if we match against an explicit value, // or if the only non-explicit keyword is "other".) String keyword=null; // When we find a match, we set msgStart>0 and also set this boolean to true // to avoid matching the keyword again (duplicates are allowed) // while we continue to look for an explicit-value match. boolean haveKeywordMatch=false; // msgStart is 0 until we find any appropriate sub-message. // We remember the first "other" sub-message if we have not seen any // appropriate sub-message before. // We remember the first matching-keyword sub-message if we have not seen // one of those before. // (The parser allows [does not check for] duplicate keywords. // We just have to make sure to take the first one.) // We avoid matching the keyword twice by also setting haveKeywordMatch=true // at the first keyword match. // We keep going until we find an explicit-value match or reach the end of the plural style. int msgStart=0; // Iterate over (ARG_SELECTOR [ARG_INT|ARG_DOUBLE] message) tuples // until ARG_LIMIT or end of plural-only pattern. do { part=pattern.getPart(partIndex++); MessagePattern.Part.Type type=part.getType(); if(type==MessagePattern.Part.Type.ARG_LIMIT) { break; } assert type==MessagePattern.Part.Type.ARG_SELECTOR; // part is an ARG_SELECTOR followed by an optional explicit value, and then a message if(pattern.getPartType(partIndex).hasNumericValue()) { // explicit value like "=2" part=pattern.getPart(partIndex++); if(number==pattern.getNumericValue(part)) { // matches explicit value return partIndex; } } else if(!haveKeywordMatch) { // plural keyword like "few" or "other" // Compare "other" first and call the selector if this is not "other". if(pattern.partSubstringMatches(part, "other")) { if(msgStart==0) { msgStart=partIndex; if(keyword!=null && keyword.equals("other")) { // This is the first "other" sub-message, // and the selected keyword is also "other". // Do not match "other" again. haveKeywordMatch=true; } } } else { if(keyword==null) { keyword=selector.select(number-offset); if(msgStart!=0 && keyword.equals("other")) { // We have already seen an "other" sub-message. // Do not match "other" again. haveKeywordMatch=true; // Skip keyword matching but do getLimitPartIndex(). } } if(!haveKeywordMatch && pattern.partSubstringMatches(part, keyword)) { // keyword matches msgStart=partIndex; // Do not match this keyword again. haveKeywordMatch=true; } } } partIndex=pattern.getLimitPartIndex(partIndex); } while(++partIndexPluralFormat
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 (msgPattern == null || msgPattern.countParts() == 0) { return numberFormat.format(number); } // Get the appropriate sub-message. int partIndex = findSubMessage(msgPattern, 0, pluralRulesWrapper, number); // Replace syntactic # signs in the top level of this sub-message // (not in nested arguments) with the formatted number-offset. number -= offset; StringBuilder result = null; int prevIndex = msgPattern.getPart(partIndex).getLimit(); for (;;) { MessagePattern.Part part = msgPattern.getPart(++partIndex); MessagePattern.Part.Type type = part.getType(); int index = part.getIndex(); if (type == MessagePattern.Part.Type.MSG_LIMIT) { if (result == null) { return pattern.substring(prevIndex, index); } else { return result.append(pattern, prevIndex, index).toString(); } } else if (type == MessagePattern.Part.Type.REPLACE_NUMBER || // JDK compatibility mode: Remove SKIP_SYNTAX. (type == MessagePattern.Part.Type.SKIP_SYNTAX && msgPattern.jdkAposMode())) { if (result == null) { result = new StringBuilder(); } result.append(pattern, prevIndex, index); if (type == MessagePattern.Part.Type.REPLACE_NUMBER) { result.append(numberFormat.format(number)); } prevIndex = part.getLimit(); } else if (type == MessagePattern.Part.Type.ARG_START) { if (result == null) { result = new StringBuilder(); } result.append(pattern, prevIndex, index); prevIndex = index; partIndex = msgPattern.getLimitPartIndex(partIndex); index = msgPattern.getPart(partIndex).getLimit(); MessagePattern.appendReducedApostrophes(pattern, prevIndex, index, result); prevIndex = index; } } } /** * 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, PluralRules.PluralType)} * with PluralType.CARDINAL. * @param ulocale the ULocale used to configure the * formatter. If ulocale is null, the * default FORMAT locale will be used. * @see Category#FORMAT * @deprecated ICU 50 This method clears the pattern and might create * a different kind of PluralRules instance; * use one of the constructors to create a new instance instead. */ public void setLocale(ULocale ulocale) { if (ulocale == null) { ulocale = ULocale.getDefault(Category.FORMAT); } init(null, PluralType.CARDINAL, 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; } /** * {@inheritDoc} * @stable ICU 3.8 */ @Override public boolean equals(Object rhs) { if(this == rhs) { return true; } if(rhs == null || getClass() != rhs.getClass()) { return false; } PluralFormat pf = (PluralFormat)rhs; return Utility.objectEquals(ulocale, pf.ulocale) && Utility.objectEquals(pluralRules, pf.pluralRules) && Utility.objectEquals(msgPattern, pf.msgPattern) && Utility.objectEquals(numberFormat, pf.numberFormat); } /** * 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 equals((Object)rhs); } /** * {@inheritDoc} * @stable ICU 3.8 */ @Override public int hashCode() { return pluralRules.hashCode() ^ parsedValues.hashCode(); } /** * {@inheritDoc} * @stable ICU 3.8 */ @Override public String toString() { StringBuilder buf = new StringBuilder(); buf.append("locale=" + ulocale); buf.append(", rules='" + pluralRules + "'"); buf.append(", pattern='" + pattern + "'"); buf.append(", format='" + numberFormat + "'"); return buf.toString(); } private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); pluralRulesWrapper = new PluralSelectorAdapter(); // Ignore the parsedValues from an earlier class version (before ICU 4.8) // and rebuild the msgPattern. parsedValues = null; if (pattern != null) { applyPattern(pattern); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy