MessageFormat.java
// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
/*
**********************************************************************
* Copyright (c) 2004-2016, International Business Machines
* Corporation and others. All Rights Reserved.
**********************************************************************
* Author: Alan Liu
* Created: April 6, 2004
* Since: ICU 3.0
**********************************************************************
*/
package com.ibm.icu.text;
import com.ibm.icu.impl.PatternProps;
import com.ibm.icu.number.NumberFormatter;
import com.ibm.icu.text.MessagePattern.ArgType;
import com.ibm.icu.text.MessagePattern.Part;
import com.ibm.icu.text.PluralRules.IFixedDecimal;
import com.ibm.icu.text.PluralRules.PluralType;
import com.ibm.icu.util.Calendar;
import com.ibm.icu.util.ICUUncheckedIOException;
import com.ibm.icu.util.ULocale;
import com.ibm.icu.util.ULocale.Category;
import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.text.AttributedCharacterIterator;
import java.text.AttributedCharacterIterator.Attribute;
import java.text.AttributedString;
import java.text.CharacterIterator;
import java.text.ChoiceFormat;
import java.text.FieldPosition;
import java.text.Format;
import java.text.ParseException;
import java.text.ParsePosition;
import java.time.DayOfWeek;
import java.time.Month;
import java.time.temporal.Temporal;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
/**
* {@icuenhanced java.text.MessageFormat}.{@icu _usage_}
*
* <p>MessageFormat prepares strings for display to users, with optional arguments
* (variables/placeholders). The arguments can occur in any order, which is necessary for
* translation into languages with different grammars.
*
* <p>A MessageFormat is constructed from a <em>pattern</em> string with arguments in {curly braces}
* which will be replaced by formatted values.
*
* <p><code>MessageFormat</code> differs from the other <code>Format</code> classes in that you
* create a <code>MessageFormat</code> object with one of its constructors (not with a <code>
* getInstance</code> style factory method). Factory methods aren't necessary because <code>
* MessageFormat</code> itself doesn't implement locale-specific behavior. Any locale-specific
* behavior is defined by the pattern that you provide and the subformats used for inserted
* arguments.
*
* <p>Arguments can be named (using identifiers) or numbered (using small ASCII-digit integers).
* Some of the API methods work only with argument numbers and throw an exception if the pattern has
* named arguments (see {@link #usesNamedArguments()}).
*
* <p>An argument might not specify any format type. In this case, a Number value is formatted with
* a default (for the locale) NumberFormat, a Date value is formatted with a default (for the
* locale) DateFormat, and for any other value its toString() value is used.
*
* <p>An argument might specify a "simple" type for which the specified Format object is created,
* cached and used.
*
* <p>An argument might have a "complex" type with nested MessageFormat sub-patterns. During
* formatting, one of these sub-messages is selected according to the argument value and recursively
* formatted.
*
* <p>After construction, a custom Format object can be set for a top-level argument, overriding the
* default formatting and parsing behavior for that argument. However, custom formatting can be
* achieved more simply by writing a typeless argument in the pattern string and supplying it with a
* preformatted string value.
*
* <p>When formatting, MessageFormat takes a collection of argument values and writes an output
* string. The argument values may be passed as an array (when the pattern contains only numbered
* arguments) or as a Map (which works for both named and numbered arguments).
*
* <p>Each argument is matched with one of the input values by array index or map key and formatted
* according to its pattern specification (or using a custom Format object if one was set). A
* numbered pattern argument is matched with a map key that contains that number as an
* ASCII-decimal-digit string (without leading zero).
*
* <h3><a id="patterns">Patterns and Their Interpretation</a></h3>
*
* <code>MessageFormat</code> uses patterns of the following form:
*
* <blockquote>
*
* <pre>
* message = messageText (argument messageText)*
* argument = noneArg | simpleArg | complexArg
* complexArg = choiceArg | pluralArg | selectArg | selectordinalArg
*
* noneArg = '{' argNameOrNumber '}'
* simpleArg = '{' argNameOrNumber ',' argType [',' argStyle] '}'
* choiceArg = '{' argNameOrNumber ',' "choice" ',' choiceStyle '}'
* pluralArg = '{' argNameOrNumber ',' "plural" ',' pluralStyle '}'
* selectArg = '{' argNameOrNumber ',' "select" ',' selectStyle '}'
* selectordinalArg = '{' argNameOrNumber ',' "selectordinal" ',' pluralStyle '}'
*
* choiceStyle: see {@link ChoiceFormat}
* pluralStyle: see {@link PluralFormat}
* selectStyle: see {@link SelectFormat}
*
* argNameOrNumber = argName | argNumber
* argName = [^[[:Pattern_Syntax:][:Pattern_White_Space:]]]+
* argNumber = '0' | ('1'..'9' ('0'..'9')*)
*
* argType = "number" | "date" | "time" | "spellout" | "ordinal" | "duration"
* argStyle = "short" | "medium" | "long" | "full" | "integer" | "currency" | "percent" | argStyleText | "::" argSkeletonText
* </pre>
*
* </blockquote>
*
* <ul>
* <li>messageText can contain quoted literal strings including syntax characters. A quoted
* literal string begins with an ASCII apostrophe and a syntax character (usually a {curly
* brace}) and continues until the next single apostrophe. A double ASCII apostrophe inside or
* outside of a quoted string represents one literal apostrophe.
* <li>Quotable syntax characters are the {curly braces} in all messageText parts, plus the '#'
* sign in a messageText immediately inside a pluralStyle, and the '|' symbol in a messageText
* immediately inside a choiceStyle.
* <li>See also {@link MessagePattern.ApostropheMode}
* <li>In argStyleText, every single ASCII apostrophe begins and ends quoted literal text, and
* unquoted {curly braces} must occur in matched pairs.
* </ul>
*
* <p>Recommendation: Use the real apostrophe (single quote) character \\u2019 for human-readable
* text, and use the ASCII apostrophe (\\u0027 ' ) only in program syntax, like quoting in
* MessageFormat. See the annotations for U+0027 Apostrophe in The Unicode Standard.
*
* <p>The <code>choice</code> argument type is deprecated. Use <code>plural</code> arguments for
* proper plural selection, and <code>select</code> arguments for simple selection among a fixed set
* of choices.
*
* <p>The <code>argType</code> and <code>argStyle</code> values are used to create a <code>Format
* </code> instance for the format element. The following table shows how the values map to Format
* instances. Combinations not shown in the table are illegal. Any <code>argStyleText</code> must be
* a valid pattern string for the Format subclass used.
*
* <table border=1>
* <tr>
* <th>argType
* <th>argStyle
* <th>resulting Format object
* <tr>
* <td colspan=2><i>(none)</i>
* <td><code>null</code>
* <tr>
* <td rowspan=6><code>number</code>
* <td><i>(none)</i>
* <td><code>NumberFormat.getInstance(getLocale())</code>
* <tr>
* <td><code>integer</code>
* <td><code>NumberFormat.getIntegerInstance(getLocale())</code>
* <tr>
* <td><code>currency</code>
* <td><code>NumberFormat.getCurrencyInstance(getLocale())</code>
* <tr>
* <td><code>percent</code>
* <td><code>NumberFormat.getPercentInstance(getLocale())</code>
* <tr>
* <td><i>argStyleText</i>
* <td><code>new DecimalFormat(argStyleText, new DecimalFormatSymbols(getLocale()))</code>
* <tr>
* <td><i>argSkeletonText</i>
* <td><code>NumberFormatter.forSkeleton(argSkeletonText).locale(getLocale()).toFormat()</code>
* <tr>
* <td rowspan=7><code>date</code>
* <td><i>(none)</i>
* <td><code>DateFormat.getDateInstance(DateFormat.DEFAULT, getLocale())</code>
* <tr>
* <td><code>short</code>
* <td><code>DateFormat.getDateInstance(DateFormat.SHORT, getLocale())</code>
* <tr>
* <td><code>medium</code>
* <td><code>DateFormat.getDateInstance(DateFormat.DEFAULT, getLocale())</code>
* <tr>
* <td><code>long</code>
* <td><code>DateFormat.getDateInstance(DateFormat.LONG, getLocale())</code>
* <tr>
* <td><code>full</code>
* <td><code>DateFormat.getDateInstance(DateFormat.FULL, getLocale())</code>
* <tr>
* <td><i>argStyleText</i>
* <td><code>new SimpleDateFormat(argStyleText, getLocale())</code>
* <tr>
* <td><i>argSkeletonText</i>
* <td><code>DateFormat.getInstanceForSkeleton(argSkeletonText, getLocale())</code>
* <tr>
* <td rowspan=6><code>time</code>
* <td><i>(none)</i>
* <td><code>DateFormat.getTimeInstance(DateFormat.DEFAULT, getLocale())</code>
* <tr>
* <td><code>short</code>
* <td><code>DateFormat.getTimeInstance(DateFormat.SHORT, getLocale())</code>
* <tr>
* <td><code>medium</code>
* <td><code>DateFormat.getTimeInstance(DateFormat.DEFAULT, getLocale())</code>
* <tr>
* <td><code>long</code>
* <td><code>DateFormat.getTimeInstance(DateFormat.LONG, getLocale())</code>
* <tr>
* <td><code>full</code>
* <td><code>DateFormat.getTimeInstance(DateFormat.FULL, getLocale())</code>
* <tr>
* <td><i>argStyleText</i>
* <td><code>new SimpleDateFormat(argStyleText, getLocale())</code>
* <tr>
* <td><code>spellout</code>
* <td><i>argStyleText (optional)</i>
* <td><code>new RuleBasedNumberFormat(getLocale(), RuleBasedNumberFormat.SPELLOUT)
* <br> .setDefaultRuleset(argStyleText);</code>
* <tr>
* <td><code>ordinal</code>
* <td><i>argStyleText (optional)</i>
* <td><code>new RuleBasedNumberFormat(getLocale(), RuleBasedNumberFormat.ORDINAL)
* <br> .setDefaultRuleset(argStyleText);</code>
* <tr>
* <td><code>duration</code>
* <td><i>argStyleText (optional)</i>
* <td><code>new RuleBasedNumberFormat(getLocale(), RuleBasedNumberFormat.DURATION)
* <br> .setDefaultRuleset(argStyleText);</code>
* </table>
*
* <h4><a id="diffsjdk">Differences from java.text.MessageFormat</a></h4>
*
* <p>The ICU MessageFormat supports both named and numbered arguments, while the JDK MessageFormat
* only supports numbered arguments. Named arguments make patterns more readable.
*
* <p>ICU implements a more user-friendly apostrophe quoting syntax. In message text, an apostrophe
* only begins quoting literal text if it immediately precedes a syntax character (mostly {curly
* braces}).<br>
* In the JDK MessageFormat, an apostrophe always begins quoting, which requires common text like
* "don't" and "aujourd'hui" to be written with doubled apostrophes like "don''t" and
* "aujourd''hui". For more details see {@link MessagePattern.ApostropheMode}.
*
* <p>ICU does not create a ChoiceFormat object for a choiceArg, pluralArg or selectArg but rather
* handles such arguments itself. The JDK MessageFormat does create and use a ChoiceFormat object (
* <code>new ChoiceFormat(argStyleText)</code>). The JDK does not support plural and select
* arguments at all.
*
* <p>Both the ICU and the JDK <code>MessageFormat</code> can control the argument formats by using
* <code>argStyle</code>. But the JDK <code>MessageFormat</code> only supports predefined formats
* and number / date / time pattern strings (which would need to be localized).<br>
* ICU supports everything the JDK does, and also number / date / time <b>skeletons</b> using the
* <code>::</code> prefix (which automatically yield output appropriate for the <code>MessageFormat
* </code> locale).
*
* <h4>Argument formatting</h4>
*
* <p>Arguments are formatted according to their type, using the default ICU formatters for those
* types, unless otherwise specified. For unknown types, <code>MessageFormat</code> will call <code>
* toString()</code>.
*
* <p>There are also several ways to control the formatting.
*
* <p>We recommend you use default styles, predefined style values, skeletons, or preformatted
* values, but not pattern strings or custom format objects.
*
* <p>For more details, see the <a
* href="https://unicode-org.github.io/icu/userguide/format_parse/messages">ICU User Guide</a>.
*
* <h4>Usage Information</h4>
*
* <p>Here are some examples of usage:
*
* <blockquote>
*
* <pre>
* Object[] arguments = {
* 7,
* new Date(System.currentTimeMillis()),
* "a disturbance in the Force"
* };
*
* String result = MessageFormat.format(
* "At {1,time,::jmm} on {1,date,::dMMMM}, there was {2} on planet {0,number,integer}.",
* arguments);
*
* <em>output</em>: At 4:34 PM on March 23, there was a disturbance
* in the Force on planet 7.
*
* </pre>
*
* </blockquote>
*
* Typically, the message format will come from resources, and the arguments will be dynamically set
* at runtime.
*
* <p>Example 2:
*
* <blockquote>
*
* <pre>
* Object[] testArgs = { 3, "MyDisk" };
*
* MessageFormat form = new MessageFormat(
* "The disk \"{1}\" contains {0} file(s).");
*
* System.out.println(form.format(testArgs));
*
* // output, with different testArgs
* <em>output</em>: The disk "MyDisk" contains 0 file(s).
* <em>output</em>: The disk "MyDisk" contains 1 file(s).
* <em>output</em>: The disk "MyDisk" contains 1,273 file(s).
* </pre>
*
* </blockquote>
*
* <p>For messages that include plural forms, you can use a plural argument:
*
* <pre>
* MessageFormat msgFmt = new MessageFormat(
* "{num_files, plural, " +
* "=0{There are no files on disk \"{disk_name}\".}" +
* "=1{There is one file on disk \"{disk_name}\".}" +
* "other{There are # files on disk \"{disk_name}\".}}",
* ULocale.ENGLISH);
* Map args = new HashMap();
* args.put("num_files", 0);
* args.put("disk_name", "MyDisk");
* System.out.println(msgFmt.format(args));
* args.put("num_files", 3);
* System.out.println(msgFmt.format(args));
*
* <em>output</em>:
* There are no files on disk "MyDisk".
* There are 3 files on "MyDisk".
* </pre>
*
* See {@link PluralFormat} and {@link PluralRules} for details.
*
* <h4><a id="synchronization">Synchronization</a></h4>
*
* <p>MessageFormats are not synchronized. It is recommended to create separate format instances for
* each thread. If multiple threads access a format concurrently, it must be synchronized
* externally.
*
* @see java.util.Locale
* @see Format
* @see NumberFormat
* @see DecimalFormat
* @see ChoiceFormat
* @see PluralFormat
* @see SelectFormat
* @author Mark Davis
* @author Markus Scherer
* @stable ICU 3.0
*/
public class MessageFormat extends UFormat implements Cloneable {
// Incremented by 1 for ICU 4.8's new format.
static final long serialVersionUID = 7136212545847378652L;
/**
* Constructs a MessageFormat for the default <code>FORMAT</code> locale and the specified
* pattern. Sets the locale and calls applyPattern(pattern).
*
* @param pattern the pattern for this message format
* @exception IllegalArgumentException if the pattern is invalid
* @see Category#FORMAT
* @stable ICU 3.0
*/
public MessageFormat(String pattern) {
this.ulocale = ULocale.getDefault(Category.FORMAT);
applyPattern(pattern);
}
/**
* Constructs a MessageFormat for the specified locale and pattern. Sets the locale and calls
* applyPattern(pattern).
*
* @param pattern the pattern for this message format
* @param locale the locale for this message format
* @exception IllegalArgumentException if the pattern is invalid
* @stable ICU 3.0
*/
public MessageFormat(String pattern, Locale locale) {
this(pattern, ULocale.forLocale(locale));
}
/**
* Constructs a MessageFormat for the specified locale and pattern. Sets the locale and calls
* applyPattern(pattern).
*
* @param pattern the pattern for this message format
* @param locale the locale for this message format
* @exception IllegalArgumentException if the pattern is invalid
* @stable ICU 3.2
*/
public MessageFormat(String pattern, ULocale locale) {
this.ulocale = locale;
applyPattern(pattern);
}
/**
* Sets the locale to be used for creating argument Format objects. This affects subsequent
* calls to the {@link #applyPattern applyPattern} method as well as to the <code>format</code>
* and {@link #formatToCharacterIterator formatToCharacterIterator} methods.
*
* @param locale the locale to be used when creating or comparing subformats
* @stable ICU 3.0
*/
public void setLocale(Locale locale) {
setLocale(ULocale.forLocale(locale));
}
/**
* Sets the locale to be used for creating argument Format objects. This affects subsequent
* calls to the {@link #applyPattern applyPattern} method as well as to the <code>format</code>
* and {@link #formatToCharacterIterator formatToCharacterIterator} methods.
*
* @param locale the locale to be used when creating or comparing subformats
* @stable ICU 3.2
*/
public void setLocale(ULocale locale) {
/* Save the pattern, and then reapply so that */
/* we pick up any changes in locale specific */
/* elements */
String existingPattern = toPattern(); /*ibm.3550*/
this.ulocale = locale;
// Invalidate all stock formatters. They are no longer valid since
// the locale has changed.
stockDateFormatter = null;
stockNumberFormatter = null;
pluralProvider = null;
ordinalProvider = null;
applyPattern(existingPattern); /*ibm.3550*/
}
/**
* Returns the locale that's used when creating or comparing subformats.
*
* @return the locale used when creating or comparing subformats
* @stable ICU 3.0
*/
public Locale getLocale() {
return ulocale.toLocale();
}
/**
* {@icu} Returns the locale that's used when creating argument Format objects.
*
* @return the locale used when creating or comparing subformats
* @stable ICU 3.2
*/
public ULocale getULocale() {
return ulocale;
}
/**
* Sets the pattern used by this message format. Parses the pattern and caches Format objects
* for simple argument types. Patterns and their interpretation are specified in the <a
* href="#patterns">class description</a>.
*
* @param pttrn the pattern for this message format
* @throws IllegalArgumentException if the pattern is invalid
* @stable ICU 3.0
*/
public void applyPattern(String pttrn) {
try {
if (msgPattern == null) {
msgPattern = new MessagePattern(pttrn);
} else {
msgPattern.parse(pttrn);
}
// Cache the formats that are explicitly mentioned in the message pattern.
cacheExplicitFormats();
} catch (RuntimeException e) {
resetPattern();
throw e;
}
}
/**
* {@icu} Sets the ApostropheMode and the pattern used by this message format. Parses the
* pattern and caches Format objects for simple argument types. Patterns and their
* interpretation are specified in the <a href="#patterns">class description</a>.
*
* <p>This method is best used only once on a given object to avoid confusion about the mode,
* and after constructing the object with an empty pattern string to minimize overhead.
*
* @param pattern the pattern for this message format
* @param aposMode the new ApostropheMode
* @throws IllegalArgumentException if the pattern is invalid
* @see MessagePattern.ApostropheMode
* @stable ICU 4.8
*/
public void applyPattern(String pattern, MessagePattern.ApostropheMode aposMode) {
if (msgPattern == null) {
msgPattern = new MessagePattern(aposMode);
} else if (aposMode != msgPattern.getApostropheMode()) {
msgPattern.clearPatternAndSetApostropheMode(aposMode);
}
applyPattern(pattern);
}
/**
* {@icu}
*
* @return this instance's ApostropheMode.
* @stable ICU 4.8
*/
public MessagePattern.ApostropheMode getApostropheMode() {
if (msgPattern == null) {
msgPattern = new MessagePattern(); // Sets the default mode.
}
return msgPattern.getApostropheMode();
}
/**
* Returns the applied pattern string.
*
* @return the pattern string
* @throws IllegalStateException after custom Format objects have been set via setFormat() or
* similar APIs
* @stable ICU 3.0
*/
public String toPattern() {
// Return the original, applied pattern string, or else "".
// Note: This does not take into account
// - changes from setFormat() and similar methods, or
// - normalization of apostrophes and arguments, for example,
// whether some date/time/number formatter was created via a pattern
// but is equivalent to the "medium" default format.
if (customFormatArgStarts != null) {
throw new IllegalStateException(
"toPattern() is not supported after custom Format objects "
+ "have been set via setFormat() or similar APIs");
}
if (msgPattern == null) {
return "";
}
String originalPattern = msgPattern.getPatternString();
return originalPattern == null ? "" : originalPattern;
}
/**
* Returns the part index of the next ARG_START after partIndex, or -1 if there is none more.
*
* @param partIndex Part index of the previous ARG_START (initially 0).
*/
private int nextTopLevelArgStart(int partIndex) {
if (partIndex != 0) {
partIndex = msgPattern.getLimitPartIndex(partIndex);
}
for (; ; ) {
MessagePattern.Part.Type type = msgPattern.getPartType(++partIndex);
if (type == MessagePattern.Part.Type.ARG_START) {
return partIndex;
}
if (type == MessagePattern.Part.Type.MSG_LIMIT) {
return -1;
}
}
}
private boolean argNameMatches(int partIndex, String argName, int argNumber) {
Part part = msgPattern.getPart(partIndex);
return part.getType() == MessagePattern.Part.Type.ARG_NAME
? msgPattern.partSubstringMatches(part, argName)
: part.getValue() == argNumber; // ARG_NUMBER
}
private String getArgName(int partIndex) {
Part part = msgPattern.getPart(partIndex);
if (part.getType() == MessagePattern.Part.Type.ARG_NAME) {
return msgPattern.getSubstring(part);
} else {
return Integer.toString(part.getValue());
}
}
/**
* Sets the Format objects to use for the values passed into <code>format</code> methods or
* returned from <code>parse</code> methods. The indices of elements in <code>newFormats</code>
* correspond to the argument indices used in the previously set pattern string. The order of
* formats in <code>newFormats</code> thus corresponds to the order of elements in the <code>
* arguments</code> array passed to the <code>format</code> methods or the result array returned
* by the <code>parse</code> methods.
*
* <p>If an argument index is used for more than one format element in the pattern string, then
* the corresponding new format is used for all such format elements. If an argument index is
* not used for any format element in the pattern string, then the corresponding new format is
* ignored. If fewer formats are provided than needed, then only the formats for argument
* indices less than <code>newFormats.length</code> are replaced.
*
* <p>This method is only supported if the format does not use named arguments, otherwise an
* IllegalArgumentException is thrown.
*
* @param newFormats the new formats to use
* @throws NullPointerException if <code>newFormats</code> is null
* @throws IllegalArgumentException if this formatter uses named arguments
* @stable ICU 3.0
*/
public void setFormatsByArgumentIndex(Format[] newFormats) {
if (msgPattern.hasNamedArguments()) {
throw new IllegalArgumentException(
"This method is not available in MessageFormat objects "
+ "that use alphanumeric argument names.");
}
for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0; ) {
int argNumber = msgPattern.getPart(partIndex + 1).getValue();
if (argNumber < newFormats.length) {
setCustomArgStartFormat(partIndex, newFormats[argNumber]);
}
}
}
/**
* {@icu} Sets the Format objects to use for the values passed into <code>format</code> methods
* or returned from <code>parse</code> methods. The keys in <code>newFormats</code> are the
* argument names in the previously set pattern string, and the values are the formats.
*
* <p>Only argument names from the pattern string are considered. Extra keys in <code>newFormats
* </code> that do not correspond to an argument name are ignored. Similarly, if there is no
* format in newFormats for an argument name, the formatter for that argument remains unchanged.
*
* <p>This may be called on formats that do not use named arguments. In this case the map will
* be queried for key Strings that represent argument indices, e.g. "0", "1", "2" etc.
*
* @param newFormats a map from String to Format providing new formats for named arguments.
* @stable ICU 3.8
*/
public void setFormatsByArgumentName(Map<String, Format> newFormats) {
for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0; ) {
String key = getArgName(partIndex + 1);
if (newFormats.containsKey(key)) {
setCustomArgStartFormat(partIndex, newFormats.get(key));
}
}
}
/**
* Sets the Format objects to use for the format elements in the previously set pattern string.
* The order of formats in <code>newFormats</code> corresponds to the order of format elements
* in the pattern string.
*
* <p>If more formats are provided than needed by the pattern string, the remaining ones are
* ignored. If fewer formats are provided than needed, then only the first <code>
* newFormats.length</code> formats are replaced.
*
* <p>Since the order of format elements in a pattern string often changes during localization,
* it is generally better to use the {@link #setFormatsByArgumentIndex
* setFormatsByArgumentIndex} method, which assumes an order of formats corresponding to the
* order of elements in the <code>arguments</code> array passed to the <code>format</code>
* methods or the result array returned by the <code>parse</code> methods.
*
* @param newFormats the new formats to use
* @exception NullPointerException if <code>newFormats</code> is null
* @stable ICU 3.0
*/
public void setFormats(Format[] newFormats) {
int formatNumber = 0;
for (int partIndex = 0;
formatNumber < newFormats.length
&& (partIndex = nextTopLevelArgStart(partIndex)) >= 0; ) {
setCustomArgStartFormat(partIndex, newFormats[formatNumber]);
++formatNumber;
}
}
/**
* Sets the Format object to use for the format elements within the previously set pattern
* string that use the given argument index. The argument index is part of the format element
* definition and represents an index into the <code>arguments</code> array passed to the <code>
* format</code> methods or the result array returned by the <code>parse</code> methods.
*
* <p>If the argument index is used for more than one format element in the pattern string, then
* the new format is used for all such format elements. If the argument index is not used for
* any format element in the pattern string, then the new format is ignored.
*
* <p>This method is only supported when exclusively numbers are used for argument names.
* Otherwise an IllegalArgumentException is thrown.
*
* @param argumentIndex the argument index for which to use the new format
* @param newFormat the new format to use
* @throws IllegalArgumentException if this format uses named arguments
* @stable ICU 3.0
*/
public void setFormatByArgumentIndex(int argumentIndex, Format newFormat) {
if (msgPattern.hasNamedArguments()) {
throw new IllegalArgumentException(
"This method is not available in MessageFormat objects "
+ "that use alphanumeric argument names.");
}
for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0; ) {
if (msgPattern.getPart(partIndex + 1).getValue() == argumentIndex) {
setCustomArgStartFormat(partIndex, newFormat);
}
}
}
/**
* {@icu} Sets the Format object to use for the format elements within the previously set
* pattern string that use the given argument name.
*
* <p>If the argument name is used for more than one format element in the pattern string, then
* the new format is used for all such format elements. If the argument name is not used for any
* format element in the pattern string, then the new format is ignored.
*
* <p>This API may be used on formats that do not use named arguments. In this case <code>
* argumentName</code> should be a String that names an argument index, e.g. "0", "1", "2"...
* etc. If it does not name a valid index, the format will be ignored. No error is thrown.
*
* @param argumentName the name of the argument to change
* @param newFormat the new format to use
* @stable ICU 3.8
*/
public void setFormatByArgumentName(String argumentName, Format newFormat) {
int argNumber = MessagePattern.validateArgumentName(argumentName);
if (argNumber < MessagePattern.ARG_NAME_NOT_NUMBER) {
return;
}
for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0; ) {
if (argNameMatches(partIndex + 1, argumentName, argNumber)) {
setCustomArgStartFormat(partIndex, newFormat);
}
}
}
/**
* Sets the Format object to use for the format element with the given format element index
* within the previously set pattern string. The format element index is the zero-based number
* of the format element counting from the start of the pattern string.
*
* <p>Since the order of format elements in a pattern string often changes during localization,
* it is generally better to use the {@link #setFormatByArgumentIndex setFormatByArgumentIndex}
* method, which accesses format elements based on the argument index they specify.
*
* @param formatElementIndex the index of a format element within the pattern
* @param newFormat the format to use for the specified format element
* @exception ArrayIndexOutOfBoundsException if formatElementIndex is equal to or larger than
* the number of format elements in the pattern string
* @stable ICU 3.0
*/
public void setFormat(int formatElementIndex, Format newFormat) {
int formatNumber = 0;
for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0; ) {
if (formatNumber == formatElementIndex) {
setCustomArgStartFormat(partIndex, newFormat);
return;
}
++formatNumber;
}
throw new ArrayIndexOutOfBoundsException(formatElementIndex);
}
/**
* Returns the Format objects used for the values passed into <code>format</code> methods or
* returned from <code>parse</code> methods. The indices of elements in the returned array
* correspond to the argument indices used in the previously set pattern string. The order of
* formats in the returned array thus corresponds to the order of elements in the <code>
* arguments</code> array passed to the <code>format</code> methods or the result array returned
* by the <code>parse</code> methods.
*
* <p>If an argument index is used for more than one format element in the pattern string, then
* the format used for the last such format element is returned in the array. If an argument
* index is not used for any format element in the pattern string, then null is returned in the
* array.
*
* <p>This method is only supported when exclusively numbers are used for argument names.
* Otherwise an IllegalArgumentException is thrown.
*
* @return the formats used for the arguments within the pattern
* @throws IllegalArgumentException if this format uses named arguments
* @stable ICU 3.0
*/
public Format[] getFormatsByArgumentIndex() {
if (msgPattern.hasNamedArguments()) {
throw new IllegalArgumentException(
"This method is not available in MessageFormat objects "
+ "that use alphanumeric argument names.");
}
ArrayList<Format> list = new ArrayList<>();
for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0; ) {
int argNumber = msgPattern.getPart(partIndex + 1).getValue();
while (argNumber >= list.size()) {
list.add(null);
}
list.set(argNumber, cachedFormatters == null ? null : cachedFormatters.get(partIndex));
}
return list.toArray(new Format[list.size()]);
}
/**
* Returns the Format objects used for the format elements in the previously set pattern string.
* The order of formats in the returned array corresponds to the order of format elements in the
* pattern string.
*
* <p>Since the order of format elements in a pattern string often changes during localization,
* it's generally better to use the {@link #getFormatsByArgumentIndex()} method, which assumes
* an order of formats corresponding to the order of elements in the <code>arguments</code>
* array passed to the <code>format</code> methods or the result array returned by the <code>
* parse</code> methods.
*
* <p>This method is only supported when exclusively numbers are used for argument names.
* Otherwise an IllegalArgumentException is thrown.
*
* @return the formats used for the format elements in the pattern
* @throws IllegalArgumentException if this format uses named arguments
* @stable ICU 3.0
*/
public Format[] getFormats() {
ArrayList<Format> list = new ArrayList<>();
for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0; ) {
list.add(cachedFormatters == null ? null : cachedFormatters.get(partIndex));
}
return list.toArray(new Format[list.size()]);
}
/**
* {@icu} Returns the top-level argument names. For more details, see {@link
* #setFormatByArgumentName(String, Format)}.
*
* @return a Set of argument names
* @stable ICU 4.8
*/
public Set<String> getArgumentNames() {
Set<String> result = new HashSet<>();
for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0; ) {
result.add(getArgName(partIndex + 1));
}
return result;
}
/**
* {@icu} Returns the first top-level format associated with the given argument name. For more
* details, see {@link #setFormatByArgumentName(String, Format)}.
*
* @param argumentName The name of the desired argument.
* @return the Format associated with the name, or null if there isn't one.
* @stable ICU 4.8
*/
public Format getFormatByArgumentName(String argumentName) {
if (cachedFormatters == null) {
return null;
}
int argNumber = MessagePattern.validateArgumentName(argumentName);
if (argNumber < MessagePattern.ARG_NAME_NOT_NUMBER) {
return null;
}
for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0; ) {
if (argNameMatches(partIndex + 1, argumentName, argNumber)) {
return cachedFormatters.get(partIndex);
}
}
return null;
}
/**
* Formats an array of objects and appends the <code>MessageFormat</code>'s pattern, with
* arguments replaced by the formatted objects, to the provided <code>StringBuffer</code>.
*
* <p>The text substituted for the individual format elements is derived from the current
* subformat of the format element and the <code>arguments</code> element at the format
* element's argument index as indicated by the first matching line of the following table. An
* argument is <i>unavailable</i> if <code>arguments</code> is <code>null</code> or has fewer
* than argumentIndex+1 elements. When an argument is unavailable no substitution is performed.
*
* <table border=1>
* <tr>
* <th>argType or Format
* <th>value object
* <th>Formatted Text
* <tr>
* <td><i>any</i>
* <td><i>unavailable</i>
* <td><code>"{" + argNameOrNumber + "}"</code>
* <tr>
* <td><i>any</i>
* <td><code>null</code>
* <td><code>"null"</code>
* <tr>
* <td>custom Format <code>!= null</code>
* <td><i>any</i>
* <td><code>customFormat.format(argument)</code>
* <tr>
* <td>noneArg, or custom Format <code>== null</code>
* <td><code>instanceof Number</code>
* <td><code>NumberFormat.getInstance(getLocale()).format(argument)</code>
* <tr>
* <td>noneArg, or custom Format <code>== null</code>
* <td><code>instanceof Date</code>
* <td><code>DateFormat.getDateTimeInstance(DateFormat.SHORT,
* DateFormat.SHORT, getLocale()).format(argument)</code>
* <tr>
* <td>noneArg, or custom Format <code>== null</code>
* <td><code>instanceof String</code>
* <td><code>argument</code>
* <tr>
* <td>noneArg, or custom Format <code>== null</code>
* <td><i>any</i>
* <td><code>argument.toString()</code>
* <tr>
* <td>complexArg
* <td><i>any</i>
* <td>result of recursive formatting of a selected sub-message
* </table>
*
* <p>If <code>pos</code> is non-null, and refers to <code>Field.ARGUMENT</code>, the location
* of the first formatted string will be returned.
*
* <p>This method is only supported when the format does not use named arguments, otherwise an
* IllegalArgumentException is thrown.
*
* @param arguments an array of objects to be formatted and substituted.
* @param result where text is appended.
* @param pos On input: an alignment field, if desired. On output: the offsets of the alignment
* field.
* @throws IllegalArgumentException if a value in the <code>arguments</code> array is not of the
* type expected by the corresponding argument or custom Format object.
* @throws IllegalArgumentException if this format uses named arguments
* @stable ICU 3.0
*/
public final StringBuffer format(Object[] arguments, StringBuffer result, FieldPosition pos) {
format(arguments, null, new AppendableWrapper(result), pos);
return result;
}
/**
* Formats a map of objects and appends the <code>MessageFormat</code>'s pattern, with arguments
* replaced by the formatted objects, to the provided <code>StringBuffer</code>.
*
* <p>The text substituted for the individual format elements is derived from the current
* subformat of the format element and the <code>arguments</code> value corresponding to the
* format element's argument name.
*
* <p>A numbered pattern argument is matched with a map key that contains that number as an
* ASCII-decimal-digit string (without leading zero).
*
* <p>An argument is <i>unavailable</i> if <code>arguments</code> is <code>null</code> or does
* not have a value corresponding to an argument name in the pattern. When an argument is
* unavailable no substitution is performed.
*
* @param arguments a map of objects to be formatted and substituted.
* @param result where text is appended.
* @param pos On input: an alignment field, if desired. On output: the offsets of the alignment
* field.
* @throws IllegalArgumentException if a value in the <code>arguments</code> array is not of the
* type expected by the corresponding argument or custom Format object.
* @return the passed-in StringBuffer
* @stable ICU 3.8
*/
public final StringBuffer format(
Map<String, Object> arguments, StringBuffer result, FieldPosition pos) {
format(null, arguments, new AppendableWrapper(result), pos);
return result;
}
/**
* Creates a MessageFormat with the given pattern and uses it to format the given arguments.
* This is equivalent to
*
* <blockquote>
*
* <code>(new {@link #MessageFormat(String) MessageFormat}(pattern)).{@link
* #format(java.lang.Object[], java.lang.StringBuffer, java.text.FieldPosition)
* format}(arguments, new StringBuffer(), null).toString()</code>
*
* </blockquote>
*
* @throws IllegalArgumentException if the pattern is invalid
* @throws IllegalArgumentException if a value in the <code>arguments</code> array is not of the
* type expected by the corresponding argument or custom Format object.
* @throws IllegalArgumentException if this format uses named arguments
* @stable ICU 3.0
*/
public static String format(String pattern, Object... arguments) {
MessageFormat temp = new MessageFormat(pattern);
return temp.format(arguments);
}
/**
* Creates a MessageFormat with the given pattern and uses it to format the given arguments. The
* pattern must identifyarguments by name instead of by number.
*
* <p>
*
* @throws IllegalArgumentException if the pattern is invalid
* @throws IllegalArgumentException if a value in the <code>arguments</code> array is not of the
* type expected by the corresponding argument or custom Format object.
* @see #format(Map, StringBuffer, FieldPosition)
* @see #format(String, Object[])
* @stable ICU 3.8
*/
public static String format(String pattern, Map<String, Object> arguments) {
MessageFormat temp = new MessageFormat(pattern);
return temp.format(arguments);
}
/**
* {@icu} Returns true if this MessageFormat uses named arguments, and false otherwise. See
* class description.
*
* @return true if named arguments are used.
* @stable ICU 3.8
*/
public boolean usesNamedArguments() {
return msgPattern.hasNamedArguments();
}
// Overrides
/**
* Formats a map or array of objects and appends the <code>MessageFormat</code>'s pattern, with
* format elements replaced by the formatted objects, to the provided <code>StringBuffer</code>.
* This is equivalent to either of
*
* <blockquote>
*
* <code>{@link #format(java.lang.Object[], java.lang.StringBuffer,
* java.text.FieldPosition) format}((Object[]) arguments, result, pos)</code> <code>
* {@link #format(java.util.Map, java.lang.StringBuffer,
* java.text.FieldPosition) format}((Map) arguments, result, pos)</code>
*
* </blockquote>
*
* A map must be provided if this format uses named arguments, otherwise an
* IllegalArgumentException will be thrown.
*
* @param arguments a map or array of objects to be formatted
* @param result where text is appended
* @param pos On input: an alignment field, if desired On output: the offsets of the alignment
* field
* @throws IllegalArgumentException if an argument in <code>arguments</code> is not of the type
* expected by the format element(s) that use it
* @throws IllegalArgumentException if <code>arguments</code> is an array of Object and this
* format uses named arguments
* @stable ICU 3.0
*/
@Override
public final StringBuffer format(Object arguments, StringBuffer result, FieldPosition pos) {
format(arguments, new AppendableWrapper(result), pos);
return result;
}
/**
* Formats an array of objects and inserts them into the <code>MessageFormat</code>'s pattern,
* producing an <code>AttributedCharacterIterator</code>. You can use the returned <code>
* AttributedCharacterIterator</code> to build the resulting String, as well as to determine
* information about the resulting String.
*
* <p>The text of the returned <code>AttributedCharacterIterator</code> is the same that would
* be returned by
*
* <blockquote>
*
* <code>{@link #format(java.lang.Object[], java.lang.StringBuffer,
* java.text.FieldPosition) format}(arguments, new StringBuffer(), null).toString()</code>
*
* </blockquote>
*
* <p>In addition, the <code>AttributedCharacterIterator</code> contains at least attributes
* indicating where text was generated from an argument in the <code>arguments</code> array. The
* keys of these attributes are of type <code>MessageFormat.Field</code>, their values are
* <code>Integer</code> objects indicating the index in the <code>arguments</code> array of the
* argument from which the text was generated.
*
* <p>The attributes/value from the underlying <code>Format</code> instances that <code>
* MessageFormat</code> uses will also be placed in the resulting <code>
* AttributedCharacterIterator</code>. This allows you to not only find where an argument is
* placed in the resulting String, but also which fields it contains in turn.
*
* @param arguments an array of objects to be formatted and substituted.
* @return AttributedCharacterIterator describing the formatted value.
* @exception NullPointerException if <code>arguments</code> is null.
* @throws IllegalArgumentException if a value in the <code>arguments</code> array is not of the
* type expected by the corresponding argument or custom Format object.
* @stable ICU 3.8
*/
@Override
public AttributedCharacterIterator formatToCharacterIterator(Object arguments) {
if (arguments == null) {
throw new NullPointerException(
"formatToCharacterIterator must be passed non-null object");
}
StringBuilder result = new StringBuilder();
AppendableWrapper wrapper = new AppendableWrapper(result);
wrapper.useAttributes();
format(arguments, wrapper, null);
AttributedString as = new AttributedString(result.toString());
for (AttributeAndPosition a : wrapper.attributes) {
as.addAttribute(a.key, a.value, a.start, a.limit);
}
return as.getIterator();
}
/**
* Parses the string.
*
* <p>Caveats: The parse may fail in a number of circumstances. For example:
*
* <ul>
* <li>If one of the arguments does not occur in the pattern.
* <li>If the format of an argument loses information, such as with a choice format where a
* large number formats to "many".
* <li>Does not yet handle recursion (where the substituted strings contain {n} references.)
* <li>Will not always find a match (or the correct match) if some part of the parse is
* ambiguous. For example, if the pattern "{1},{2}" is used with the string arguments
* {"a,b", "c"}, it will format as "a,b,c". When the result is parsed, it will return
* {"a", "b,c"}.
* <li>If a single argument is parsed more than once in the string, then the later parse wins.
* </ul>
*
* When the parse fails, use ParsePosition.getErrorIndex() to find out where in the string did
* the parsing failed. The returned error index is the starting offset of the sub-patterns that
* the string is comparing with. For example, if the parsing string "AAA {0} BBB" is comparing
* against the pattern "AAD {0} BBB", the error index is 0. When an error occurs, the call to
* this method will return null. If the source is null, return an empty array.
*
* @throws IllegalArgumentException if this format uses named arguments
* @stable ICU 3.0
*/
public Object[] parse(String source, ParsePosition pos) {
if (msgPattern.hasNamedArguments()) {
throw new IllegalArgumentException(
"This method is not available in MessageFormat objects "
+ "that use named argument.");
}
// Count how many slots we need in the array.
int maxArgId = -1;
for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0; ) {
int argNumber = msgPattern.getPart(partIndex + 1).getValue();
if (argNumber > maxArgId) {
maxArgId = argNumber;
}
}
Object[] resultArray = new Object[maxArgId + 1];
int backupStartPos = pos.getIndex();
parse(0, source, pos, resultArray, null);
if (pos.getIndex() == backupStartPos) { // unchanged, returned object is null
return null;
}
return resultArray;
}
/**
* {@icu} Parses the string, returning the results in a Map. This is similar to the version that
* returns an array of Object. This supports both named and numbered arguments-- if numbered,
* the keys in the map are the corresponding ASCII-decimal-digit strings (e.g. "0", "1",
* "2"...).
*
* @param source the text to parse
* @param pos the position at which to start parsing. on return, contains the result of the
* parse.
* @return a Map containing key/value pairs for each parsed argument.
* @stable ICU 3.8
*/
public Map<String, Object> parseToMap(String source, ParsePosition pos) {
Map<String, Object> result = new HashMap<>();
int backupStartPos = pos.getIndex();
parse(0, source, pos, null, result);
if (pos.getIndex() == backupStartPos) {
return null;
}
return result;
}
/**
* Parses text from the beginning of the given string to produce an object array. The method may
* not use the entire text of the given string.
*
* <p>See the {@link #parse(String, ParsePosition)} method for more information on message
* parsing.
*
* @param source A <code>String</code> whose beginning should be parsed.
* @return An <code>Object</code> array parsed from the string.
* @exception ParseException if the beginning of the specified string cannot be parsed.
* @exception IllegalArgumentException if this format uses named arguments
* @stable ICU 3.0
*/
public Object[] parse(String source) throws ParseException {
ParsePosition pos = new ParsePosition(0);
Object[] result = parse(source, pos);
if (pos.getIndex() == 0) // unchanged, returned object is null
throw new ParseException("MessageFormat parse error!", pos.getErrorIndex());
return result;
}
/**
* Parses the string, filling either the Map or the Array. This is a private method that all the
* public parsing methods call. This supports both named and numbered arguments-- if numbered,
* the keys in the map are the corresponding ASCII-decimal-digit strings (e.g. "0", "1",
* "2"...).
*
* @param msgStart index in the message pattern to start from.
* @param source the text to parse
* @param pos the position at which to start parsing. on return, contains the result of the
* parse.
* @param args if not null, the parse results will be filled here (The pattern has to have
* numbered arguments in order for this to not be null).
* @param argsMap if not null, the parse results will be filled here.
*/
private void parse(
int msgStart,
String source,
ParsePosition pos,
Object[] args,
Map<String, Object> argsMap) {
if (source == null) {
return;
}
String msgString = msgPattern.getPatternString();
int prevIndex = msgPattern.getPart(msgStart).getLimit();
int sourceOffset = pos.getIndex();
ParsePosition tempStatus = new ParsePosition(0);
for (int i = msgStart + 1; ; ++i) {
Part part = msgPattern.getPart(i);
Part.Type type = part.getType();
int index = part.getIndex();
// Make sure the literal string matches.
int len = index - prevIndex;
if (len == 0 || msgString.regionMatches(prevIndex, source, sourceOffset, len)) {
sourceOffset += len;
prevIndex += len;
} else {
pos.setErrorIndex(sourceOffset);
return; // leave index as is to signal error
}
if (type == Part.Type.MSG_LIMIT) {
// Things went well! Done.
pos.setIndex(sourceOffset);
return;
}
if (type == Part.Type.SKIP_SYNTAX || type == Part.Type.INSERT_CHAR) {
prevIndex = part.getLimit();
continue;
}
// We do not support parsing Plural formats. (No REPLACE_NUMBER here.)
assert type == Part.Type.ARG_START : "Unexpected Part " + part + " in parsed message.";
int argLimit = msgPattern.getLimitPartIndex(i);
ArgType argType = part.getArgType();
part = msgPattern.getPart(++i);
// Compute the argId, so we can use it as a key.
Object argId = null;
int argNumber = 0;
String key = null;
if (args != null) {
argNumber = part.getValue(); // ARG_NUMBER
argId = argNumber;
} else {
if (part.getType() == MessagePattern.Part.Type.ARG_NAME) {
key = msgPattern.getSubstring(part);
} else /* ARG_NUMBER */ {
key = Integer.toString(part.getValue());
}
argId = key;
}
++i;
Format formatter = null;
boolean haveArgResult = false;
Object argResult = null;
if (cachedFormatters != null && (formatter = cachedFormatters.get(i - 2)) != null) {
// Just parse using the formatter.
tempStatus.setIndex(sourceOffset);
argResult = formatter.parseObject(source, tempStatus);
if (tempStatus.getIndex() == sourceOffset) {
pos.setErrorIndex(sourceOffset);
return; // leave index as is to signal error
}
haveArgResult = true;
sourceOffset = tempStatus.getIndex();
} else if (argType == ArgType.NONE
|| (cachedFormatters != null && cachedFormatters.containsKey(i - 2))) {
// Match as a string.
// if at end, use longest possible match
// otherwise uses first match to intervening string
// does NOT recursively try all possibilities
String stringAfterArgument = getLiteralStringUntilNextArgument(argLimit);
int next;
if (stringAfterArgument.length() != 0) {
next = source.indexOf(stringAfterArgument, sourceOffset);
} else {
next = source.length();
}
if (next < 0) {
pos.setErrorIndex(sourceOffset);
return; // leave index as is to signal error
} else {
String strValue = source.substring(sourceOffset, next);
if (!strValue.equals("{" + argId.toString() + "}")) {
haveArgResult = true;
argResult = strValue;
}
sourceOffset = next;
}
} else if (argType == ArgType.CHOICE) {
tempStatus.setIndex(sourceOffset);
double choiceResult = parseChoiceArgument(msgPattern, i, source, tempStatus);
if (tempStatus.getIndex() == sourceOffset) {
pos.setErrorIndex(sourceOffset);
return; // leave index as is to signal error
}
argResult = choiceResult;
haveArgResult = true;
sourceOffset = tempStatus.getIndex();
} else if (argType.hasPluralStyle() || argType == ArgType.SELECT) {
// No can do!
throw new UnsupportedOperationException(
"Parsing of plural/select/selectordinal argument is not supported.");
} else {
// This should never happen.
throw new IllegalStateException("unexpected argType " + argType);
}
if (haveArgResult) {
if (args != null) {
args[argNumber] = argResult;
} else if (argsMap != null) {
argsMap.put(key, argResult);
}
}
prevIndex = msgPattern.getPart(argLimit).getLimit();
i = argLimit;
}
}
/**
* {@icu} Parses text from the beginning of the given string to produce a map from argument to
* values. The method may not use the entire text of the given string.
*
* <p>See the {@link #parse(String, ParsePosition)} method for more information on message
* parsing.
*
* @param source A <code>String</code> whose beginning should be parsed.
* @return A <code>Map</code> parsed from the string.
* @throws ParseException if the beginning of the specified string cannot be parsed.
* @see #parseToMap(String, ParsePosition)
* @stable ICU 3.8
*/
public Map<String, Object> parseToMap(String source) throws ParseException {
ParsePosition pos = new ParsePosition(0);
Map<String, Object> result = new HashMap<>();
parse(0, source, pos, null, result);
if (pos.getIndex() == 0) // unchanged, returned object is null
throw new ParseException("MessageFormat parse error!", pos.getErrorIndex());
return result;
}
/**
* Parses text from a string to produce an object array or Map.
*
* <p>The method attempts to parse text starting at the index given by <code>pos</code>. If
* parsing succeeds, then the index of <code>pos</code> is updated to the index after the last
* character used (parsing does not necessarily use all characters up to the end of the string),
* and the parsed object array is returned. The updated <code>pos</code> can be used to indicate
* the starting point for the next call to this method. If an error occurs, then the index of
* <code>pos</code> is not changed, the error index of <code>pos</code> is set to the index of
* the character where the error occurred, and null is returned.
*
* <p>See the {@link #parse(String, ParsePosition)} method for more information on message
* parsing.
*
* @param source A <code>String</code>, part of which should be parsed.
* @param pos A <code>ParsePosition</code> object with index and error index information as
* described above.
* @return An <code>Object</code> parsed from the string, either an array of Object, or a Map,
* depending on whether named arguments are used. This can be queried using <code>
* usesNamedArguments</code>. In case of error, returns null.
* @throws NullPointerException if <code>pos</code> is null.
* @stable ICU 3.0
*/
@Override
public Object parseObject(String source, ParsePosition pos) {
if (!msgPattern.hasNamedArguments()) {
return parse(source, pos);
} else {
return parseToMap(source, pos);
}
}
/**
* {@inheritDoc}
*
* @stable ICU 3.0
*/
@Override
public MessageFormat clone() {
MessageFormat other = (MessageFormat) super.clone();
if (customFormatArgStarts != null) {
other.customFormatArgStarts = new HashSet<>();
for (Integer key : customFormatArgStarts) {
other.customFormatArgStarts.add(key);
}
} else {
other.customFormatArgStarts = null;
}
if (cachedFormatters != null) {
other.cachedFormatters = new HashMap<>();
Iterator<Map.Entry<Integer, Format>> it = cachedFormatters.entrySet().iterator();
while (it.hasNext()) {
Map.Entry<Integer, Format> entry = it.next();
other.cachedFormatters.put(entry.getKey(), entry.getValue());
}
} else {
other.cachedFormatters = null;
}
other.msgPattern = msgPattern == null ? null : msgPattern.clone();
other.stockDateFormatter = stockDateFormatter == null ? null : stockDateFormatter.clone();
other.stockNumberFormatter =
stockNumberFormatter == null ? null : stockNumberFormatter.clone();
other.pluralProvider = null;
other.ordinalProvider = null;
return other;
}
/**
* {@inheritDoc}
*
* @stable ICU 3.0
*/
@Override
public boolean equals(Object obj) {
if (this == obj) // quick check
return true;
if (obj == null || getClass() != obj.getClass()) return false;
MessageFormat other = (MessageFormat) obj;
return Objects.equals(ulocale, other.ulocale)
&& Objects.equals(msgPattern, other.msgPattern)
&& Objects.equals(cachedFormatters, other.cachedFormatters)
&& Objects.equals(customFormatArgStarts, other.customFormatArgStarts);
// Note: It might suffice to only compare custom formatters
// rather than all formatters.
}
/**
* {@inheritDoc}
*
* @stable ICU 3.0
*/
@Override
public int hashCode() {
return msgPattern.getPatternString().hashCode(); // enough for reasonable distribution
}
/**
* Defines constants that are used as attribute keys in the <code>AttributedCharacterIterator
* </code> returned from <code>MessageFormat.formatToCharacterIterator</code>.
*
* @stable ICU 3.8
*/
public static class Field extends Format.Field {
private static final long serialVersionUID = 7510380454602616157L;
/**
* Create a <code>Field</code> with the specified name.
*
* @param name The name of the attribute
* @stable ICU 3.8
*/
protected Field(String name) {
super(name);
}
/**
* Resolves instances being deserialized to the predefined constants.
*
* @return resolved MessageFormat.Field constant
* @throws InvalidObjectException if the constant could not be resolved.
* @stable ICU 3.8
*/
@Override
protected Object readResolve() throws InvalidObjectException {
if (this.getClass() != MessageFormat.Field.class) {
throw new InvalidObjectException(
"A subclass of MessageFormat.Field must implement readResolve.");
}
if (this.getName().equals(ARGUMENT.getName())) {
return ARGUMENT;
} else {
throw new InvalidObjectException("Unknown attribute name.");
}
}
/**
* Constant identifying a portion of a message that was generated from an argument passed
* into <code>formatToCharacterIterator</code>. The value associated with the key will be an
* <code>Integer</code> indicating the index in the <code>arguments</code> array of the
* argument from which the text was generated.
*
* @stable ICU 3.8
*/
public static final Field ARGUMENT = new Field("message argument field");
}
// ===========================privates============================
// *Important*: All fields must be declared *transient* so that we can fully
// control serialization!
// See for example Joshua Bloch's "Effective Java", chapter 10 Serialization.
/** The locale to use for formatting numbers and dates. */
private transient ULocale ulocale;
/** The MessagePattern which contains the parsed structure of the pattern string. */
private transient MessagePattern msgPattern;
/**
* Cached formatters so we can just use them whenever needed instead of creating them from
* scratch every time.
*/
private transient Map<Integer, Format> cachedFormatters;
/**
* Set of ARG_START part indexes where custom, user-provided Format objects have been set via
* setFormat() or similar API.
*/
private transient Set<Integer> customFormatArgStarts;
/**
* Stock formatters. Those are used when a format is not explicitly mentioned in the message.
* The format is inferred from the argument.
*/
private transient DateFormat stockDateFormatter;
private transient NumberFormat stockNumberFormatter;
private transient PluralSelectorProvider pluralProvider;
private transient PluralSelectorProvider ordinalProvider;
private DateFormat getStockDateFormatter() {
if (stockDateFormatter == null) {
stockDateFormatter =
DateFormat.getDateTimeInstance(
DateFormat.SHORT, DateFormat.SHORT, ulocale); // fix
}
return stockDateFormatter;
}
private NumberFormat getStockNumberFormatter() {
if (stockNumberFormatter == null) {
stockNumberFormatter = NumberFormat.getInstance(ulocale);
}
return stockNumberFormatter;
}
// *Important*: All fields must be declared *transient*.
// See the longer comment above ulocale.
/**
* Formats the arguments and writes the result into the AppendableWrapper, updates the field
* position.
*
* <p>Exactly one of args and argsMap must be null, the other non-null.
*
* @param msgStart Index to msgPattern part to start formatting from.
* @param pluralNumber null except when formatting a plural argument sub-message where a '#' is
* replaced by the format string for this number.
* @param args The formattable objects array. Non-null iff numbered values are used.
* @param argsMap The key-value map of formattable objects. Non-null iff named values are used.
* @param dest Output parameter to receive the result. The result (string & attributes) is
* appended to existing contents.
* @param fp Field position status.
*/
private void format(
int msgStart,
PluralSelectorContext pluralNumber,
Object[] args,
Map<String, Object> argsMap,
AppendableWrapper dest,
FieldPosition fp) {
String msgString = msgPattern.getPatternString();
int prevIndex = msgPattern.getPart(msgStart).getLimit();
for (int i = msgStart + 1; ; ++i) {
Part part = msgPattern.getPart(i);
Part.Type type = part.getType();
int index = part.getIndex();
dest.append(msgString, prevIndex, index);
if (type == Part.Type.MSG_LIMIT) {
return;
}
prevIndex = part.getLimit();
if (type == Part.Type.REPLACE_NUMBER) {
if (pluralNumber.forReplaceNumber) {
// number-offset was already formatted.
dest.formatAndAppend(
pluralNumber.formatter, pluralNumber.number, pluralNumber.numberString);
} else {
dest.formatAndAppend(getStockNumberFormatter(), pluralNumber.number);
}
continue;
}
if (type != Part.Type.ARG_START) {
continue;
}
int argLimit = msgPattern.getLimitPartIndex(i);
ArgType argType = part.getArgType();
part = msgPattern.getPart(++i);
Object arg;
boolean noArg = false;
Object argId = null;
String argName = msgPattern.getSubstring(part);
if (args != null) {
int argNumber = part.getValue(); // ARG_NUMBER
if (dest.attributes != null) {
// We only need argId if we add it into the attributes.
argId = argNumber;
}
if (0 <= argNumber && argNumber < args.length) {
arg = args[argNumber];
} else {
arg = null;
noArg = true;
}
} else {
argId = argName;
if (argsMap != null) {
arg = argsMap.get(argName);
if (arg == null) {
noArg = !argsMap.containsKey(argName);
}
} else {
arg = null;
noArg = true;
}
}
++i;
int prevDestLength = dest.length;
Format formatter = null;
if (noArg) {
dest.append("{");
dest.append(argName);
dest.append("}");
} else if (arg == null) {
dest.append("null");
} else if (pluralNumber != null && pluralNumber.numberArgIndex == (i - 2)) {
if (pluralNumber.offset == 0) {
// The number was already formatted with this formatter.
dest.formatAndAppend(
pluralNumber.formatter, pluralNumber.number, pluralNumber.numberString);
} else {
// Do not use the formatted (number-offset) string for a named argument
// that formats the number without subtracting the offset.
dest.formatAndAppend(pluralNumber.formatter, arg);
}
} else if (cachedFormatters != null
&& (formatter = cachedFormatters.get(i - 2)) != null) {
// Handles all ArgType.SIMPLE, and formatters from setFormat() and its siblings.
if (formatter instanceof ChoiceFormat
|| formatter instanceof PluralFormat
|| formatter instanceof SelectFormat) {
// We only handle nested formats here if they were provided via setFormat() or
// its siblings.
// Otherwise they are not cached and instead handled below according to argType.
String subMsgString = formatter.format(arg);
if (subMsgString.indexOf('{') >= 0
|| (subMsgString.indexOf('\'') >= 0 && !msgPattern.jdkAposMode())) {
MessageFormat subMsgFormat = new MessageFormat(subMsgString, ulocale);
subMsgFormat.format(0, null, args, argsMap, dest, null);
} else if (dest.attributes == null) {
dest.append(subMsgString);
} else {
// This formats the argument twice, once above to get the subMsgString
// and then once more here.
// It only happens in formatToCharacterIterator()
// on a complex Format set via setFormat(),
// and only when the selected subMsgString does not need further formatting.
// This imitates ICU 4.6 behavior.
dest.formatAndAppend(formatter, arg);
}
} else {
dest.formatAndAppend(formatter, arg);
}
} else if (argType == ArgType.NONE
|| (cachedFormatters != null && cachedFormatters.containsKey(i - 2))) {
// ArgType.NONE, or
// any argument which got reset to null via setFormat() or its siblings.
if (arg instanceof Number) {
dest.formatAndAppend(getStockNumberFormatter(), arg);
} else if (arg instanceof Date) {
dest.formatAndAppend(getStockDateFormatter(), arg);
} else if (arg instanceof Calendar) {
dest.formatAndAppend(getStockDateFormatter(), arg);
} else if (arg instanceof java.util.Calendar) {
dest.formatAndAppend(getStockDateFormatter(), arg);
} else if (arg instanceof Temporal) {
dest.formatAndAppend(getStockDateFormatter(), arg);
} else if (arg instanceof DayOfWeek) {
dest.formatAndAppend(getStockDateFormatter(), arg);
} else if (arg instanceof Month) {
dest.formatAndAppend(getStockDateFormatter(), arg);
} else {
dest.append(arg.toString());
}
} else if (argType == ArgType.CHOICE) {
if (!(arg instanceof Number)) {
throw new IllegalArgumentException("'" + arg + "' is not a Number");
}
double number = ((Number) arg).doubleValue();
int subMsgStart = findChoiceSubMessage(msgPattern, i, number);
formatComplexSubMessage(subMsgStart, null, args, argsMap, dest);
} else if (argType.hasPluralStyle()) {
if (!(arg instanceof Number)) {
throw new IllegalArgumentException("'" + arg + "' is not a Number");
}
PluralSelectorProvider selector;
if (argType == ArgType.PLURAL) {
if (pluralProvider == null) {
pluralProvider = new PluralSelectorProvider(this, PluralType.CARDINAL);
}
selector = pluralProvider;
} else {
if (ordinalProvider == null) {
ordinalProvider = new PluralSelectorProvider(this, PluralType.ORDINAL);
}
selector = ordinalProvider;
}
Number number = (Number) arg;
double offset = msgPattern.getPluralOffset(i);
PluralSelectorContext context =
new PluralSelectorContext(i, argName, number, offset);
int subMsgStart =
PluralFormat.findSubMessage(
msgPattern, i, selector, context, number.doubleValue());
formatComplexSubMessage(subMsgStart, context, args, argsMap, dest);
} else if (argType == ArgType.SELECT) {
int subMsgStart = SelectFormat.findSubMessage(msgPattern, i, arg.toString());
formatComplexSubMessage(subMsgStart, null, args, argsMap, dest);
} else {
// This should never happen.
throw new IllegalStateException("unexpected argType " + argType);
}
fp = updateMetaData(dest, prevDestLength, fp, argId);
prevIndex = msgPattern.getPart(argLimit).getLimit();
i = argLimit;
}
}
private void formatComplexSubMessage(
int msgStart,
PluralSelectorContext pluralNumber,
Object[] args,
Map<String, Object> argsMap,
AppendableWrapper dest) {
if (!msgPattern.jdkAposMode()) {
format(msgStart, pluralNumber, args, argsMap, dest, null);
return;
}
// JDK compatibility mode: (see JDK MessageFormat.format() API docs)
// - remove SKIP_SYNTAX; that is, remove half of the apostrophes
// - if the result string contains an open curly brace '{' then
// instantiate a temporary MessageFormat object and format again;
// otherwise just append the result string
String msgString = msgPattern.getPatternString();
String subMsgString;
StringBuilder sb = null;
int prevIndex = msgPattern.getPart(msgStart).getLimit();
for (int i = msgStart; ; ) {
Part part = msgPattern.getPart(++i);
Part.Type type = part.getType();
int index = part.getIndex();
if (type == Part.Type.MSG_LIMIT) {
if (sb == null) {
subMsgString = msgString.substring(prevIndex, index);
} else {
subMsgString = sb.append(msgString, prevIndex, index).toString();
}
break;
} else if (type == Part.Type.REPLACE_NUMBER || type == Part.Type.SKIP_SYNTAX) {
if (sb == null) {
sb = new StringBuilder();
}
sb.append(msgString, prevIndex, index);
if (type == Part.Type.REPLACE_NUMBER) {
if (pluralNumber.forReplaceNumber) {
// number-offset was already formatted.
sb.append(pluralNumber.numberString);
} else {
sb.append(getStockNumberFormatter().format(pluralNumber.number));
}
}
prevIndex = part.getLimit();
} else if (type == Part.Type.ARG_START) {
if (sb == null) {
sb = new StringBuilder();
}
sb.append(msgString, prevIndex, index);
prevIndex = index;
i = msgPattern.getLimitPartIndex(i);
index = msgPattern.getPart(i).getLimit();
MessagePattern.appendReducedApostrophes(msgString, prevIndex, index, sb);
prevIndex = index;
}
}
if (subMsgString.indexOf('{') >= 0) {
MessageFormat subMsgFormat = new MessageFormat("", ulocale);
subMsgFormat.applyPattern(subMsgString, MessagePattern.ApostropheMode.DOUBLE_REQUIRED);
subMsgFormat.format(0, null, args, argsMap, dest, null);
} else {
dest.append(subMsgString);
}
}
/**
* Read as much literal string from the pattern string as possible. This stops as soon as it
* finds an argument, or it reaches the end of the string.
*
* @param from Index in the pattern string to start from.
* @return A substring from the pattern string representing the longest possible substring with
* no arguments.
*/
private String getLiteralStringUntilNextArgument(int from) {
StringBuilder b = new StringBuilder();
String msgString = msgPattern.getPatternString();
int prevIndex = msgPattern.getPart(from).getLimit();
for (int i = from + 1; ; ++i) {
Part part = msgPattern.getPart(i);
Part.Type type = part.getType();
int index = part.getIndex();
b.append(msgString, prevIndex, index);
if (type == Part.Type.ARG_START || type == Part.Type.MSG_LIMIT) {
return b.toString();
}
assert type == Part.Type.SKIP_SYNTAX || type == Part.Type.INSERT_CHAR
: "Unexpected Part " + part + " in parsed message.";
prevIndex = part.getLimit();
}
}
private FieldPosition updateMetaData(
AppendableWrapper dest, int prevLength, FieldPosition fp, Object argId) {
if (dest.attributes != null && prevLength < dest.length) {
dest.attributes.add(new AttributeAndPosition(argId, prevLength, dest.length));
}
if (fp != null && Field.ARGUMENT.equals(fp.getFieldAttribute())) {
fp.setBeginIndex(prevLength);
fp.setEndIndex(dest.length);
return null;
}
return fp;
}
// This lives here because ICU4J does not have its own ChoiceFormat class.
/**
* Finds the ChoiceFormat sub-message for the given number.
*
* @param pattern A MessagePattern.
* @param partIndex the index of the first ChoiceFormat argument style part.
* @param number a number to be mapped to one of the ChoiceFormat argument's intervals
* @return the sub-message start part index.
*/
private static int findChoiceSubMessage(MessagePattern pattern, int partIndex, double number) {
int count = pattern.countParts();
int msgStart;
// Iterate over (ARG_INT|DOUBLE, ARG_SELECTOR, message) tuples
// until ARG_LIMIT or end of choice-only pattern.
// Ignore the first number and selector and start the loop on the first message.
partIndex += 2;
for (; ; ) {
// Skip but remember the current sub-message.
msgStart = partIndex;
partIndex = pattern.getLimitPartIndex(partIndex);
if (++partIndex >= count) {
// Reached the end of the choice-only pattern.
// Return with the last sub-message.
break;
}
Part part = pattern.getPart(partIndex++);
Part.Type type = part.getType();
if (type == Part.Type.ARG_LIMIT) {
// Reached the end of the ChoiceFormat style.
// Return with the last sub-message.
break;
}
// part is an ARG_INT or ARG_DOUBLE
assert type.hasNumericValue();
double boundary = pattern.getNumericValue(part);
// Fetch the ARG_SELECTOR character.
int selectorIndex = pattern.getPatternIndex(partIndex++);
char boundaryChar = pattern.getPatternString().charAt(selectorIndex);
if (boundaryChar == '<' ? !(number > boundary) : !(number >= boundary)) {
// The number is in the interval between the previous boundary and the current one.
// Return with the sub-message between them.
// The !(a>b) and !(a>=b) comparisons are equivalent to
// (a<=b) and (a<b) except they "catch" NaN.
break;
}
}
return msgStart;
}
// Ported from C++ ChoiceFormat::parse().
private static double parseChoiceArgument(
MessagePattern pattern, int partIndex, String source, ParsePosition pos) {
// find the best number (defined as the one with the longest parse)
int start = pos.getIndex();
int furthest = start;
double bestNumber = Double.NaN;
double tempNumber = 0.0;
while (pattern.getPartType(partIndex) != Part.Type.ARG_LIMIT) {
tempNumber = pattern.getNumericValue(pattern.getPart(partIndex));
partIndex += 2; // skip the numeric part and ignore the ARG_SELECTOR
int msgLimit = pattern.getLimitPartIndex(partIndex);
int len = matchStringUntilLimitPart(pattern, partIndex, msgLimit, source, start);
if (len >= 0) {
int newIndex = start + len;
if (newIndex > furthest) {
furthest = newIndex;
bestNumber = tempNumber;
if (furthest == source.length()) {
break;
}
}
}
partIndex = msgLimit + 1;
}
if (furthest == start) {
pos.setErrorIndex(start);
} else {
pos.setIndex(furthest);
}
return bestNumber;
}
/**
* Matches the pattern string from the end of the partIndex to the beginning of the
* limitPartIndex, including all syntax except SKIP_SYNTAX, against the source string starting
* at sourceOffset. If they match, returns the length of the source string match. Otherwise
* returns -1.
*/
private static int matchStringUntilLimitPart(
MessagePattern pattern,
int partIndex,
int limitPartIndex,
String source,
int sourceOffset) {
int matchingSourceLength = 0;
String msgString = pattern.getPatternString();
int prevIndex = pattern.getPart(partIndex).getLimit();
for (; ; ) {
Part part = pattern.getPart(++partIndex);
if (partIndex == limitPartIndex || part.getType() == Part.Type.SKIP_SYNTAX) {
int index = part.getIndex();
int length = index - prevIndex;
if (length != 0
&& !source.regionMatches(sourceOffset, msgString, prevIndex, length)) {
return -1; // mismatch
}
matchingSourceLength += length;
if (partIndex == limitPartIndex) {
return matchingSourceLength;
}
prevIndex = part.getLimit(); // SKIP_SYNTAX
}
}
}
/**
* Finds the "other" sub-message.
*
* @param partIndex the index of the first PluralFormat argument style part.
* @return the "other" sub-message start part index.
*/
private int findOtherSubMessage(int partIndex) {
int count = msgPattern.countParts();
MessagePattern.Part part = msgPattern.getPart(partIndex);
if (part.getType().hasNumericValue()) {
++partIndex;
}
// Iterate over (ARG_SELECTOR [ARG_INT|ARG_DOUBLE] message) tuples
// until ARG_LIMIT or end of plural-only pattern.
do {
part = msgPattern.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 (msgPattern.partSubstringMatches(part, "other")) {
return partIndex;
}
if (msgPattern.getPartType(partIndex).hasNumericValue()) {
++partIndex; // skip the numeric-value part of "=1" etc.
}
partIndex = msgPattern.getLimitPartIndex(partIndex);
} while (++partIndex < count);
return 0;
}
/**
* Returns the ARG_START index of the first occurrence of the plural number in a sub-message.
* Returns -1 if it is a REPLACE_NUMBER. Returns 0 if there is neither.
*/
private int findFirstPluralNumberArg(int msgStart, String argName) {
for (int i = msgStart + 1; ; ++i) {
Part part = msgPattern.getPart(i);
Part.Type type = part.getType();
if (type == Part.Type.MSG_LIMIT) {
return 0;
}
if (type == Part.Type.REPLACE_NUMBER) {
return -1;
}
if (type == Part.Type.ARG_START) {
ArgType argType = part.getArgType();
if (argName.length() != 0
&& (argType == ArgType.NONE || argType == ArgType.SIMPLE)) {
part = msgPattern.getPart(i + 1); // ARG_NUMBER or ARG_NAME
if (msgPattern.partSubstringMatches(part, argName)) {
return i;
}
}
i = msgPattern.getLimitPartIndex(i);
}
}
}
/**
* Mutable input/output values for the PluralSelectorProvider. Separate so that it is possible
* to make MessageFormat Freezable.
*/
private static final class PluralSelectorContext {
private PluralSelectorContext(int start, String name, Number num, double off) {
startIndex = start;
argName = name;
// number needs to be set even when select() is not called.
// Keep it as a Number/Formattable:
// For format() methods, and to preserve information (e.g., BigDecimal).
if (off == 0) {
number = num;
} else {
number = num.doubleValue() - off;
}
offset = off;
}
@Override
public String toString() {
throw new AssertionError(
"PluralSelectorContext being formatted, rather than its number");
}
// Input values for plural selection with decimals.
int startIndex;
String argName;
/** argument number - plural offset */
Number number;
double offset;
// Output values for plural selection with decimals.
/** -1 if REPLACE_NUMBER, 0 arg not found, >0 ARG_START index */
int numberArgIndex;
Format formatter;
/** formatted argument number - plural offset */
String numberString;
/** true if number-offset was formatted with the stock number formatter */
boolean forReplaceNumber;
}
/**
* This provider helps defer instantiation of a PluralRules object until we actually need to
* select a keyword. For example, if the number matches an explicit-value selector like "=1" we
* do not need any PluralRules.
*/
private static final class PluralSelectorProvider implements PluralFormat.PluralSelector {
public PluralSelectorProvider(MessageFormat mf, PluralType type) {
msgFormat = mf;
this.type = type;
}
@Override
public String select(Object ctx, double number) {
if (rules == null) {
rules = PluralRules.forLocale(msgFormat.ulocale, type);
}
// Select a sub-message according to how the number is formatted,
// which is specified in the selected sub-message.
// We avoid this circle by looking at how
// the number is formatted in the "other" sub-message
// which must always be present and usually contains the number.
// Message authors should be consistent across sub-messages.
PluralSelectorContext context = (PluralSelectorContext) ctx;
int otherIndex = msgFormat.findOtherSubMessage(context.startIndex);
context.numberArgIndex =
msgFormat.findFirstPluralNumberArg(otherIndex, context.argName);
if (context.numberArgIndex > 0 && msgFormat.cachedFormatters != null) {
context.formatter = msgFormat.cachedFormatters.get(context.numberArgIndex);
}
if (context.formatter == null) {
context.formatter = msgFormat.getStockNumberFormatter();
context.forReplaceNumber = true;
}
assert context.number.doubleValue() == number; // argument number minus the offset
context.numberString = context.formatter.format(context.number);
if (context.formatter instanceof DecimalFormat) {
IFixedDecimal dec = ((DecimalFormat) context.formatter).getFixedDecimal(number);
return rules.select(dec);
} else {
return rules.select(number);
}
}
private MessageFormat msgFormat;
private PluralRules rules;
private PluralType type;
}
@SuppressWarnings("unchecked")
private void format(Object arguments, AppendableWrapper result, FieldPosition fp) {
if ((arguments == null || arguments instanceof Map)) {
format(null, (Map<String, Object>) arguments, result, fp);
} else {
format((Object[]) arguments, null, result, fp);
}
}
/**
* Internal routine used by format.
*
* @throws IllegalArgumentException if an argument in the <code>arguments</code> map is not of
* the type expected by the format element(s) that use it.
*/
private void format(
Object[] arguments,
Map<String, Object> argsMap,
AppendableWrapper dest,
FieldPosition fp) {
if (arguments != null && msgPattern.hasNamedArguments()) {
throw new IllegalArgumentException(
"This method is not available in MessageFormat objects "
+ "that use alphanumeric argument names.");
}
format(0, null, arguments, argsMap, dest, fp);
}
private void resetPattern() {
if (msgPattern != null) {
msgPattern.clear();
}
if (cachedFormatters != null) {
cachedFormatters.clear();
}
customFormatArgStarts = null;
}
private static final String[] typeList = {
"number", "date", "time", "spellout", "ordinal", "duration"
};
private static final int TYPE_NUMBER = 0,
TYPE_DATE = 1,
TYPE_TIME = 2,
TYPE_SPELLOUT = 3,
TYPE_ORDINAL = 4,
TYPE_DURATION = 5;
private static final String[] modifierList = {"", "currency", "percent", "integer"};
private static final int MODIFIER_EMPTY = 0,
MODIFIER_CURRENCY = 1,
MODIFIER_PERCENT = 2,
MODIFIER_INTEGER = 3;
private static final String[] dateModifierList = {"", "short", "medium", "long", "full"};
private static final int DATE_MODIFIER_EMPTY = 0,
DATE_MODIFIER_SHORT = 1,
DATE_MODIFIER_MEDIUM = 2,
DATE_MODIFIER_LONG = 3,
DATE_MODIFIER_FULL = 4;
Format dateTimeFormatForPatternOrSkeleton(String style) {
// Ignore leading whitespace when looking for "::", the skeleton signal sequence
int i = PatternProps.skipWhiteSpace(style, 0);
if (style.regionMatches(i, "::", 0, 2)) { // Skeleton
return DateFormat.getInstanceForSkeleton(style.substring(i + 2), ulocale);
} else { // Pattern
return new SimpleDateFormat(style, ulocale);
}
}
// Creates an appropriate Format object for the type and style passed.
// Both arguments cannot be null.
private Format createAppropriateFormat(String type, String style) {
Format newFormat = null;
int subformatType = findKeyword(type, typeList);
switch (subformatType) {
case TYPE_NUMBER:
switch (findKeyword(style, modifierList)) {
case MODIFIER_EMPTY:
newFormat = NumberFormat.getInstance(ulocale);
break;
case MODIFIER_CURRENCY:
newFormat = NumberFormat.getCurrencyInstance(ulocale);
break;
case MODIFIER_PERCENT:
newFormat = NumberFormat.getPercentInstance(ulocale);
break;
case MODIFIER_INTEGER:
newFormat = NumberFormat.getIntegerInstance(ulocale);
break;
default: // pattern or skeleton
// Ignore leading whitespace when looking for "::", the skeleton signal
// sequence
int i = PatternProps.skipWhiteSpace(style, 0);
if (style.regionMatches(i, "::", 0, 2)) {
// Skeleton
newFormat =
NumberFormatter.forSkeleton(style.substring(i + 2))
.locale(ulocale)
.toFormat();
} else {
// Pattern
newFormat = new DecimalFormat(style, new DecimalFormatSymbols(ulocale));
}
break;
}
break;
case TYPE_DATE:
switch (findKeyword(style, dateModifierList)) {
case DATE_MODIFIER_EMPTY:
newFormat = DateFormat.getDateInstance(DateFormat.DEFAULT, ulocale);
break;
case DATE_MODIFIER_SHORT:
newFormat = DateFormat.getDateInstance(DateFormat.SHORT, ulocale);
break;
case DATE_MODIFIER_MEDIUM:
newFormat = DateFormat.getDateInstance(DateFormat.DEFAULT, ulocale);
break;
case DATE_MODIFIER_LONG:
newFormat = DateFormat.getDateInstance(DateFormat.LONG, ulocale);
break;
case DATE_MODIFIER_FULL:
newFormat = DateFormat.getDateInstance(DateFormat.FULL, ulocale);
break;
default: // pattern or skeleton
newFormat = dateTimeFormatForPatternOrSkeleton(style);
break;
}
break;
case TYPE_TIME:
switch (findKeyword(style, dateModifierList)) {
case DATE_MODIFIER_EMPTY:
newFormat = DateFormat.getTimeInstance(DateFormat.DEFAULT, ulocale);
break;
case DATE_MODIFIER_SHORT:
newFormat = DateFormat.getTimeInstance(DateFormat.SHORT, ulocale);
break;
case DATE_MODIFIER_MEDIUM:
newFormat = DateFormat.getTimeInstance(DateFormat.DEFAULT, ulocale);
break;
case DATE_MODIFIER_LONG:
newFormat = DateFormat.getTimeInstance(DateFormat.LONG, ulocale);
break;
case DATE_MODIFIER_FULL:
newFormat = DateFormat.getTimeInstance(DateFormat.FULL, ulocale);
break;
default: // pattern or skeleton
newFormat = dateTimeFormatForPatternOrSkeleton(style);
break;
}
break;
case TYPE_SPELLOUT:
{
RuleBasedNumberFormat rbnf =
new RuleBasedNumberFormat(ulocale, RuleBasedNumberFormat.SPELLOUT);
String ruleset = style.trim();
if (ruleset.length() != 0) {
try {
rbnf.setDefaultRuleSet(ruleset);
} catch (Exception e) {
// warn invalid ruleset
}
}
newFormat = rbnf;
}
break;
case TYPE_ORDINAL:
{
RuleBasedNumberFormat rbnf =
new RuleBasedNumberFormat(ulocale, RuleBasedNumberFormat.ORDINAL);
String ruleset = style.trim();
if (ruleset.length() != 0) {
try {
rbnf.setDefaultRuleSet(ruleset);
} catch (Exception e) {
// warn invalid ruleset
}
}
newFormat = rbnf;
}
break;
case TYPE_DURATION:
{
RuleBasedNumberFormat rbnf =
new RuleBasedNumberFormat(ulocale, RuleBasedNumberFormat.DURATION);
String ruleset = style.trim();
if (ruleset.length() != 0) {
try {
rbnf.setDefaultRuleSet(ruleset);
} catch (Exception e) {
// warn invalid ruleset
}
}
newFormat = rbnf;
}
break;
default:
throw new IllegalArgumentException("Unknown format type \"" + type + "\"");
}
return newFormat;
}
private static final Locale rootLocale = new Locale(""); // Locale.ROOT only @since 1.6
private static final int findKeyword(String s, String[] list) {
s = PatternProps.trimWhiteSpace(s).toLowerCase(rootLocale);
for (int i = 0; i < list.length; ++i) {
if (s.equals(list[i])) return i;
}
return -1;
}
/**
* Custom serialization, new in ICU 4.8. We do not want to use default serialization because we
* only have a small amount of persistent state which is better expressed explicitly rather than
* via writing field objects.
*
* @param out The output stream.
* @serialData Writes the locale as a BCP 47 language tag string, the
* MessagePattern.ApostropheMode as an object, and the pattern string (null if none was
* applied). Followed by an int with the number of (int formatIndex, Object formatter)
* pairs, and that many such pairs, corresponding to previous setFormat() calls for custom
* formats. Followed by an int with the number of (int, Object) pairs, and that many such
* pairs, for future (post-ICU 4.8) extension of the serialization format.
*/
private void writeObject(java.io.ObjectOutputStream out) throws IOException {
out.defaultWriteObject();
// ICU 4.8 custom serialization.
// locale as a BCP 47 language tag
out.writeObject(ulocale.toLanguageTag());
// ApostropheMode
if (msgPattern == null) {
msgPattern = new MessagePattern();
}
out.writeObject(msgPattern.getApostropheMode());
// message pattern string
out.writeObject(msgPattern.getPatternString());
// custom formatters
if (customFormatArgStarts == null || customFormatArgStarts.isEmpty()) {
out.writeInt(0);
} else {
out.writeInt(customFormatArgStarts.size());
int formatIndex = 0;
for (int partIndex = 0; (partIndex = nextTopLevelArgStart(partIndex)) >= 0; ) {
if (customFormatArgStarts.contains(partIndex)) {
out.writeInt(formatIndex);
out.writeObject(cachedFormatters.get(partIndex));
}
++formatIndex;
}
}
// number of future (int, Object) pairs
out.writeInt(0);
}
/**
* Custom deserialization, new in ICU 4.8. See comments on writeObject().
*
* @throws InvalidObjectException if the objects read from the stream is invalid.
*/
private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
// ICU 4.8 custom deserialization.
String languageTag = (String) in.readObject();
ulocale = ULocale.forLanguageTag(languageTag);
MessagePattern.ApostropheMode aposMode = (MessagePattern.ApostropheMode) in.readObject();
if (msgPattern == null || aposMode != msgPattern.getApostropheMode()) {
msgPattern = new MessagePattern(aposMode);
}
String msg = (String) in.readObject();
if (msg != null) {
applyPattern(msg);
}
// custom formatters
for (int numFormatters = in.readInt(); numFormatters > 0; --numFormatters) {
int formatIndex = in.readInt();
Format formatter = (Format) in.readObject();
setFormat(formatIndex, formatter);
}
// skip future (int, Object) pairs
for (int numPairs = in.readInt(); numPairs > 0; --numPairs) {
in.readInt();
in.readObject();
}
}
private void cacheExplicitFormats() {
if (cachedFormatters != null) {
cachedFormatters.clear();
}
customFormatArgStarts = null;
// The last two "parts" can at most be ARG_LIMIT and MSG_LIMIT
// which we need not examine.
int limit = msgPattern.countParts() - 2;
// This loop starts at part index 1 because we do need to examine
// ARG_START parts. (But we can ignore the MSG_START.)
for (int i = 1; i < limit; ++i) {
Part part = msgPattern.getPart(i);
if (part.getType() != Part.Type.ARG_START) {
continue;
}
ArgType argType = part.getArgType();
if (argType != ArgType.SIMPLE) {
continue;
}
int index = i;
i += 2;
String explicitType = msgPattern.getSubstring(msgPattern.getPart(i++));
String style = "";
if ((part = msgPattern.getPart(i)).getType() == MessagePattern.Part.Type.ARG_STYLE) {
style = msgPattern.getSubstring(part);
++i;
}
Format formatter = createAppropriateFormat(explicitType, style);
setArgStartFormat(index, formatter);
}
}
/** Sets a formatter for a MessagePattern ARG_START part index. */
private void setArgStartFormat(int argStart, Format formatter) {
if (cachedFormatters == null) {
cachedFormatters = new HashMap<>();
}
cachedFormatters.put(argStart, formatter);
}
/**
* Sets a custom formatter for a MessagePattern ARG_START part index. "Custom" formatters are
* provided by the user via setFormat() or similar APIs.
*/
private void setCustomArgStartFormat(int argStart, Format formatter) {
setArgStartFormat(argStart, formatter);
if (customFormatArgStarts == null) {
customFormatArgStarts = new HashSet<>();
}
customFormatArgStarts.add(argStart);
}
private static final char SINGLE_QUOTE = '\'';
private static final char CURLY_BRACE_LEFT = '{';
private static final char CURLY_BRACE_RIGHT = '}';
private static final int STATE_INITIAL = 0;
private static final int STATE_SINGLE_QUOTE = 1;
private static final int STATE_IN_QUOTE = 2;
private static final int STATE_MSG_ELEMENT = 3;
/**
* {@icu} Converts an 'apostrophe-friendly' pattern into a standard pattern. <em>This is
* obsolete for ICU 4.8 and higher MessageFormat pattern strings.</em> It can still be useful
* together with {@link java.text.MessageFormat}.
*
* <p>See the class description for more about apostrophes and quoting, and differences between
* ICU and {@link java.text.MessageFormat}.
*
* <p>{@link java.text.MessageFormat} and ICU 4.6 and earlier MessageFormat treat all ASCII
* apostrophes as quotes, which is problematic in some languages, e.g. French, where apostrophe
* is commonly used. This utility assumes that only an unpaired apostrophe immediately before a
* brace is a true quote. Other unpaired apostrophes are paired, and the resulting standard
* pattern string is returned.
*
* <p><b>Note</b>: It is not guaranteed that the returned pattern is indeed a valid pattern. The
* only effect is to convert between patterns having different quoting semantics.
*
* <p><b>Note</b>: This method only works on top-level messageText, not messageText nested
* inside a complexArg.
*
* @param pattern the 'apostrophe-friendly' pattern to convert
* @return the standard equivalent of the original pattern
* @stable ICU 3.4
*/
public static String autoQuoteApostrophe(String pattern) {
StringBuilder buf = new StringBuilder(pattern.length() * 2);
int state = STATE_INITIAL;
int braceCount = 0;
for (int i = 0, j = pattern.length(); i < j; ++i) {
char c = pattern.charAt(i);
switch (state) {
case STATE_INITIAL:
switch (c) {
case SINGLE_QUOTE:
state = STATE_SINGLE_QUOTE;
break;
case CURLY_BRACE_LEFT:
state = STATE_MSG_ELEMENT;
++braceCount;
break;
}
break;
case STATE_SINGLE_QUOTE:
switch (c) {
case SINGLE_QUOTE:
state = STATE_INITIAL;
break;
case CURLY_BRACE_LEFT:
case CURLY_BRACE_RIGHT:
state = STATE_IN_QUOTE;
break;
default:
buf.append(SINGLE_QUOTE);
state = STATE_INITIAL;
break;
}
break;
case STATE_IN_QUOTE:
switch (c) {
case SINGLE_QUOTE:
state = STATE_INITIAL;
break;
}
break;
case STATE_MSG_ELEMENT:
switch (c) {
case CURLY_BRACE_LEFT:
++braceCount;
break;
case CURLY_BRACE_RIGHT:
if (--braceCount == 0) {
state = STATE_INITIAL;
}
break;
}
break;
/// CLOVER:OFF
default: // Never happens.
break;
/// CLOVER:ON
}
buf.append(c);
}
// End of scan
if (state == STATE_SINGLE_QUOTE || state == STATE_IN_QUOTE) {
buf.append(SINGLE_QUOTE);
}
return new String(buf);
}
/**
* Convenience wrapper for Appendable, tracks the result string length. Also, Appendable throws
* IOException, and we turn that into a RuntimeException so that we need no throws clauses.
*/
private static final class AppendableWrapper {
public AppendableWrapper(StringBuilder sb) {
app = sb;
length = sb.length();
attributes = null;
}
public AppendableWrapper(StringBuffer sb) {
app = sb;
length = sb.length();
attributes = null;
}
public void useAttributes() {
attributes = new ArrayList<>();
}
public void append(CharSequence s) {
try {
app.append(s);
length += s.length();
} catch (IOException e) {
throw new ICUUncheckedIOException(e);
}
}
public void append(CharSequence s, int start, int limit) {
try {
app.append(s, start, limit);
length += limit - start;
} catch (IOException e) {
throw new ICUUncheckedIOException(e);
}
}
public void append(CharacterIterator iterator) {
length += append(app, iterator);
}
public static int append(Appendable result, CharacterIterator iterator) {
try {
int start = iterator.getBeginIndex();
int limit = iterator.getEndIndex();
int length = limit - start;
if (start < limit) {
result.append(iterator.first());
while (++start < limit) {
result.append(iterator.next());
}
}
return length;
} catch (IOException e) {
throw new ICUUncheckedIOException(e);
}
}
public void formatAndAppend(Format formatter, Object arg) {
if (attributes == null) {
append(formatter.format(arg));
} else {
AttributedCharacterIterator formattedArg = formatter.formatToCharacterIterator(arg);
int prevLength = length;
append(formattedArg);
// Copy all of the attributes from formattedArg to our attributes list.
formattedArg.first();
int start = formattedArg.getIndex(); // Should be 0 but might not be.
int limit = formattedArg.getEndIndex(); // == start + length - prevLength
int offset = prevLength - start; // Adjust attribute indexes for the result string.
while (start < limit) {
Map<Attribute, Object> map = formattedArg.getAttributes();
int runLimit = formattedArg.getRunLimit();
if (map.size() != 0) {
for (Map.Entry<Attribute, Object> entry : map.entrySet()) {
attributes.add(
new AttributeAndPosition(
entry.getKey(),
entry.getValue(),
offset + start,
offset + runLimit));
}
}
start = runLimit;
formattedArg.setIndex(start);
}
}
}
public void formatAndAppend(Format formatter, Object arg, String argString) {
if (attributes == null && argString != null) {
append(argString);
} else {
formatAndAppend(formatter, arg);
}
}
private Appendable app;
private int length;
private List<AttributeAndPosition> attributes;
}
private static final class AttributeAndPosition {
/** Defaults the field to Field.ARGUMENT. */
public AttributeAndPosition(Object fieldValue, int startIndex, int limitIndex) {
init(Field.ARGUMENT, fieldValue, startIndex, limitIndex);
}
public AttributeAndPosition(
Attribute field, Object fieldValue, int startIndex, int limitIndex) {
init(field, fieldValue, startIndex, limitIndex);
}
public void init(Attribute field, Object fieldValue, int startIndex, int limitIndex) {
key = field;
value = fieldValue;
start = startIndex;
limit = limitIndex;
}
private Attribute key;
private Object value;
private int start;
private int limit;
}
}