public final class

MessagePattern

extends java.lang.Object

implements java.lang.Cloneable, Freezable<MessagePattern>

 java.lang.Object

↳androidx.core.i18n.messageformat_icu.text.MessagePattern

Gradle dependencies

compile group: 'androidx.core', name: 'core-i18n', version: '1.0.0-alpha01'

  • groupId: androidx.core
  • artifactId: core-i18n
  • version: 1.0.0-alpha01

Artifact androidx.core:core-i18n:1.0.0-alpha01 it located at Google repository (https://maven.google.com/)

Overview

Parses and represents ICU MessageFormat patterns. Also handles patterns for ChoiceFormat, PluralFormat and SelectFormat. Used in the implementations of those classes as well as in tools for message validation, translation and format conversion.

The parser handles all syntax relevant for identifying message arguments. This includes "complex" arguments whose style strings contain nested MessageFormat pattern substrings. For "simple" arguments (with no nested MessageFormat pattern substrings), the argument style is not parsed any further.

The parser handles named and numbered message arguments and allows both in one message.

Once a pattern has been parsed successfully, iterate through the parsed data with countParts(), getPart() and related methods.

The data logically represents a parse tree, but is stored and accessed as a list of "parts" for fast and simple parsing and to minimize object allocations. Arguments and nested messages are best handled via recursion. For every _START "part", MessagePattern.getLimitPartIndex(int) efficiently returns the index of the corresponding _LIMIT "part".

List of "parts":

 message = MSG_START (SKIP_SYNTAX | INSERT_CHAR | REPLACE_NUMBER | argument)* MSG_LIMIT
 argument = noneArg | simpleArg | complexArg
 complexArg = choiceArg | pluralArg | selectArg

 noneArg = ARG_START.NONE (ARG_NAME | ARG_NUMBER) ARG_LIMIT.NONE
 simpleArg = ARG_START.SIMPLE (ARG_NAME | ARG_NUMBER) ARG_TYPE [ARG_STYLE] ARG_LIMIT.SIMPLE
 choiceArg = ARG_START.CHOICE (ARG_NAME | ARG_NUMBER) choiceStyle ARG_LIMIT.CHOICE
 pluralArg = ARG_START.PLURAL (ARG_NAME | ARG_NUMBER) pluralStyle ARG_LIMIT.PLURAL
 selectArg = ARG_START.SELECT (ARG_NAME | ARG_NUMBER) selectStyle ARG_LIMIT.SELECT

 choiceStyle = ((ARG_INT | ARG_DOUBLE) ARG_SELECTOR message)+
 pluralStyle = [ARG_INT | ARG_DOUBLE] (ARG_SELECTOR [ARG_INT | ARG_DOUBLE] message)+
 selectStyle = (ARG_SELECTOR message)+
 
  • Literal output text is not represented directly by "parts" but accessed between parts of a message, from one part's getLimit() to the next part's getIndex().
  • ARG_START.CHOICE stands for an ARG_START Part with ArgType CHOICE.
  • In the choiceStyle, the ARG_SELECTOR has the '<', the '#' or the less-than-or-equal-to sign (U+2264).
  • In the pluralStyle, the first, optional numeric Part has the "offset:" value. The optional numeric Part between each (ARG_SELECTOR, message) pair is the value of an explicit-number selector like "=2", otherwise the selector is a non-numeric identifier.
  • The REPLACE_NUMBER Part can occur only in an immediate sub-message of the pluralStyle.

    This class is not intended for public subclassing. icu_annot::stable ICU 4.8

    Summary

    Fields
    public static final intARG_NAME_NOT_NUMBER

    Return value from MessagePattern.validateArgumentName(String) for when the string is a valid "pattern identifier" but not a number.

    public static final intARG_NAME_NOT_VALID

    Return value from MessagePattern.validateArgumentName(String) for when the string is invalid.

    public static final doubleNO_NUMERIC_VALUE

    Special value that is returned by getNumericValue(Part) when no numeric value is defined for a part.

    Constructors
    publicMessagePattern()

    Constructs an empty MessagePattern with default ApostropheMode.

    publicMessagePattern(MessagePattern.ApostropheMode mode)

    Constructs an empty MessagePattern.

    publicMessagePattern(java.lang.String pattern)

    Constructs a MessagePattern with default ApostropheMode and parses the MessageFormat pattern string.

    Methods
    public java.lang.StringautoQuoteApostropheDeep()

    Returns a version of the parsed pattern string where each ASCII apostrophe is doubled (escaped) if it is not already, and if it is not interpreted as quoting syntax.

    public voidclear()

    Clears this MessagePattern.

    public voidclearPatternAndSetApostropheMode(MessagePattern.ApostropheMode mode)

    Clears this MessagePattern and sets the ApostropheMode.

    public java.lang.Objectclone()

    Creates and returns a copy of this object.

    public MessagePatterncloneAsThawed()

    Creates and returns an unfrozen copy of this object.

    public intcountParts()

    Returns the number of "parts" created by parsing the pattern string.

    public booleanequals(java.lang.Object other)

    public MessagePatternfreeze()

    Freezes this object, making it immutable and thread-safe.

    public MessagePattern.ApostropheModegetApostropheMode()

    public intgetLimitPartIndex(int start)

    Returns the index of the ARG|MSG_LIMIT part corresponding to the ARG|MSG_START at start.

    public doublegetNumericValue(MessagePattern.Part part)

    Returns the numeric value associated with an ARG_INT or ARG_DOUBLE.

    public MessagePattern.PartgetPart(int i)

    Gets the i-th pattern "part".

    public MessagePattern.Part.TypegetPartType(int i)

    Returns the Part.Type of the i-th pattern "part".

    public intgetPatternIndex(int partIndex)

    Returns the pattern index of the specified pattern "part".

    public java.lang.StringgetPatternString()

    public doublegetPluralOffset(int pluralStart)

    Returns the "offset:" value of a PluralFormat argument, or 0 if none is specified.

    public java.lang.StringgetSubstring(MessagePattern.Part part)

    Returns the substring of the pattern string indicated by the Part.

    public inthashCode()

    icu_annot::stable ICU 4.8

    public booleanhasNamedArguments()

    Does the parsed pattern have named arguments like {first_name}?

    public booleanhasNumberedArguments()

    Does the parsed pattern have numbered arguments like {2}?

    public booleanisFrozen()

    Determines whether this object is frozen (immutable) or not.

    public booleanjdkAposMode()

    public MessagePatternparse(java.lang.String pattern)

    Parses a MessageFormat pattern string.

    public MessagePatternparseChoiceStyle(java.lang.String pattern)

    Parses a ChoiceFormat pattern string.

    public MessagePatternparsePluralStyle(java.lang.String pattern)

    Parses a PluralFormat pattern string.

    public MessagePatternparseSelectStyle(java.lang.String pattern)

    Parses a SelectFormat pattern string.

    public booleanpartSubstringMatches(MessagePattern.Part part, java.lang.String s)

    Compares the part's substring with the input string s.

    public java.lang.StringtoString()

    icu_annot::stable ICU 4.8

    public static intvalidateArgumentName(java.lang.String name)

    Validates and parses an argument name or argument number string.

    from java.lang.Objectfinalize, getClass, notify, notifyAll, wait, wait, wait

    Fields

    public static final int ARG_NAME_NOT_NUMBER

    Return value from MessagePattern.validateArgumentName(String) for when the string is a valid "pattern identifier" but not a number. icu_annot::stable ICU 4.8

    public static final int ARG_NAME_NOT_VALID

    Return value from MessagePattern.validateArgumentName(String) for when the string is invalid. It might not be a valid "pattern identifier", or it have only ASCII digits but there is a leading zero or the number is too large. icu_annot::stable ICU 4.8

    public static final double NO_NUMERIC_VALUE

    Special value that is returned by getNumericValue(Part) when no numeric value is defined for a part.

    See also: icu_annot::stable ICU 4.8

    Constructors

    public MessagePattern()

    Constructs an empty MessagePattern with default ApostropheMode. icu_annot::stable ICU 4.8

    public MessagePattern(MessagePattern.ApostropheMode mode)

    Constructs an empty MessagePattern.

    Parameters:

    mode: Explicit ApostropheMode. icu_annot::stable ICU 4.8

    public MessagePattern(java.lang.String pattern)

    Constructs a MessagePattern with default ApostropheMode and parses the MessageFormat pattern string.

    Parameters:

    pattern: a MessageFormat pattern string

    Methods

    public MessagePattern parse(java.lang.String pattern)

    Parses a MessageFormat pattern string.

    Parameters:

    pattern: a MessageFormat pattern string

    Returns:

    this

    public MessagePattern parseChoiceStyle(java.lang.String pattern)

    Parses a ChoiceFormat pattern string.

    Parameters:

    pattern: a ChoiceFormat pattern string

    Returns:

    this

    public MessagePattern parsePluralStyle(java.lang.String pattern)

    Parses a PluralFormat pattern string.

    Parameters:

    pattern: a PluralFormat pattern string

    Returns:

    this

    public MessagePattern parseSelectStyle(java.lang.String pattern)

    Parses a SelectFormat pattern string.

    Parameters:

    pattern: a SelectFormat pattern string

    Returns:

    this

    public void clear()

    Clears this MessagePattern. countParts() will return 0. icu_annot::stable ICU 4.8

    public void clearPatternAndSetApostropheMode(MessagePattern.ApostropheMode mode)

    Clears this MessagePattern and sets the ApostropheMode. countParts() will return 0.

    Parameters:

    mode: The new ApostropheMode. icu_annot::stable ICU 4.8

    public boolean equals(java.lang.Object other)

    Parameters:

    other: another object to compare with.

    Returns:

    true if this object is equivalent to the other one. icu_annot::stable ICU 4.8

    public int hashCode()

    icu_annot::stable ICU 4.8

    public MessagePattern.ApostropheMode getApostropheMode()

    Returns:

    this instance's ApostropheMode. icu_annot::stable ICU 4.8

    public boolean jdkAposMode()

    Returns:

    true if getApostropheMode() == ApostropheMode.DOUBLE_REQUIRED icu_annot::internal

    public java.lang.String getPatternString()

    Returns:

    the parsed pattern string (null if none was parsed). icu_annot::stable ICU 4.8

    public boolean hasNamedArguments()

    Does the parsed pattern have named arguments like {first_name}?

    Returns:

    true if the parsed pattern has at least one named argument. icu_annot::stable ICU 4.8

    public boolean hasNumberedArguments()

    Does the parsed pattern have numbered arguments like {2}?

    Returns:

    true if the parsed pattern has at least one numbered argument. icu_annot::stable ICU 4.8

    public java.lang.String toString()

    icu_annot::stable ICU 4.8

    public static int validateArgumentName(java.lang.String name)

    Validates and parses an argument name or argument number string. An argument name must be a "pattern identifier", that is, it must contain no Unicode Pattern_Syntax or Pattern_White_Space characters. If it only contains ASCII digits, then it must be a small integer with no leading zero.

    Parameters:

    name: Input string.

    Returns:

    >=0 if the name is a valid number, ARG_NAME_NOT_NUMBER (-1) if it is a "pattern identifier" but not all ASCII digits, ARG_NAME_NOT_VALID (-2) if it is neither. icu_annot::stable ICU 4.8

    public java.lang.String autoQuoteApostropheDeep()

    Returns a version of the parsed pattern string where each ASCII apostrophe is doubled (escaped) if it is not already, and if it is not interpreted as quoting syntax.

    For example, this turns "I don't '{know}' {gender,select,female{h''er}other{h'im}}." into "I don''t '{know}' {gender,select,female{h''er}other{h''im}}."

    Returns:

    the deep-auto-quoted version of the parsed pattern string.

    See also:

    public int countParts()

    Returns the number of "parts" created by parsing the pattern string. Returns 0 if no pattern has been parsed or clear() was called.

    Returns:

    the number of pattern parts. icu_annot::stable ICU 4.8

    public MessagePattern.Part getPart(int i)

    Gets the i-th pattern "part".

    Parameters:

    i: The index of the Part data. (0..countParts()-1)

    Returns:

    the i-th pattern "part".

    public MessagePattern.Part.Type getPartType(int i)

    Returns the Part.Type of the i-th pattern "part". Convenience method for getPart(i).getType().

    Parameters:

    i: The index of the Part data. (0..countParts()-1)

    Returns:

    The Part.Type of the i-th Part.

    public int getPatternIndex(int partIndex)

    Returns the pattern index of the specified pattern "part". Convenience method for getPart(partIndex).getIndex().

    Parameters:

    partIndex: The index of the Part data. (0..countParts()-1)

    Returns:

    The pattern index of this Part.

    public java.lang.String getSubstring(MessagePattern.Part part)

    Returns the substring of the pattern string indicated by the Part. Convenience method for getPatternString().substring(part.getIndex(), part.getLimit()).

    Parameters:

    part: a part of this MessagePattern.

    Returns:

    the substring associated with part. icu_annot::stable ICU 4.8

    public boolean partSubstringMatches(MessagePattern.Part part, java.lang.String s)

    Compares the part's substring with the input string s.

    Parameters:

    part: a part of this MessagePattern.
    s: a string.

    Returns:

    true if getSubstring(part).equals(s). icu_annot::stable ICU 4.8

    public double getNumericValue(MessagePattern.Part part)

    Returns the numeric value associated with an ARG_INT or ARG_DOUBLE.

    Parameters:

    part: a part of this MessagePattern.

    Returns:

    the part's numeric value, or NO_NUMERIC_VALUE if this is not a numeric part. icu_annot::stable ICU 4.8

    public double getPluralOffset(int pluralStart)

    Returns the "offset:" value of a PluralFormat argument, or 0 if none is specified.

    Parameters:

    pluralStart: the index of the first PluralFormat argument style part. (0..countParts()-1)

    Returns:

    the "offset:" value.

    public int getLimitPartIndex(int start)

    Returns the index of the ARG|MSG_LIMIT part corresponding to the ARG|MSG_START at start.

    Parameters:

    start: The index of some Part data (0..countParts()-1); this Part should be of Type ARG_START or MSG_START.

    Returns:

    The first i>start where getPart(i).getType()==ARG|MSG_LIMIT at the same nesting level, or start itself if getPartType(msgStart)!=ARG|MSG_START.

    public java.lang.Object clone()

    Creates and returns a copy of this object.

    Returns:

    a copy of this object (or itself if frozen). icu_annot::stable ICU 4.8

    public MessagePattern cloneAsThawed()

    Creates and returns an unfrozen copy of this object.

    Returns:

    a copy of this object. icu_annot::stable ICU 4.8

    public MessagePattern freeze()

    Freezes this object, making it immutable and thread-safe.

    Returns:

    this icu_annot::stable ICU 4.8

    public boolean isFrozen()

    Determines whether this object is frozen (immutable) or not.

    Returns:

    true if this object is frozen. icu_annot::stable ICU 4.8

    Source

    /*
     *******************************************************************************
     *   Copyright (C) 2010-2014, International Business Machines
     *   Corporation and others.  All Rights Reserved.
     *******************************************************************************
     *   created on: 2010aug21
     *   created by: Markus W. Scherer
     */
    
    package androidx.core.i18n.messageformat_icu.text;
    
    import androidx.annotation.RestrictTo;
    import androidx.core.i18n.messageformat_icu.impl.PatternProps;
    import androidx.core.i18n.messageformat_icu.util.Freezable;
    import androidx.core.i18n.messageformat_icu.util.ICUCloneNotSupportedException;
    
    import java.util.ArrayList;
    import java.util.Locale;
    
    import androidx.core.i18n.messageformat_icu.impl.ICUConfig;
    
    //Note: Minimize ICU dependencies, only use a very small part of the ICU core.
    //In particular, do not depend on *Format classes.
    
    /**
     * Parses and represents ICU MessageFormat patterns.
     * Also handles patterns for ChoiceFormat, PluralFormat and SelectFormat.
     * Used in the implementations of those classes as well as in tools
     * for message validation, translation and format conversion.
     * <p>
     * The parser handles all syntax relevant for identifying message arguments.
     * This includes "complex" arguments whose style strings contain
     * nested MessageFormat pattern substrings.
     * For "simple" arguments (with no nested MessageFormat pattern substrings),
     * the argument style is not parsed any further.
     * <p>
     * The parser handles named and numbered message arguments and allows both in one message.
     * <p>
     * Once a pattern has been parsed successfully, iterate through the parsed data
     * with countParts(), getPart() and related methods.
     * <p>
     * The data logically represents a parse tree, but is stored and accessed
     * as a list of "parts" for fast and simple parsing and to minimize object allocations.
     * Arguments and nested messages are best handled via recursion.
     * For every _START "part", {@link #getLimitPartIndex(int)} efficiently returns
     * the index of the corresponding _LIMIT "part".
     * <p>
     * List of "parts":
     * <pre>
     * message = MSG_START (SKIP_SYNTAX | INSERT_CHAR | REPLACE_NUMBER | argument)* MSG_LIMIT
     * argument = noneArg | simpleArg | complexArg
     * complexArg = choiceArg | pluralArg | selectArg
     *
     * noneArg = ARG_START.NONE (ARG_NAME | ARG_NUMBER) ARG_LIMIT.NONE
     * simpleArg = ARG_START.SIMPLE (ARG_NAME | ARG_NUMBER) ARG_TYPE [ARG_STYLE] ARG_LIMIT.SIMPLE
     * choiceArg = ARG_START.CHOICE (ARG_NAME | ARG_NUMBER) choiceStyle ARG_LIMIT.CHOICE
     * pluralArg = ARG_START.PLURAL (ARG_NAME | ARG_NUMBER) pluralStyle ARG_LIMIT.PLURAL
     * selectArg = ARG_START.SELECT (ARG_NAME | ARG_NUMBER) selectStyle ARG_LIMIT.SELECT
     *
     * choiceStyle = ((ARG_INT | ARG_DOUBLE) ARG_SELECTOR message)+
     * pluralStyle = [ARG_INT | ARG_DOUBLE] (ARG_SELECTOR [ARG_INT | ARG_DOUBLE] message)+
     * selectStyle = (ARG_SELECTOR message)+
     * </pre>
     * <ul>
     *   <li>Literal output text is not represented directly by "parts" but accessed
     *       between parts of a message, from one part's getLimit() to the next part's getIndex().
     *   <li><code>ARG_START.CHOICE</code> stands for an ARG_START Part with ArgType CHOICE.
     *   <li>In the choiceStyle, the ARG_SELECTOR has the '<', the '#' or
     *       the less-than-or-equal-to sign (U+2264).
     *   <li>In the pluralStyle, the first, optional numeric Part has the "offset:" value.
     *       The optional numeric Part between each (ARG_SELECTOR, message) pair
     *       is the value of an explicit-number selector like "=2",
     *       otherwise the selector is a non-numeric identifier.
     *   <li>The REPLACE_NUMBER Part can occur only in an immediate sub-message of the pluralStyle.
     * <p>
     * This class is not intended for public subclassing.
     *
     * icu_annot::stable ICU 4.8
     * @author Markus Scherer
     */
    @RestrictTo(RestrictTo.Scope.LIBRARY)
    public final class MessagePattern implements Cloneable, Freezable<MessagePattern> {
        /**
         * Mode for when an apostrophe starts quoted literal text for MessageFormat output.
         * The default is DOUBLE_OPTIONAL unless overridden via ICUConfig
         * (/com/ibm/icu/ICUConfig.properties).
         * <p>
         * A pair of adjacent apostrophes always results in a single apostrophe in the output,
         * even when the pair is between two single, text-quoting apostrophes.
         * <p>
         * The following table shows examples of desired MessageFormat.format() output
         * with the pattern strings that yield that output.
         * <p>
         * <table>
         *   <tr>
         *     <th>Desired output</th>
         *     <th>DOUBLE_OPTIONAL</th>
         *     <th>DOUBLE_REQUIRED</th>
         *   </tr>
         *   <tr>
         *     <td>I see {many}</td>
         *     <td>I see '{many}'</td>
         *     <td>(same)</td>
         *   </tr>
         *   <tr>
         *     <td>I said {'Wow!'}</td>
         *     <td>I said '{''Wow!''}'</td>
         *     <td>(same)</td>
         *   </tr>
         *   <tr>
         *     <td>I don't know</td>
         *     <td>I don't know OR<br> I don''t know</td>
         *     <td>I don''t know</td>
         *   </tr>
         * </table>
         * icu_annot::stable ICU 4.8
         */
        public enum ApostropheMode {
            /**
             * A literal apostrophe is represented by
             * either a single or a double apostrophe pattern character.
             * Within a MessageFormat pattern, a single apostrophe only starts quoted literal text
             * if it immediately precedes a curly brace {},
             * or a pipe symbol | if inside a choice format,
             * or a pound symbol # if inside a plural format.
             * <p>
             * This is the default behavior starting with ICU 4.8.
             * icu_annot::stable ICU 4.8
             */
            DOUBLE_OPTIONAL,
            /**
             * A literal apostrophe must be represented by
             * a double apostrophe pattern character.
             * A single apostrophe always starts quoted literal text.
             * <p>
             * This is the behavior of ICU 4.6 and earlier, and of the JDK.
             * icu_annot::stable ICU 4.8
             */
            DOUBLE_REQUIRED
        }
    
        /**
         * Constructs an empty MessagePattern with default ApostropheMode.
         * icu_annot::stable ICU 4.8
         */
        public MessagePattern() {
            aposMode=defaultAposMode;
        }
    
        /**
         * Constructs an empty MessagePattern.
         * @param mode Explicit ApostropheMode.
         * icu_annot::stable ICU 4.8
         */
        public MessagePattern(ApostropheMode mode) {
            aposMode=mode;
        }
    
        /**
         * Constructs a MessagePattern with default ApostropheMode and
         * parses the MessageFormat pattern string.
         * @param pattern a MessageFormat pattern string
         * @throws IllegalArgumentException for syntax errors in the pattern string
         * @throws IndexOutOfBoundsException if certain limits are exceeded
         *         (e.g., argument number too high, argument name too long, etc.)
         * @throws NumberFormatException if a number could not be parsed
         * icu_annot::stable ICU 4.8
         */
        public MessagePattern(String pattern) {
            aposMode=defaultAposMode;
            parse(pattern);
        }
    
        /**
         * Parses a MessageFormat pattern string.
         * @param pattern a MessageFormat pattern string
         * @return this
         * @throws IllegalArgumentException for syntax errors in the pattern string
         * @throws IndexOutOfBoundsException if certain limits are exceeded
         *         (e.g., argument number too high, argument name too long, etc.)
         * @throws NumberFormatException if a number could not be parsed
         * icu_annot::stable ICU 4.8
         */
        public MessagePattern parse(String pattern) {
            preParse(pattern);
            parseMessage(0, 0, 0, ArgType.NONE);
            postParse();
            return this;
        }
    
        /**
         * Parses a ChoiceFormat pattern string.
         * @param pattern a ChoiceFormat pattern string
         * @return this
         * @throws IllegalArgumentException for syntax errors in the pattern string
         * @throws IndexOutOfBoundsException if certain limits are exceeded
         *         (e.g., argument number too high, argument name too long, etc.)
         * @throws NumberFormatException if a number could not be parsed
         * icu_annot::stable ICU 4.8
         */
        public MessagePattern parseChoiceStyle(String pattern) {
            preParse(pattern);
            parseChoiceStyle(0, 0);
            postParse();
            return this;
        }
    
        /**
         * Parses a PluralFormat pattern string.
         * @param pattern a PluralFormat pattern string
         * @return this
         * @throws IllegalArgumentException for syntax errors in the pattern string
         * @throws IndexOutOfBoundsException if certain limits are exceeded
         *         (e.g., argument number too high, argument name too long, etc.)
         * @throws NumberFormatException if a number could not be parsed
         * icu_annot::stable ICU 4.8
         */
        public MessagePattern parsePluralStyle(String pattern) {
            preParse(pattern);
            parsePluralOrSelectStyle(ArgType.PLURAL, 0, 0);
            postParse();
            return this;
        }
    
        /**
         * Parses a SelectFormat pattern string.
         * @param pattern a SelectFormat pattern string
         * @return this
         * @throws IllegalArgumentException for syntax errors in the pattern string
         * @throws IndexOutOfBoundsException if certain limits are exceeded
         *         (e.g., argument number too high, argument name too long, etc.)
         * @throws NumberFormatException if a number could not be parsed
         * icu_annot::stable ICU 4.8
         */
        public MessagePattern parseSelectStyle(String pattern) {
            preParse(pattern);
            parsePluralOrSelectStyle(ArgType.SELECT, 0, 0);
            postParse();
            return this;
        }
    
        /**
         * Clears this MessagePattern.
         * countParts() will return 0.
         * icu_annot::stable ICU 4.8
         */
        public void clear() {
            // Mostly the same as preParse().
            if(isFrozen()) {
                throw new UnsupportedOperationException(
                    "Attempt to clear() a frozen MessagePattern instance.");
            }
            msg=null;
            hasArgNames=hasArgNumbers=false;
            needsAutoQuoting=false;
            parts.clear();
            if(numericValues!=null) {
                numericValues.clear();
            }
        }
    
        /**
         * Clears this MessagePattern and sets the ApostropheMode.
         * countParts() will return 0.
         * @param mode The new ApostropheMode.
         * icu_annot::stable ICU 4.8
         */
        public void clearPatternAndSetApostropheMode(ApostropheMode mode) {
            clear();
            aposMode=mode;
        }
    
        /**
         * @param other another object to compare with.
         * @return true if this object is equivalent to the other one.
         * icu_annot::stable ICU 4.8
         */
        @Override
        public boolean equals(Object other) {
            if(this==other) {
                return true;
            }
            if(other==null || getClass()!=other.getClass()) {
                return false;
            }
            MessagePattern o=(MessagePattern)other;
            return
                aposMode.equals(o.aposMode) &&
                (msg==null ? o.msg==null : msg.equals(o.msg)) &&
                parts.equals(o.parts);
            // No need to compare numericValues if msg and parts are the same.
        }
    
        /**
         * {@inheritDoc}
         * icu_annot::stable ICU 4.8
         */
        @Override
        public int hashCode() {
            return (aposMode.hashCode()*37+(msg!=null ? msg.hashCode() : 0))*37+parts.hashCode();
        }
    
        /**
         * @return this instance's ApostropheMode.
         * icu_annot::stable ICU 4.8
         */
        public ApostropheMode getApostropheMode() {
            return aposMode;
        }
    
        /**
         * @return true if getApostropheMode() == ApostropheMode.DOUBLE_REQUIRED
         * icu_annot::internal
         */
        public boolean jdkAposMode() {
            return aposMode == ApostropheMode.DOUBLE_REQUIRED;
        }
    
        /**
         * @return the parsed pattern string (null if none was parsed).
         * icu_annot::stable ICU 4.8
         */
        public String getPatternString() {
            return msg;
        }
    
        /**
         * Does the parsed pattern have named arguments like {first_name}?
         * @return true if the parsed pattern has at least one named argument.
         * icu_annot::stable ICU 4.8
         */
        public boolean hasNamedArguments() {
            return hasArgNames;
        }
    
        /**
         * Does the parsed pattern have numbered arguments like {2}?
         * @return true if the parsed pattern has at least one numbered argument.
         * icu_annot::stable ICU 4.8
         */
        public boolean hasNumberedArguments() {
            return hasArgNumbers;
        }
    
        /**
         * {@inheritDoc}
         * icu_annot::stable ICU 4.8
         */
        @Override
        public String toString() {
            return msg;
        }
    
        /**
         * Validates and parses an argument name or argument number string.
         * An argument name must be a "pattern identifier", that is, it must contain
         * no Unicode Pattern_Syntax or Pattern_White_Space characters.
         * If it only contains ASCII digits, then it must be a small integer with no leading zero.
         * @param name Input string.
         * @return &gt;=0 if the name is a valid number,
         *         ARG_NAME_NOT_NUMBER (-1) if it is a "pattern identifier" but not all ASCII digits,
         *         ARG_NAME_NOT_VALID (-2) if it is neither.
         * icu_annot::stable ICU 4.8
         */
        public static int validateArgumentName(String name) {
            if(!PatternProps.isIdentifier(name)) {
                return ARG_NAME_NOT_VALID;
            }
            return parseArgNumber(name, 0, name.length());
        }
    
        /**
         * Return value from {@link #validateArgumentName(String)} for when
         * the string is a valid "pattern identifier" but not a number.
         * icu_annot::stable ICU 4.8
         */
        public static final int ARG_NAME_NOT_NUMBER=-1;
    
        /**
         * Return value from {@link #validateArgumentName(String)} for when
         * the string is invalid.
         * It might not be a valid "pattern identifier",
         * or it have only ASCII digits but there is a leading zero or the number is too large.
         * icu_annot::stable ICU 4.8
         */
        public static final int ARG_NAME_NOT_VALID=-2;
    
        /**
         * Returns a version of the parsed pattern string where each ASCII apostrophe
         * is doubled (escaped) if it is not already, and if it is not interpreted as quoting syntax.
         * <p>
         * For example, this turns "I don't '{know}' {gender,select,female{h''er}other{h'im}}."
         * into "I don''t '{know}' {gender,select,female{h''er}other{h''im}}."
         * @return the deep-auto-quoted version of the parsed pattern string.
         * @see MessageFormat#autoQuoteApostrophe(String)
         * icu_annot::stable ICU 4.8
         */
        public String autoQuoteApostropheDeep() {
            if(!needsAutoQuoting) {
                return msg;
            }
            StringBuilder modified=null;
            // Iterate backward so that the insertion indexes do not change.
            int count=countParts();
            for(int i=count; i>0;) {
                Part part;
                if((part=getPart(--i)).getType()==Part.Type.INSERT_CHAR) {
                    if(modified==null) {
                        modified=new StringBuilder(msg.length()+10).append(msg);
                    }
                    modified.insert(part.index, (char)part.value);
                }
            }
            if(modified==null) {
                return msg;
            } else {
                return modified.toString();
            }
        }
    
        /**
         * Returns the number of "parts" created by parsing the pattern string.
         * Returns 0 if no pattern has been parsed or clear() was called.
         * @return the number of pattern parts.
         * icu_annot::stable ICU 4.8
         */
        public int countParts() {
            return parts.size();
        }
    
        /**
         * Gets the i-th pattern "part".
         * @param i The index of the Part data. (0..countParts()-1)
         * @return the i-th pattern "part".
         * @throws IndexOutOfBoundsException if i is outside the (0..countParts()-1) range
         * icu_annot::stable ICU 4.8
         */
        public Part getPart(int i) {
            return parts.get(i);
        }
    
        /**
         * Returns the Part.Type of the i-th pattern "part".
         * Convenience method for getPart(i).getType().
         * @param i The index of the Part data. (0..countParts()-1)
         * @return The Part.Type of the i-th Part.
         * @throws IndexOutOfBoundsException if i is outside the (0..countParts()-1) range
         * icu_annot::stable ICU 4.8
         */
        public Part.Type getPartType(int i) {
            return parts.get(i).type;
        }
    
        /**
         * Returns the pattern index of the specified pattern "part".
         * Convenience method for getPart(partIndex).getIndex().
         * @param partIndex The index of the Part data. (0..countParts()-1)
         * @return The pattern index of this Part.
         * @throws IndexOutOfBoundsException if partIndex is outside the (0..countParts()-1) range
         * icu_annot::stable ICU 4.8
         */
        public int getPatternIndex(int partIndex) {
            return parts.get(partIndex).index;
        }
    
        /**
         * Returns the substring of the pattern string indicated by the Part.
         * Convenience method for getPatternString().substring(part.getIndex(), part.getLimit()).
         * @param part a part of this MessagePattern.
         * @return the substring associated with part.
         * icu_annot::stable ICU 4.8
         */
        public String getSubstring(Part part) {
            int index=part.index;
            return msg.substring(index, index+part.length);
        }
    
        /**
         * Compares the part's substring with the input string s.
         * @param part a part of this MessagePattern.
         * @param s a string.
         * @return true if getSubstring(part).equals(s).
         * icu_annot::stable ICU 4.8
         */
        public boolean partSubstringMatches(Part part, String s) {
            return msg.regionMatches(part.index, s, 0, part.length);
        }
    
        /**
         * Returns the numeric value associated with an ARG_INT or ARG_DOUBLE.
         * @param part a part of this MessagePattern.
         * @return the part's numeric value, or NO_NUMERIC_VALUE if this is not a numeric part.
         * icu_annot::stable ICU 4.8
         */
        public double getNumericValue(Part part) {
            Part.Type type=part.type;
            if(type==Part.Type.ARG_INT) {
                return part.value;
            } else if(type==Part.Type.ARG_DOUBLE) {
                return numericValues.get(part.value);
            } else {
                return NO_NUMERIC_VALUE;
            }
        }
    
        /**
         * Special value that is returned by getNumericValue(Part) when no
         * numeric value is defined for a part.
         * @see #getNumericValue
         * icu_annot::stable ICU 4.8
         */
        public static final double NO_NUMERIC_VALUE=-123456789;
    
        /**
         * Returns the "offset:" value of a PluralFormat argument, or 0 if none is specified.
         * @param pluralStart the index of the first PluralFormat argument style part. (0..countParts()-1)
         * @return the "offset:" value.
         * @throws IndexOutOfBoundsException if pluralStart is outside the (0..countParts()-1) range
         * icu_annot::stable ICU 4.8
         */
        public double getPluralOffset(int pluralStart) {
            Part part=parts.get(pluralStart);
            if(part.type.hasNumericValue()) {
                return getNumericValue(part);
            } else {
                return 0;
            }
        }
    
        /**
         * Returns the index of the ARG|MSG_LIMIT part corresponding to the ARG|MSG_START at start.
         * @param start The index of some Part data (0..countParts()-1);
         *        this Part should be of Type ARG_START or MSG_START.
         * @return The first i>start where getPart(i).getType()==ARG|MSG_LIMIT at the same nesting level,
         *         or start itself if getPartType(msgStart)!=ARG|MSG_START.
         * @throws IndexOutOfBoundsException if start is outside the (0..countParts()-1) range
         * icu_annot::stable ICU 4.8
         */
        public int getLimitPartIndex(int start) {
            int limit=parts.get(start).limitPartIndex;
            if(limit<start) {
                return start;
            }
            return limit;
        }
    
        /**
         * A message pattern "part", representing a pattern parsing event.
         * There is a part for the start and end of a message or argument,
         * for quoting and escaping of and with ASCII apostrophes,
         * and for syntax elements of "complex" arguments.
         * icu_annot::stable ICU 4.8
         */
        public static final class Part {
            private Part(Type t, int i, int l, int v) {
                type=t;
                index=i;
                length=(char)l;
                value=(short)v;
            }
    
            /**
             * Returns the type of this part.
             * @return the part type.
             * icu_annot::stable ICU 4.8
             */
            public Type getType() {
                return type;
            }
    
            /**
             * Returns the pattern string index associated with this Part.
             * @return this part's pattern string index.
             * icu_annot::stable ICU 4.8
             */
            public int getIndex() {
                return index;
            }
    
            /**
             * Returns the length of the pattern substring associated with this Part.
             * This is 0 for some parts.
             * @return this part's pattern substring length.
             * icu_annot::stable ICU 4.8
             */
            public int getLength() {
                return length;
            }
    
            /**
             * Returns the pattern string limit (exclusive-end) index associated with this Part.
             * Convenience method for getIndex()+getLength().
             * @return this part's pattern string limit index, same as getIndex()+getLength().
             * icu_annot::stable ICU 4.8
             */
            public int getLimit() {
                return index+length;
            }
    
            /**
             * Returns a value associated with this part.
             * See the documentation of each part type for details.
             * @return the part value.
             * icu_annot::stable ICU 4.8
             */
            public int getValue() {
                return value;
            }
    
            /**
             * Returns the argument type if this part is of type ARG_START or ARG_LIMIT,
             * otherwise ArgType.NONE.
             * @return the argument type for this part.
             * icu_annot::stable ICU 4.8
             */
            public ArgType getArgType() {
                Type type=getType();
                if(type==Type.ARG_START || type==Type.ARG_LIMIT) {
                    return argTypes[value];
                } else {
                    return ArgType.NONE;
                }
            }
    
            /**
             * Part type constants.
             * icu_annot::stable ICU 4.8
             */
            public enum Type {
                /**
                 * Start of a message pattern (main or nested).
                 * The length is 0 for the top-level message
                 * and for a choice argument sub-message, otherwise 1 for the '{'.
                 * The value indicates the nesting level, starting with 0 for the main message.
                 * <p>
                 * There is always a later MSG_LIMIT part.
                 * icu_annot::stable ICU 4.8
                 */
                MSG_START,
                /**
                 * End of a message pattern (main or nested).
                 * The length is 0 for the top-level message and
                 * the last sub-message of a choice argument,
                 * otherwise 1 for the '}' or (in a choice argument style) the '|'.
                 * The value indicates the nesting level, starting with 0 for the main message.
                 * icu_annot::stable ICU 4.8
                 */
                MSG_LIMIT,
                /**
                 * Indicates a substring of the pattern string which is to be skipped when formatting.
                 * For example, an apostrophe that begins or ends quoted text
                 * would be indicated with such a part.
                 * The value is undefined and currently always 0.
                 * icu_annot::stable ICU 4.8
                 */
                SKIP_SYNTAX,
                /**
                 * Indicates that a syntax character needs to be inserted for auto-quoting.
                 * The length is 0.
                 * The value is the character code of the insertion character. (U+0027=APOSTROPHE)
                 * icu_annot::stable ICU 4.8
                 */
                INSERT_CHAR,
                /**
                 * Indicates a syntactic (non-escaped) # symbol in a plural variant.
                 * When formatting, replace this part's substring with the
                 * (value-offset) for the plural argument value.
                 * The value is undefined and currently always 0.
                 * icu_annot::stable ICU 4.8
                 */
                REPLACE_NUMBER,
                /**
                 * Start of an argument.
                 * The length is 1 for the '{'.
                 * The value is the ordinal value of the ArgType. Use getArgType().
                 * <p>
                 * This part is followed by either an ARG_NUMBER or ARG_NAME,
                 * followed by optional argument sub-parts (see ArgType constants)
                 * and finally an ARG_LIMIT part.
                 * icu_annot::stable ICU 4.8
                 */
                ARG_START,
                /**
                 * End of an argument.
                 * The length is 1 for the '}'.
                 * The value is the ordinal value of the ArgType. Use getArgType().
                 * icu_annot::stable ICU 4.8
                 */
                ARG_LIMIT,
                /**
                 * The argument number, provided by the value.
                 * icu_annot::stable ICU 4.8
                 */
                ARG_NUMBER,
                /**
                 * The argument name.
                 * The value is undefined and currently always 0.
                 * icu_annot::stable ICU 4.8
                 */
                ARG_NAME,
                /**
                 * The argument type.
                 * The value is undefined and currently always 0.
                 * icu_annot::stable ICU 4.8
                 */
                ARG_TYPE,
                /**
                 * The argument style text.
                 * The value is undefined and currently always 0.
                 * icu_annot::stable ICU 4.8
                 */
                ARG_STYLE,
                /**
                 * A selector substring in a "complex" argument style.
                 * The value is undefined and currently always 0.
                 * icu_annot::stable ICU 4.8
                 */
                ARG_SELECTOR,
                /**
                 * An integer value, for example the offset or an explicit selector value
                 * in a PluralFormat style.
                 * The part value is the integer value.
                 * icu_annot::stable ICU 4.8
                 */
                ARG_INT,
                /**
                 * A numeric value, for example the offset or an explicit selector value
                 * in a PluralFormat style.
                 * The part value is an index into an internal array of numeric values;
                 * use getNumericValue().
                 * icu_annot::stable ICU 4.8
                 */
                ARG_DOUBLE;
    
                /**
                 * Indicates whether this part has a numeric value.
                 * If so, then that numeric value can be retrieved via {@link MessagePattern#getNumericValue(Part)}.
                 * @return true if this part has a numeric value.
                 * icu_annot::stable ICU 4.8
                 */
                public boolean hasNumericValue() {
                    return this==ARG_INT || this==ARG_DOUBLE;
                }
            }
    
            /**
             * @return a string representation of this part.
             * icu_annot::stable ICU 4.8
             */
            @Override
            public String toString() {
                String valueString=(type==Type.ARG_START || type==Type.ARG_LIMIT) ?
                    getArgType().name() : Integer.toString(value);
                return type.name()+"("+valueString+")@"+index;
            }
    
            /**
             * @param other another object to compare with.
             * @return true if this object is equivalent to the other one.
             * icu_annot::stable ICU 4.8
             */
            @Override
            public boolean equals(Object other) {
                if(this==other) {
                    return true;
                }
                if(other==null || getClass()!=other.getClass()) {
                    return false;
                }
                Part o=(Part)other;
                return
                    type.equals(o.type) &&
                    index==o.index &&
                    length==o.length &&
                    value==o.value &&
                    limitPartIndex==o.limitPartIndex;
            }
    
            /**
             * {@inheritDoc}
             * icu_annot::stable ICU 4.8
             */
            @Override
            public int hashCode() {
                return ((type.hashCode()*37+index)*37+length)*37+value;
            }
    
            private static final int MAX_LENGTH=0xffff;
            private static final int MAX_VALUE=Short.MAX_VALUE;
    
            // Some fields are not final because they are modified during pattern parsing.
            // After pattern parsing, the parts are effectively immutable.
            private final Type type;
            private final int index;
            private final char length;
            private short value;
            private int limitPartIndex;
        }
    
        /**
         * Argument type constants.
         * Returned by Part.getArgType() for ARG_START and ARG_LIMIT parts.
         *
         * Messages nested inside an argument are each delimited by MSG_START and MSG_LIMIT,
         * with a nesting level one greater than the surrounding message.
         * icu_annot::stable ICU 4.8
         */
        public enum ArgType {
            /**
             * The argument has no specified type.
             * icu_annot::stable ICU 4.8
             */
            NONE,
            /**
             * The argument has a "simple" type which is provided by the ARG_TYPE part.
             * An ARG_STYLE part might follow that.
             * icu_annot::stable ICU 4.8
             */
            SIMPLE,
            /**
             * The argument is a ChoiceFormat with one or more
             * ((ARG_INT | ARG_DOUBLE), ARG_SELECTOR, message) tuples.
             * icu_annot::stable ICU 4.8
             */
            CHOICE,
            /**
             * The argument is a cardinal-number PluralFormat with an optional ARG_INT or ARG_DOUBLE offset
             * (e.g., offset:1)
             * and one or more (ARG_SELECTOR [explicit-value] message) tuples.
             * If the selector has an explicit value (e.g., =2), then
             * that value is provided by the ARG_INT or ARG_DOUBLE part preceding the message.
             * Otherwise the message immediately follows the ARG_SELECTOR.
             * icu_annot::stable ICU 4.8
             */
            PLURAL,
            /**
             * The argument is a SelectFormat with one or more (ARG_SELECTOR, message) pairs.
             * icu_annot::stable ICU 4.8
             */
            SELECT,
            /**
             * The argument is an ordinal-number PluralFormat
             * with the same style parts sequence and semantics as {@link ArgType#PLURAL}.
             * icu_annot::stable ICU 50
             */
            SELECTORDINAL;
    
            /**
             * @return true if the argument type has a plural style part sequence and semantics,
             * for example {@link ArgType#PLURAL} and {@link ArgType#SELECTORDINAL}.
             * icu_annot::stable ICU 50
             */
            public boolean hasPluralStyle() {
                return this == PLURAL || this == SELECTORDINAL;
            }
        }
    
        /**
         * Creates and returns a copy of this object.
         * @return a copy of this object (or itself if frozen).
         * icu_annot::stable ICU 4.8
         */
        @Override
        public Object clone() {
            if(isFrozen()) {
                return this;
            } else {
                return cloneAsThawed();
            }
        }
    
        /**
         * Creates and returns an unfrozen copy of this object.
         * @return a copy of this object.
         * icu_annot::stable ICU 4.8
         */
        @SuppressWarnings("unchecked")
        @Override
        public MessagePattern cloneAsThawed() {
            MessagePattern newMsg;
            try {
                newMsg=(MessagePattern)super.clone();
            } catch (CloneNotSupportedException e) {
                throw new ICUCloneNotSupportedException(e);
            }
            newMsg.parts=(ArrayList<Part>)parts.clone();
            if(numericValues!=null) {
                newMsg.numericValues=(ArrayList<Double>)numericValues.clone();
            }
            newMsg.frozen=false;
            return newMsg;
        }
    
        /**
         * Freezes this object, making it immutable and thread-safe.
         * @return this 
         * icu_annot::stable ICU 4.8
         */
        @Override
        public MessagePattern freeze() {
            frozen=true;
            return this;
        }
    
        /**
         * Determines whether this object is frozen (immutable) or not.
         * @return true if this object is frozen.
         * icu_annot::stable ICU 4.8
         */
        @Override
        public boolean isFrozen() {
            return frozen;
        }
    
        private void preParse(String pattern) {
            if(isFrozen()) {
                throw new UnsupportedOperationException(
                    "Attempt to parse("+prefix(pattern)+") on frozen MessagePattern instance.");
            }
            msg=pattern;
            hasArgNames=hasArgNumbers=false;
            needsAutoQuoting=false;
            parts.clear();
            if(numericValues!=null) {
                numericValues.clear();
            }
        }
    
        private void postParse() {
            // Nothing to be done currently.
        }
    
        private int parseMessage(int index, int msgStartLength, int nestingLevel, ArgType parentType) {
            if(nestingLevel>Part.MAX_VALUE) {
                throw new IndexOutOfBoundsException();
            }
            int msgStart=parts.size();
            addPart(Part.Type.MSG_START, index, msgStartLength, nestingLevel);
            index+=msgStartLength;
            while(index<msg.length()) {
                char c=msg.charAt(index++);
                if(c=='\'') {
                    if(index==msg.length()) {
                        // The apostrophe is the last character in the pattern. 
                        // Add a Part for auto-quoting.
                        addPart(Part.Type.INSERT_CHAR, index, 0, '\'');  // value=char to be inserted
                        needsAutoQuoting=true;
                    } else {
                        c=msg.charAt(index);
                        if(c=='\'') {
                            // double apostrophe, skip the second one
                            addPart(Part.Type.SKIP_SYNTAX, index++, 1, 0);
                        } else if(
                            aposMode==ApostropheMode.DOUBLE_REQUIRED ||
                            c=='{' || c=='}' ||
                            (parentType==ArgType.CHOICE && c=='|') ||
                            (parentType.hasPluralStyle() && c=='#')
                        ) {
                            // skip the quote-starting apostrophe
                            addPart(Part.Type.SKIP_SYNTAX, index-1, 1, 0);
                            // find the end of the quoted literal text
                            for(;;) {
                                index=msg.indexOf('\'', index+1);
                                if(index>=0) {
                                    if((index+1)<msg.length() && msg.charAt(index+1)=='\'') {
                                        // double apostrophe inside quoted literal text
                                        // still encodes a single apostrophe, skip the second one
                                        addPart(Part.Type.SKIP_SYNTAX, ++index, 1, 0);
                                    } else {
                                        // skip the quote-ending apostrophe
                                        addPart(Part.Type.SKIP_SYNTAX, index++, 1, 0);
                                        break;
                                    }
                                } else {
                                    // The quoted text reaches to the end of the of the message.
                                    index=msg.length();
                                    // Add a Part for auto-quoting.
                                    addPart(Part.Type.INSERT_CHAR, index, 0, '\'');  // value=char to be inserted
                                    needsAutoQuoting=true;
                                    break;
                                }
                            }
                        } else {
                            // Interpret the apostrophe as literal text.
                            // Add a Part for auto-quoting.
                            addPart(Part.Type.INSERT_CHAR, index, 0, '\'');  // value=char to be inserted
                            needsAutoQuoting=true;
                        }
                    }
                } else if(parentType.hasPluralStyle() && c=='#') {
                    // The unquoted # in a plural message fragment will be replaced
                    // with the (number-offset).
                    addPart(Part.Type.REPLACE_NUMBER, index-1, 1, 0);
                } else if(c=='{') {
                    index=parseArg(index-1, 1, nestingLevel);
                } else if((nestingLevel>0 && c=='}') || (parentType==ArgType.CHOICE && c=='|')) {
                    // Finish the message before the terminator.
                    // In a choice style, report the "}" substring only for the following ARG_LIMIT,
                    // not for this MSG_LIMIT.
                    int limitLength=(parentType==ArgType.CHOICE && c=='}') ? 0 : 1;
                    addLimitPart(msgStart, Part.Type.MSG_LIMIT, index-1, limitLength, nestingLevel);
                    if(parentType==ArgType.CHOICE) {
                        // Let the choice style parser see the '}' or '|'.
                        return index-1;
                    } else {
                        // continue parsing after the '}'
                        return index;
                    }
                }  // else: c is part of literal text
            }
            if(nestingLevel>0 && !inTopLevelChoiceMessage(nestingLevel, parentType)) {
                throw new IllegalArgumentException(
                    "Unmatched '{' braces in message "+prefix());
            }
            addLimitPart(msgStart, Part.Type.MSG_LIMIT, index, 0, nestingLevel);
            return index;
        }
    
        private int parseArg(int index, int argStartLength, int nestingLevel) {
            int argStart=parts.size();
            ArgType argType=ArgType.NONE;
            addPart(Part.Type.ARG_START, index, argStartLength, argType.ordinal());
            int nameIndex=index=skipWhiteSpace(index+argStartLength);
            if(index==msg.length()) {
                throw new IllegalArgumentException(
                    "Unmatched '{' braces in message "+prefix());
            }
            // parse argument name or number
            index=skipIdentifier(index);
            int number=parseArgNumber(nameIndex, index);
            if(number>=0) {
                int length=index-nameIndex;
                if(length>Part.MAX_LENGTH || number>Part.MAX_VALUE) {
                    throw new IndexOutOfBoundsException(
                        "Argument number too large: "+prefix(nameIndex));
                }
                hasArgNumbers=true;
                addPart(Part.Type.ARG_NUMBER, nameIndex, length, number);
            } else if(number==ARG_NAME_NOT_NUMBER) {
                int length=index-nameIndex;
                if(length>Part.MAX_LENGTH) {
                    throw new IndexOutOfBoundsException(
                        "Argument name too long: "+prefix(nameIndex));
                }
                hasArgNames=true;
                addPart(Part.Type.ARG_NAME, nameIndex, length, 0);
            } else {  // number<-1 (ARG_NAME_NOT_VALID)
                throw new IllegalArgumentException("Bad argument syntax: "+prefix(nameIndex));
            }
            index=skipWhiteSpace(index);
            if(index==msg.length()) {
                throw new IllegalArgumentException(
                    "Unmatched '{' braces in message "+prefix());
            }
            char c=msg.charAt(index);
            if(c=='}') {
                // all done
            } else if(c!=',') {
                throw new IllegalArgumentException("Bad argument syntax: "+prefix(nameIndex));
            } else /* ',' */ {
                // parse argument type: case-sensitive a-zA-Z
                int typeIndex=index=skipWhiteSpace(index+1);
                while(index<msg.length() && isArgTypeChar(msg.charAt(index))) {
                    ++index;
                }
                int length=index-typeIndex;
                index=skipWhiteSpace(index);
                if(index==msg.length()) {
                    throw new IllegalArgumentException(
                        "Unmatched '{' braces in message "+prefix());
                }
                if(length==0 || ((c=msg.charAt(index))!=',' && c!='}')) {
                    throw new IllegalArgumentException("Bad argument syntax: "+prefix(nameIndex));
                }
                if(length>Part.MAX_LENGTH) {
                    throw new IndexOutOfBoundsException(
                        "Argument type name too long: "+prefix(nameIndex));
                }
                argType=ArgType.SIMPLE;
                if(length==6) {
                    // case-insensitive comparisons for complex-type names
                    if(isChoice(typeIndex)) {
                        argType=ArgType.CHOICE;
                    } else if(isPlural(typeIndex)) {
                        argType=ArgType.PLURAL;
                    } else if(isSelect(typeIndex)) {
                        argType=ArgType.SELECT;
                    }
                } else if(length==13) {
                    if(isSelect(typeIndex) && isOrdinal(typeIndex+6)) {
                        argType=ArgType.SELECTORDINAL;
                    }
                }
                // change the ARG_START type from NONE to argType
                parts.get(argStart).value=(short)argType.ordinal();
                if(argType==ArgType.SIMPLE) {
                    addPart(Part.Type.ARG_TYPE, typeIndex, length, 0);
                }
                // look for an argument style (pattern)
                if(c=='}') {
                    if(argType!=ArgType.SIMPLE) {
                        throw new IllegalArgumentException(
                            "No style field for complex argument: "+prefix(nameIndex));
                    }
                } else /* ',' */ {
                    ++index;
                    if(argType==ArgType.SIMPLE) {
                        index=parseSimpleStyle(index);
                    } else if(argType==ArgType.CHOICE) {
                        index=parseChoiceStyle(index, nestingLevel);
                    } else {
                        index=parsePluralOrSelectStyle(argType, index, nestingLevel);
                    }
                }
            }
            // Argument parsing stopped on the '}'.
            addLimitPart(argStart, Part.Type.ARG_LIMIT, index, 1, argType.ordinal());
            return index+1;
        }
    
        private int parseSimpleStyle(int index) {
            int start=index;
            int nestedBraces=0;
            while(index<msg.length()) {
                char c=msg.charAt(index++);
                if(c=='\'') {
                    // Treat apostrophe as quoting but include it in the style part.
                    // Find the end of the quoted literal text.
                    index=msg.indexOf('\'', index);
                    if(index<0) {
                        throw new IllegalArgumentException(
                            "Quoted literal argument style text reaches to the end of the message: "+
                            prefix(start));
                    }
                    // skip the quote-ending apostrophe
                    ++index;
                } else if(c=='{') {
                    ++nestedBraces;
                } else if(c=='}') {
                    if(nestedBraces>0) {
                        --nestedBraces;
                    } else {
                        int length=--index-start;
                        if(length>Part.MAX_LENGTH) {
                            throw new IndexOutOfBoundsException(
                                "Argument style text too long: "+prefix(start));
                        }
                        addPart(Part.Type.ARG_STYLE, start, length, 0);
                        return index;
                    }
                }  // c is part of literal text
            }
            throw new IllegalArgumentException(
                "Unmatched '{' braces in message "+prefix());
        }
    
        private int parseChoiceStyle(int index, int nestingLevel) {
            int start=index;
            index=skipWhiteSpace(index);
            if(index==msg.length() || msg.charAt(index)=='}') {
                throw new IllegalArgumentException(
                    "Missing choice argument pattern in "+prefix());
            }
            for(;;) {
                // The choice argument style contains |-separated (number, separator, message) triples.
                // Parse the number.
                int numberIndex=index;
                index=skipDouble(index);
                int length=index-numberIndex;
                if(length==0) {
                    throw new IllegalArgumentException("Bad choice pattern syntax: "+prefix(start));
                }
                if(length>Part.MAX_LENGTH) {
                    throw new IndexOutOfBoundsException(
                        "Choice number too long: "+prefix(numberIndex));
                }
                parseDouble(numberIndex, index, true);  // adds ARG_INT or ARG_DOUBLE
                // Parse the separator.
                index=skipWhiteSpace(index);
                if(index==msg.length()) {
                    throw new IllegalArgumentException("Bad choice pattern syntax: "+prefix(start));
                }
                char c=msg.charAt(index);
                if(!(c=='#' || c=='<' || c=='\u2264')) {  // U+2264 is <=
                    throw new IllegalArgumentException(
                        "Expected choice separator (#<\u2264) instead of '"+c+
                        "' in choice pattern "+prefix(start));
                }
                addPart(Part.Type.ARG_SELECTOR, index, 1, 0);
                // Parse the message fragment.
                index=parseMessage(++index, 0, nestingLevel+1, ArgType.CHOICE);
                // parseMessage(..., CHOICE) returns the index of the terminator, or msg.length().
                if(index==msg.length()) {
                    return index;
                }
                if(msg.charAt(index)=='}') {
                    if(!inMessageFormatPattern(nestingLevel)) {
                        throw new IllegalArgumentException(
                            "Bad choice pattern syntax: "+prefix(start));
                    }
                    return index;
                }  // else the terminator is '|'
                index=skipWhiteSpace(index+1);
            }
        }
    
        private int parsePluralOrSelectStyle(ArgType argType, int index, int nestingLevel) {
            int start=index;
            boolean isEmpty=true;
            boolean hasOther=false;
            for(;;) {
                // First, collect the selector looking for a small set of terminators.
                // It would be a little faster to consider the syntax of each possible
                // token right here, but that makes the code too complicated.
                index=skipWhiteSpace(index);
                boolean eos=index==msg.length();
                if(eos || msg.charAt(index)=='}') {
                    if(eos==inMessageFormatPattern(nestingLevel)) {
                        throw new IllegalArgumentException(
                            "Bad "+
                            argType.toString().toLowerCase(Locale.ENGLISH)+
                            " pattern syntax: "+prefix(start));
                    }
                    if(!hasOther) {
                        throw new IllegalArgumentException(
                            "Missing 'other' keyword in "+
                            argType.toString().toLowerCase(Locale.ENGLISH)+
                            " pattern in "+prefix());
                    }
                    return index;
                }
                int selectorIndex=index;
                if(argType.hasPluralStyle() && msg.charAt(selectorIndex)=='=') {
                    // explicit-value plural selector: =double
                    index=skipDouble(index+1);
                    int length=index-selectorIndex;
                    if(length==1) {
                        throw new IllegalArgumentException(
                            "Bad "+
                            argType.toString().toLowerCase(Locale.ENGLISH)+
                            " pattern syntax: "+prefix(start));
                    }
                    if(length>Part.MAX_LENGTH) {
                        throw new IndexOutOfBoundsException(
                            "Argument selector too long: "+prefix(selectorIndex));
                    }
                    addPart(Part.Type.ARG_SELECTOR, selectorIndex, length, 0);
                    parseDouble(selectorIndex+1, index, false);  // adds ARG_INT or ARG_DOUBLE
                } else {
                    index=skipIdentifier(index);
                    int length=index-selectorIndex;
                    if(length==0) {
                        throw new IllegalArgumentException(
                            "Bad "+
                            argType.toString().toLowerCase(Locale.ENGLISH)+
                            " pattern syntax: "+prefix(start));
                    }
                    // Note: The ':' in "offset:" is just beyond the skipIdentifier() range.
                    if( argType.hasPluralStyle() && length==6 && index<msg.length() &&
                        msg.regionMatches(selectorIndex, "offset:", 0, 7)
                    ) {
                        // plural offset, not a selector
                        if(!isEmpty) {
                            throw new IllegalArgumentException(
                                "Plural argument 'offset:' (if present) must precede key-message pairs: "+
                                prefix(start));
                        }
                        // allow whitespace between offset: and its value
                        int valueIndex=skipWhiteSpace(index+1);  // The ':' is at index.
                        index=skipDouble(valueIndex);
                        if(index==valueIndex) {
                            throw new IllegalArgumentException(
                                "Missing value for plural 'offset:' "+prefix(start));
                        }
                        if((index-valueIndex)>Part.MAX_LENGTH) {
                            throw new IndexOutOfBoundsException(
                                "Plural offset value too long: "+prefix(valueIndex));
                        }
                        parseDouble(valueIndex, index, false);  // adds ARG_INT or ARG_DOUBLE
                        isEmpty=false;
                        continue;  // no message fragment after the offset
                    } else {
                        // normal selector word
                        if(length>Part.MAX_LENGTH) {
                            throw new IndexOutOfBoundsException(
                                "Argument selector too long: "+prefix(selectorIndex));
                        }
                        addPart(Part.Type.ARG_SELECTOR, selectorIndex, length, 0);
                        if(msg.regionMatches(selectorIndex, "other", 0, length)) {
                            hasOther=true;
                        }
                    }
                }
    
                // parse the message fragment following the selector
                index=skipWhiteSpace(index);
                if(index==msg.length() || msg.charAt(index)!='{') {
                    throw new IllegalArgumentException(
                        "No message fragment after "+
                        argType.toString().toLowerCase(Locale.ENGLISH)+
                        " selector: "+prefix(selectorIndex));
                }
                index=parseMessage(index, 1, nestingLevel+1, argType);
                isEmpty=false;
            }
        }
    
        /**
         * Validates and parses an argument name or argument number string.
         * This internal method assumes that the input substring is a "pattern identifier".
         * @return &gt;=0 if the name is a valid number,
         *         ARG_NAME_NOT_NUMBER (-1) if it is a "pattern identifier" but not all ASCII digits,
         *         ARG_NAME_NOT_VALID (-2) if it is neither.
         * @see #validateArgumentName(String)
         */
        private static int parseArgNumber(CharSequence s, int start, int limit) {
            // If the identifier contains only ASCII digits, then it is an argument _number_
            // and must not have leading zeros (except "0" itself).
            // Otherwise it is an argument _name_.
            if(start>=limit) {
                return ARG_NAME_NOT_VALID;
            }
            int number;
            // Defer numeric errors until we know there are only digits.
            boolean badNumber;
            char c=s.charAt(start++);
            if(c=='0') {
                if(start==limit) {
                    return 0;
                } else {
                    number=0;
                    badNumber=true;  // leading zero
                }
            } else if('1'<=c && c<='9') {
                number=c-'0';
                badNumber=false;
            } else {
                return ARG_NAME_NOT_NUMBER;
            }
            while(start<limit) {
                c=s.charAt(start++);
                if('0'<=c && c<='9') {
                    if(number>=Integer.MAX_VALUE/10) {
                        badNumber=true;  // overflow
                    }
                    number=number*10+(c-'0');
                } else {
                    return ARG_NAME_NOT_NUMBER;
                }
            }
            // There are only ASCII digits.
            if(badNumber) {
                return ARG_NAME_NOT_VALID;
            } else {
                return number;
            }
        }
    
        private int parseArgNumber(int start, int limit) {
            return parseArgNumber(msg, start, limit);
        }
    
        /**
         * Parses a number from the specified message substring.
         * @param start start index into the message string
         * @param limit limit index into the message string, must be start<limit
         * @param allowInfinity true if U+221E is allowed (for ChoiceFormat)
         */
        private void parseDouble(int start, int limit, boolean allowInfinity) {
            assert start<limit;
            // fake loop for easy exit and single throw statement
            for(;;) {
                // fast path for small integers and infinity
                int value=0;
                int isNegative=0;  // not boolean so that we can easily add it to value
                int index=start;
                char c=msg.charAt(index++);
                if(c=='-') {
                    isNegative=1;
                    if(index==limit) {
                        break;  // no number
                    }
                    c=msg.charAt(index++);
                } else if(c=='+') {
                    if(index==limit) {
                        break;  // no number
                    }
                    c=msg.charAt(index++);
                }
                if(c==0x221e) {  // infinity
                    if(allowInfinity && index==limit) {
                        addArgDoublePart(
                            isNegative!=0 ? Double.NEGATIVE_INFINITY : Double.POSITIVE_INFINITY,
                            start, limit-start);
                        return;
                    } else {
                        break;
                    }
                }
                // try to parse the number as a small integer but fall back to a double
                while('0'<=c && c<='9') {
                    value=value*10+(c-'0');
                    if(value>(Part.MAX_VALUE+isNegative)) {
                        break;  // not a small-enough integer
                    }
                    if(index==limit) {
                        addPart(Part.Type.ARG_INT, start, limit-start, isNegative!=0 ? -value : value);
                        return;
                    }
                    c=msg.charAt(index++);
                }
                // Let Double.parseDouble() throw a NumberFormatException.
                double numericValue=Double.parseDouble(msg.substring(start, limit));
                addArgDoublePart(numericValue, start, limit-start);
                return;
            }
            throw new NumberFormatException(
                "Bad syntax for numeric value: "+msg.substring(start, limit));
        }
    
        /**
         * Appends the s[start, limit[ substring to sb, but with only half of the apostrophes
         * according to JDK pattern behavior.
         * icu_annot::internal
         */
        /* package */ static void appendReducedApostrophes(String s, int start, int limit,
                                                           StringBuilder sb) {
            int doubleApos=-1;
            for(;;) {
                int i=s.indexOf('\'', start);
                if(i<0 || i>=limit) {
                    sb.append(s, start, limit);
                    break;
                }
                if(i==doubleApos) {
                    // Double apostrophe at start-1 and start==i, append one.
                    sb.append('\'');
                    ++start;
                    doubleApos=-1;
                } else {
                    // Append text between apostrophes and skip this one.
                    sb.append(s, start, i);
                    doubleApos=start=i+1;
                }
            }
        }
    
        private int skipWhiteSpace(int index) {
            return PatternProps.skipWhiteSpace(msg, index);
        }
    
        private int skipIdentifier(int index) {
            return PatternProps.skipIdentifier(msg, index);
        }
    
        /**
         * Skips a sequence of characters that could occur in a double value.
         * Does not fully parse or validate the value.
         */
        private int skipDouble(int index) {
            while(index<msg.length()) {
                char c=msg.charAt(index);
                // U+221E: Allow the infinity symbol, for ChoiceFormat patterns.
                if((c<'0' && "+-.".indexOf(c)<0) || (c>'9' && c!='e' && c!='E' && c!=0x221e)) {
                    break;
                }
                ++index;
            }
            return index;
        }
    
        private static boolean isArgTypeChar(int c) {
            return ('a'<=c && c<='z') || ('A'<=c && c<='Z');
        }
    
        private boolean isChoice(int index) {
            char c;
            return
                ((c=msg.charAt(index++))=='c' || c=='C') &&
                ((c=msg.charAt(index++))=='h' || c=='H') &&
                ((c=msg.charAt(index++))=='o' || c=='O') &&
                ((c=msg.charAt(index++))=='i' || c=='I') &&
                ((c=msg.charAt(index++))=='c' || c=='C') &&
                ((c=msg.charAt(index))=='e' || c=='E');
        }
    
        private boolean isPlural(int index) {
            char c;
            return
                ((c=msg.charAt(index++))=='p' || c=='P') &&
                ((c=msg.charAt(index++))=='l' || c=='L') &&
                ((c=msg.charAt(index++))=='u' || c=='U') &&
                ((c=msg.charAt(index++))=='r' || c=='R') &&
                ((c=msg.charAt(index++))=='a' || c=='A') &&
                ((c=msg.charAt(index))=='l' || c=='L');
        }
    
        private boolean isSelect(int index) {
            char c;
            return
                ((c=msg.charAt(index++))=='s' || c=='S') &&
                ((c=msg.charAt(index++))=='e' || c=='E') &&
                ((c=msg.charAt(index++))=='l' || c=='L') &&
                ((c=msg.charAt(index++))=='e' || c=='E') &&
                ((c=msg.charAt(index++))=='c' || c=='C') &&
                ((c=msg.charAt(index))=='t' || c=='T');
        }
    
        private boolean isOrdinal(int index) {
            char c;
            return
                ((c=msg.charAt(index++))=='o' || c=='O') &&
                ((c=msg.charAt(index++))=='r' || c=='R') &&
                ((c=msg.charAt(index++))=='d' || c=='D') &&
                ((c=msg.charAt(index++))=='i' || c=='I') &&
                ((c=msg.charAt(index++))=='n' || c=='N') &&
                ((c=msg.charAt(index++))=='a' || c=='A') &&
                ((c=msg.charAt(index))=='l' || c=='L');
        }
    
        /**
         * @return true if we are inside a MessageFormat (sub-)pattern,
         *         as opposed to inside a top-level choice/plural/select pattern.
         */
        private boolean inMessageFormatPattern(int nestingLevel) {
            return nestingLevel>0 || parts.get(0).type==Part.Type.MSG_START;
        }
    
        /**
         * @return true if we are in a MessageFormat sub-pattern
         *         of a top-level ChoiceFormat pattern.
         */
        private boolean inTopLevelChoiceMessage(int nestingLevel, ArgType parentType) {
            return
                nestingLevel==1 &&
                parentType==ArgType.CHOICE &&
                parts.get(0).type!=Part.Type.MSG_START;
        }
    
        private void addPart(Part.Type type, int index, int length, int value) {
            parts.add(new Part(type, index, length, value));
        }
    
        private void addLimitPart(int start, Part.Type type, int index, int length, int value) {
            parts.get(start).limitPartIndex=parts.size();
            addPart(type, index, length, value);
        }
    
        private void addArgDoublePart(double numericValue, int start, int length) {
            int numericIndex;
            if(numericValues==null) {
                numericValues=new ArrayList<Double>();
                numericIndex=0;
            } else {
                numericIndex=numericValues.size();
                if(numericIndex>Part.MAX_VALUE) {
                    throw new IndexOutOfBoundsException("Too many numeric values");
                }
            }
            numericValues.add(numericValue);
            addPart(Part.Type.ARG_DOUBLE, start, length, numericIndex);
        }
    
        private static final int MAX_PREFIX_LENGTH=24;
    
        /**
         * Returns a prefix of s.substring(start). Used for Exception messages.
         * @param s
         * @param start start index in s
         * @return s.substring(start) or a prefix of that
         */
        private static String prefix(String s, int start) {
            StringBuilder prefix=new StringBuilder(MAX_PREFIX_LENGTH+20);
            if(start==0) {
                prefix.append("\"");
            } else {
                prefix.append("[at pattern index ").append(start).append("] \"");
            }
            int substringLength=s.length()-start;
            if(substringLength<=MAX_PREFIX_LENGTH) {
                prefix.append(start==0 ? s : s.substring(start));
            } else {
                int limit=start+MAX_PREFIX_LENGTH-4;
                if(Character.isHighSurrogate(s.charAt(limit-1))) {
                    // remove lead surrogate from the end of the prefix
                    --limit;
                }
                prefix.append(s, start, limit).append(" ...");
            }
            return prefix.append("\"").toString();
        }
    
        private static String prefix(String s) {
            return prefix(s, 0);
        }
    
        private String prefix(int start) {
            return prefix(msg, start);
        }
    
        private String prefix() {
            return prefix(msg, 0);
        }
    
        private ApostropheMode aposMode;
        private String msg;
        private ArrayList<Part> parts=new ArrayList<Part>();
        private ArrayList<Double> numericValues;
        private boolean hasArgNames;
        private boolean hasArgNumbers;
        private boolean needsAutoQuoting;
        private boolean frozen;
    
        private static final ApostropheMode defaultAposMode=
            ApostropheMode.valueOf(
                ICUConfig.get("com.ibm.icu.text.MessagePattern.ApostropheMode", "DOUBLE_OPTIONAL"));
    
        private static final ArgType[] argTypes=ArgType.values();
    }