]> granicus.if.org Git - icu/commitdiff
ICU-8745 merge MessagePatternUtil into trunk, from merge --reintegrate branches/marku...
authorMarkus Scherer <markus.icu@gmail.com>
Mon, 15 Aug 2011 21:09:39 +0000 (21:09 +0000)
committerMarkus Scherer <markus.icu@gmail.com>
Mon, 15 Aug 2011 21:09:39 +0000 (21:09 +0000)
X-SVN-Rev: 30510

icu4j/demos/src/com/ibm/icu/dev/demo/messagepattern/MessagePatternUtilDemo.java [new file with mode: 0644]
icu4j/main/classes/core/src/com/ibm/icu/text/MessagePatternUtil.java [new file with mode: 0644]
icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/MessagePatternUtilTest.java [new file with mode: 0644]
icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/TestAll.java

diff --git a/icu4j/demos/src/com/ibm/icu/dev/demo/messagepattern/MessagePatternUtilDemo.java b/icu4j/demos/src/com/ibm/icu/dev/demo/messagepattern/MessagePatternUtilDemo.java
new file mode 100644 (file)
index 0000000..47d41b4
--- /dev/null
@@ -0,0 +1,403 @@
+/*
+*******************************************************************************
+*   Copyright (C) 2011, International Business Machines
+*   Corporation and others.  All Rights Reserved.
+*******************************************************************************
+*   created on: 2011jul14
+*   created by: Markus W. Scherer
+*/
+
+package com.ibm.icu.dev.demo.messagepattern;
+
+import com.ibm.icu.text.MessagePattern;
+import com.ibm.icu.text.MessagePatternUtil;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Demo code for MessagePattern class.
+ * @author Markus Scherer
+ * @since 2011-jul-14
+ */
+public class MessagePatternUtilDemo {
+    private static final String manySpaces="                    ";
+
+    private static final void printMessage(MessagePatternUtil.MessageNode msg, int depth) {
+        String indent = manySpaces.substring(0, depth * 2);
+        for (MessagePatternUtil.MessageContentsNode contents : msg.getContents()) {
+            switch (contents.getType()) {
+            case TEXT:
+                System.out.println(indent + "text: «" +
+                                   ((MessagePatternUtil.TextNode)contents).getText() + "»");
+                break;
+            case ARG:
+                printArg((MessagePatternUtil.ArgNode)contents, depth);
+                break;
+            case REPLACE_NUMBER:
+                System.out.println(indent + "replace: number");
+                break;
+            }
+        }
+    }
+
+    private static final void printArg(MessagePatternUtil.ArgNode arg, int depth) {
+        System.out.print(manySpaces.substring(0, depth * 2) + "arg: «" + arg.getName() + "»");
+        MessagePattern.ArgType argType = arg.getArgType();
+        if (argType == MessagePattern.ArgType.NONE) {
+            System.out.println(" (no type)");
+        } else {
+            System.out.print(" (" + arg.getTypeName() + ")");
+            if (argType == MessagePattern.ArgType.SIMPLE) {
+                String styleString = arg.getSimpleStyle();
+                if (styleString == null) {
+                    System.out.println(" (no style)");
+                } else {
+                    System.out.println(" style: «" + styleString + "»");
+                }
+            } else {
+                System.out.println();
+                printComplexArgStyle(arg.getComplexStyle(), depth + 1);
+            }
+        }
+    }
+
+    private static final void printComplexArgStyle(MessagePatternUtil.ComplexArgStyleNode style,
+                                                   int depth) {
+        if (style.hasExplicitOffset()) {
+            System.out.println(manySpaces.substring(0, depth * 2) + "offset: " + style.getOffset());
+        }
+        String indent = manySpaces.substring(0, depth * 2);
+        MessagePattern.ArgType argType = style.getArgType();
+        for (MessagePatternUtil.VariantNode variant : style.getVariants()) {
+            double value;
+            switch (argType) {
+            case CHOICE:
+                System.out.println(indent + variant.getSelectorValue() + " " + variant.getSelector() + ":");
+                break;
+            case PLURAL:
+                value = variant.getSelectorValue();
+                if (value == MessagePattern.NO_NUMERIC_VALUE) {
+                    System.out.println(indent + variant.getSelector() + ":");
+                } else {
+                    System.out.println(indent + variant.getSelector() + " (" + value + "):");
+                }
+                break;
+            case SELECT:
+                System.out.println(indent + variant.getSelector() + ":");
+                break;
+            }
+            printMessage(variant.getMessage(), depth + 1);
+        }
+    }
+
+    /**
+     * This is a <em>prototype/demo/sample</em> for how we could use the MessagePatternUtil class
+     * for generating something like JavaScript code for evaluating some
+     * of the MessageFormat syntax.
+     *
+     * <p>This is not intended to be production code, nor to generate production code
+     * or even syntactically correct JavaScript.
+     * @param msg
+     */
+    private static final void genCode(MessagePatternUtil.MessageNode msg) {
+        List<String> args = new ArrayList<String>();
+        addArgs(msg, args);
+        System.out.print("def function(");
+        boolean firstArg = true;
+        for (String argName : args) {
+            if (firstArg) {
+                System.out.print(argName);
+                firstArg = false;
+            } else {
+                System.out.print(", " + argName);
+            }
+        }
+        System.out.println(") {");
+        genCode(msg, 1, true, "");
+        System.out.println("  return result");
+        System.out.println("}");
+    }
+
+    private static final void genCode(MessagePatternUtil.MessageNode msg,
+                                      int depth,
+                                      boolean firstResult,
+                                      String pluralNumber) {
+        String prefix = manySpaces.substring(0, depth * 2) + "result ";
+        for (MessagePatternUtil.MessageContentsNode contents : msg.getContents()) {
+            String operator = firstResult ? "=" : "+=";
+            switch (contents.getType()) {
+            case TEXT:
+                System.out.println(
+                        prefix + operator + " \"" +
+                        escapeString(((MessagePatternUtil.TextNode)contents).getText()) +
+                "\"");
+                break;
+            case ARG:
+                genCode((MessagePatternUtil.ArgNode)contents, depth, firstResult);
+                break;
+            case REPLACE_NUMBER:
+                System.out.println(prefix + operator + " formatNumber(" + pluralNumber + ")");
+                break;
+            }
+            firstResult = false;
+        }
+    }
+
+    private static final void genCode(MessagePatternUtil.ArgNode arg,
+                                      int depth,
+                                      boolean firstResult) {
+        String prefix = manySpaces.substring(0, depth * 2) + "result ";
+        String operator = firstResult ? "=" : "+=";
+        String argName = arg.getName();
+        if (arg.getNumber() >= 0) {
+            argName = "arg_" + argName;  // Prefix for numbered argument.
+        }
+        switch (arg.getArgType()) {
+        case NONE:
+            System.out.println(prefix + operator + " " + argName);
+            break;
+        case SIMPLE:
+        case CHOICE:
+            System.out.println(prefix + operator + " \"(unsupported syntax)\"");
+            break;
+        case PLURAL:
+            genCodeForPlural(arg.getComplexStyle(), depth, firstResult, argName);
+            break;
+        case SELECT:
+            genCodeForSelect(arg.getComplexStyle(), depth, firstResult, argName);
+            break;
+        }
+    }
+
+    private static final void genCodeForPlural(MessagePatternUtil.ComplexArgStyleNode style,
+                                               int depth,
+                                               boolean firstResult,
+                                               String argName) {
+        List<MessagePatternUtil.VariantNode> numericVariants =
+            new ArrayList<MessagePatternUtil.VariantNode>();
+        List<MessagePatternUtil.VariantNode> keywordVariants =
+            new ArrayList<MessagePatternUtil.VariantNode>();
+        MessagePatternUtil.VariantNode otherVariant =
+            style.getVariantsByType(numericVariants, keywordVariants);
+        double offset = style.getOffset();
+        String pluralNumber = offset == 0. ? argName : argName + " - " + offset;
+        int origDepth = depth;
+        if (!numericVariants.isEmpty()) {
+            genCodeForNumericVariants(numericVariants, depth++, firstResult, argName, pluralNumber);
+        }
+        if (!keywordVariants.isEmpty()) {
+            System.out.println(manySpaces.substring(0, depth * 2) +
+                               "_keyword = PluralRules.select(" + pluralNumber + ")");
+            genCodeForKeywordVariants(keywordVariants, depth++, firstResult, "_keyword", pluralNumber);
+        }
+        genCode(otherVariant.getMessage(), depth, firstResult, pluralNumber);
+        if (origDepth < depth) {
+            System.out.println(manySpaces.substring(0, --depth * 2) + "}");
+            if (origDepth < depth) {
+                System.out.println(manySpaces.substring(0, --depth * 2) + "}");
+            }
+        }
+    }
+
+    private static final void genCodeForSelect(MessagePatternUtil.ComplexArgStyleNode style,
+                                               int depth,
+                                               boolean firstResult,
+                                               String argName) {
+        List<MessagePatternUtil.VariantNode> keywordVariants =
+            new ArrayList<MessagePatternUtil.VariantNode>();
+        MessagePatternUtil.VariantNode otherVariant = style.getVariantsByType(null, keywordVariants);
+        if (keywordVariants.isEmpty()) {
+            genCode(otherVariant.getMessage(), depth, firstResult, "");
+        } else {
+            genCodeForKeywordVariants(keywordVariants, depth, firstResult, argName, "");
+            genCode(otherVariant.getMessage(), depth + 1, firstResult, "");
+            System.out.println(manySpaces.substring(0, depth * 2) + "}");
+        }
+    }
+
+    private static final void genCodeForNumericVariants(List<MessagePatternUtil.VariantNode> variants,
+                                                        int depth,
+                                                        boolean firstResult,
+                                                        String varName,
+                                                        String pluralNumber) {
+        String indent = manySpaces.substring(0, depth++ * 2);
+        boolean firstVariant = true;
+        for (MessagePatternUtil.VariantNode variant : variants) {
+            System.out.println(
+                    indent +
+                    (firstVariant ? "if (" : "} else if (") +
+                    varName + " == " + variant.getSelectorValue() + ") {");
+            genCode(variant.getMessage(), depth, firstResult, pluralNumber);
+            firstVariant = false;
+        }
+        System.out.println(indent + "} else {");
+    }
+
+    private static final void genCodeForKeywordVariants(List<MessagePatternUtil.VariantNode> variants,
+                                                        int depth,
+                                                        boolean firstResult,
+                                                        String varName,
+                                                        String pluralNumber) {
+        String indent = manySpaces.substring(0, depth++ * 2);
+        boolean firstVariant = true;
+        for (MessagePatternUtil.VariantNode variant : variants) {
+            System.out.println(
+                    indent +
+                    (firstVariant ? "if (" : "} else if (") +
+                    varName + " == \"" + variant.getSelector() + "\") {");
+            genCode(variant.getMessage(), depth, firstResult, pluralNumber);
+            firstVariant = false;
+        }
+        System.out.println(indent + "} else {");
+    }
+
+    /**
+     * Adds the message's argument names to the args list.
+     * Adds each argument only once, in the order of first appearance.
+     * Numbered arguments get an "arg_" prefix prepended.
+     * @param msg
+     * @param args
+     */
+    private static final void addArgs(MessagePatternUtil.MessageNode msg, List<String> args) {
+        for (MessagePatternUtil.MessageContentsNode contents : msg.getContents()) {
+            if (contents.getType() == MessagePatternUtil.MessageContentsNode.Type.ARG) {
+                MessagePatternUtil.ArgNode arg = (MessagePatternUtil.ArgNode)contents;
+                String argName;
+                if (arg.getNumber() >= 0) {
+                    argName = "arg_" + arg.getNumber();  // Prefix for numbered argument.
+                } else {
+                    argName = arg.getName();
+                }
+                if (!args.contains(argName)) {
+                    args.add(argName);
+                }
+                MessagePatternUtil.ComplexArgStyleNode complexStyle = arg.getComplexStyle();
+                if (complexStyle != null) {
+                    for (MessagePatternUtil.VariantNode variant : complexStyle.getVariants()) {
+                        addArgs(variant.getMessage(), args);
+                    }
+                }
+            }
+        }
+    }
+
+    private static final String escapeString(String s) {
+        if (s.indexOf('"') < 0) {
+            return s;
+        } else {
+            return s.replace("\"", "\\\"");
+        }
+    }
+
+    private static final MessagePatternUtil.MessageNode print(String s) {
+        System.out.println("message:  «" + s + "»");
+        try {
+            MessagePatternUtil.MessageNode msg = MessagePatternUtil.buildMessageNode(s);
+            printMessage(msg, 1);
+            genCode(msg);
+            return msg;
+        } catch(Exception e) {
+            System.out.println("Exception: "+e.getMessage());
+            return null;
+        }
+    }
+
+    public static void main(String[] argv) {
+        print("Hello!");
+        print("Hel'lo!");
+        print("Hel'{o");
+        print("Hel'{'o");
+        // double apostrophe inside quoted literal text still encodes a single apostrophe
+        print("a'{bc''de'f");
+        print("a'{bc''de'f{0,number,g'hi''jk'l#}");
+        print("abc{0}def");
+        print("abc{ arg }def");
+        print("abc{1}def{arg}ghi");
+        print("abc{2, number}ghi{3, select, xx {xxx} other {ooo}} xyz");
+        print("abc{gender,select,"+
+                  "other{His name is {tc,XMB,<ph name=\"PERSON\">{$PERSON}</ph>}.}}xyz");
+        print("abc{num_people, plural, offset:17 few{fff} other {oooo}}xyz");
+        print("abc{ num , plural , offset: 2 =1 {1} =-1 {-1} =3.14 {3.14} other {oo} }xyz");
+        print("I don't {a,plural,other{w'{'on't #'#'}} and "+
+              "{b,select,other{shan't'}'}} '{'''know'''}' and "+
+              "{c,choice,0#can't'|'}"+
+              "{z,number,#'#'###.00'}'}.");
+        print("a_{0,choice,-∞ #-inf|  5≤ five | 99 # ninety'|'nine  }_z");
+        print("a_{0,plural,other{num=#'#'=#'#'={1,number,##}!}}_z");
+        print("}}}{0}}");  // yes, unmatched '}' are ok in ICU MessageFormat
+        print("Hello {0}!");
+        String msg="++{0, select, female{{1} calls you her friend}"+
+                                 "other{{1} calls you '{their}' friend}"+
+                                 "male{{1} calls you his friend}}--";
+        print(msg);
+        msg="_'__{gender, select, female{Her n'ame is {person_name}.}"+
+                                 "other{His n'ame is {person_name}.}}__'_";
+        print(msg);
+        print("{num,plural,offset:1 =0{no one} =1{one, that is one and # others} " +
+                                   "one{one and # (probably 1) others} few{one and # others} " +
+                                   "other{lots & lots}}");
+        print(
+            "{p1_gender,select," +
+              "female{" +
+                "{p2_gender,select," +
+                  "female{" +
+                    "{num_people,plural,offset:1 "+
+                      "=0{she alone}" +
+                      "=1{she and her girlfriend {p2}}" +
+                      "=2{she and her girlfriend {p2} and another}" +
+                      "other{she, her girlfriend {p2} and # others}}}" +
+                  "male{" +
+                    "{num_people,plural,offset:1 "+
+                      "=0{she alone}" +
+                      "=1{she and her boyfriend {p2}}" +
+                      "=2{she and her boyfriend {p2} and another}" +
+                      "other{she, her boyfriend {p2} and # others}}}" +
+                  "other{" +
+                    "{num_people,plural,offset:1 "+
+                      "=0{she alone}" +
+                      "=1{she and her friend {p2}}" +
+                      "=2{she and her friend {p2} and another}" +
+                      "other{she, her friend {p2} and # others}}}}}" +
+              "male{" +
+                "{p2_gender,select," +
+                  "female{" +
+                    "{num_people,plural,offset:1 "+
+                      "=0{he alone}" +
+                      "=1{he and his girlfriend {p2}}" +
+                      "=2{he and his girlfriend {p2} and another}" +
+                      "other{he, his girlfriend {p2} and # others}}}" +
+                    "male{" +
+                      "{num_people,plural,offset:1 "+
+                        "=0{he alone}" +
+                        "=1{he and his boyfriend {p2}}" +
+                        "=2{he and his boyfriend {p2} and another}" +
+                        "other{he, his boyfriend {p2} and # others}}}" +
+                    "other{" +
+                      "{num_people,plural,offset:1 "+
+                        "=0{she alone}" +
+                        "=1{she and his friend {p2}}" +
+                        "=2{she and his friend {p2} and another}" +
+                        "other{she, his friend {p2} and # others}}}}}" +
+              "other{" +
+                "{p2_gender,select," +
+                  "female{" +
+                    "{num_people,plural,offset:1 "+
+                      "=0{they alone}" +
+                      "=1{they and their girlfriend {p2}}" +
+                      "=2{they and their girlfriend {p2} and another}" +
+                      "other{they, their girlfriend {p2} and # others}}}" +
+                  "male{" +
+                    "{num_people,plural,offset:1 "+
+                      "=0{they alone}" +
+                      "=1{they and their boyfriend {p2}}" +
+                      "=2{they and their boyfriend {p2} and another}" +
+                      "other{they, their boyfriend {p2} and # others}}}" +
+                  "other{" +
+                    "{num_people,plural,offset:1 "+
+                      "=0{they alone}" +
+                      "=1{they and their friend {p2}}" +
+                      "=2{they and their friend {p2} and another}" +
+                      "other{they, their friend {p2} and # others}}}}}}");
+    }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/text/MessagePatternUtil.java b/icu4j/main/classes/core/src/com/ibm/icu/text/MessagePatternUtil.java
new file mode 100644 (file)
index 0000000..880f826
--- /dev/null
@@ -0,0 +1,613 @@
+/*
+*******************************************************************************
+*   Copyright (C) 2011, International Business Machines
+*   Corporation and others.  All Rights Reserved.
+*******************************************************************************
+*   created on: 2011jul14
+*   created by: Markus W. Scherer
+*/
+
+package com.ibm.icu.text;
+
+import com.ibm.icu.text.MessagePattern;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Utilities for working with a MessagePattern.
+ * Intended for use in tools when convenience is more important than
+ * minimizing runtime and object creations.
+ *
+ * <p>This class and its nested classes are not intended for public subclassing.
+ * @draft ICU 49
+ * @provisional This API might change or be removed in a future release.
+ * @author Markus Scherer
+ */
+public final class MessagePatternUtil {
+    /**
+     * Factory method, builds and returns a MessageNode from a MessageFormat pattern string.
+     * @param patternString a MessageFormat pattern string
+     * @return a MessageNode or a ComplexArgStyleNode
+     * @throws IllegalArgumentException if the MessagePattern is empty
+     *         or does not represent a MessageFormat pattern
+     * @draft ICU 49
+     * @provisional This API might change or be removed in a future release.
+     */
+    public static MessageNode buildMessageNode(String patternString) {
+        return buildMessageNode(new MessagePattern(patternString));
+    }
+
+    /**
+     * Factory method, builds and returns a MessageNode from a MessagePattern.
+     * @param pattern a parsed MessageFormat pattern string
+     * @return a MessageNode or a ComplexArgStyleNode
+     * @throws IllegalArgumentException if the MessagePattern is empty
+     *         or does not represent a MessageFormat pattern
+     * @draft ICU 49
+     * @provisional This API might change or be removed in a future release.
+     */
+    public static MessageNode buildMessageNode(MessagePattern pattern) {
+        int limit = pattern.countParts() - 1;
+        if (limit < 0) {
+            throw new IllegalArgumentException("The MessagePattern is empty");
+        } else if (pattern.getPartType(0) != MessagePattern.Part.Type.MSG_START) {
+            throw new IllegalArgumentException(
+            "The MessagePattern does not represent a MessageFormat pattern");
+        }
+        return buildMessageNode(pattern, 0, limit);
+    }
+
+    /**
+     * Common base class for all elements in a tree of nodes
+     * returned by {@link MessagePatternUtil#buildMessageNode(MessagePattern)}.
+     * @draft ICU 49
+     * @provisional This API might change or be removed in a future release.
+     */
+    public static class Node {
+        private Node() {}
+    }
+
+    /**
+     * A Node representing a parsed MessageFormat pattern string.
+     * @draft ICU 49
+     * @provisional This API might change or be removed in a future release.
+     */
+    public static class MessageNode extends Node {
+        /**
+         * @return the list of MessageContentsNode nodes that this message contains
+         * @draft ICU 49
+         * @provisional This API might change or be removed in a future release.
+         */
+        public List<MessageContentsNode> getContents() {
+            return list;
+        }
+        /**
+         * {@inheritDoc}
+         * @draft ICU 49
+         * @provisional This API might change or be removed in a future release.
+         */
+        @Override
+        public String toString() {
+            return list.toString();
+        }
+
+        private MessageNode() {
+            super();
+        }
+        private void addContentsNode(MessageContentsNode node) {
+            if (node instanceof TextNode && !list.isEmpty()) {
+                // Coalesce adjacent text nodes.
+                MessageContentsNode lastNode = list.get(list.size() - 1);
+                if (lastNode instanceof TextNode) {
+                    TextNode textNode = (TextNode)lastNode;
+                    textNode.text = textNode.text + ((TextNode)node).text;
+                    return;
+                }
+            }
+            list.add(node);
+        }
+        private MessageNode freeze() {
+            list = Collections.unmodifiableList(list);
+            return this;
+        }
+
+        private List<MessageContentsNode> list = new ArrayList<MessageContentsNode>();
+    }
+
+    /**
+     * A piece of MessageNode contents.
+     * Use getType() to determine the type and the actual Node subclass.
+     * @draft ICU 49
+     * @provisional This API might change or be removed in a future release.
+     */
+    public static class MessageContentsNode extends Node {
+        /**
+         * The type of a piece of MessageNode contents.
+         * @draft ICU 49
+         * @provisional This API might change or be removed in a future release.
+         */
+        public enum Type {
+            /**
+             * This is a TextNode containing literal text (downcast and call getText()).
+             * @draft ICU 49
+             * @provisional This API might change or be removed in a future release.
+             */
+            TEXT,
+            /**
+             * This is an ArgNode representing a message argument
+             * (downcast and use specific methods).
+             * @draft ICU 49
+             * @provisional This API might change or be removed in a future release.
+             */
+            ARG,
+            /**
+             * This Node represents a place in a plural argument's variant where
+             * the formatted (plural-offset) value is to be put.
+             * @draft ICU 49
+             * @provisional This API might change or be removed in a future release.
+             */
+            REPLACE_NUMBER
+        }
+        /**
+         * Returns the type of this piece of MessageNode contents.
+         * @draft ICU 49
+         * @provisional This API might change or be removed in a future release.
+         */
+        public Type getType() {
+            return type;
+        }
+        /**
+         * {@inheritDoc}
+         * @draft ICU 49
+         * @provisional This API might change or be removed in a future release.
+         */
+        @Override
+        public String toString() {
+            return "{REPLACE_NUMBER}";
+        }
+
+        private MessageContentsNode(Type type) {
+            super();
+            this.type = type;
+        }
+        private static MessageContentsNode createReplaceNumberNode() {
+            return new MessageContentsNode(Type.REPLACE_NUMBER);
+        }
+
+        private Type type;
+    }
+
+    /**
+     * Literal text, a piece of MessageNode contents.
+     * @draft ICU 49
+     * @provisional This API might change or be removed in a future release.
+     */
+    public static class TextNode extends MessageContentsNode {
+        /**
+         * @return the literal text at this point in the message
+         * @draft ICU 49
+         * @provisional This API might change or be removed in a future release.
+         */
+        public String getText() {
+            return text;
+        }
+        /**
+         * {@inheritDoc}
+         * @draft ICU 49
+         * @provisional This API might change or be removed in a future release.
+         */
+        @Override
+        public String toString() {
+            return "«" + text + "»";
+        }
+
+        private TextNode(String text) {
+            super(Type.TEXT);
+            this.text = text;
+        }
+
+        private String text;
+    }
+
+    /**
+     * A piece of MessageNode contents representing a message argument and its details.
+     * @draft ICU 49
+     * @provisional This API might change or be removed in a future release.
+     */
+    public static class ArgNode extends MessageContentsNode {
+        /**
+         * @return the argument type
+         * @draft ICU 49
+         * @provisional This API might change or be removed in a future release.
+         */
+        public MessagePattern.ArgType getArgType() {
+            return argType;
+        }
+        /**
+         * @return the argument name string (the decimal-digit string if the argument has a number)
+         * @draft ICU 49
+         * @provisional This API might change or be removed in a future release.
+         */
+        public String getName() {
+            return name;
+        }
+        /**
+         * @return the argument number, or -1 if none (for a named argument)
+         * @draft ICU 49
+         * @provisional This API might change or be removed in a future release.
+         */
+        public int getNumber() {
+            return number;
+        }
+        /**
+         * @return the argument type string, or null if none was specified
+         * @draft ICU 49
+         * @provisional This API might change or be removed in a future release.
+         */
+        public String getTypeName() {
+            return typeName;
+        }
+        /**
+         * @return the simple-argument style string,
+         *         or null if no style is specified and for other argument types
+         * @draft ICU 49
+         * @provisional This API might change or be removed in a future release.
+         */
+        public String getSimpleStyle() {
+            return style;
+        }
+        /**
+         * @return the complex-argument-style object,
+         *         or null if the argument type is NONE_ARG or SIMPLE_ARG
+         * @draft ICU 49
+         * @provisional This API might change or be removed in a future release.
+         */
+        public ComplexArgStyleNode getComplexStyle() {
+            return complexStyle;
+        }
+        /**
+         * {@inheritDoc}
+         * @draft ICU 49
+         * @provisional This API might change or be removed in a future release.
+         */
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            sb.append('{').append(name);
+            if (argType != MessagePattern.ArgType.NONE) {
+                sb.append(',').append(typeName);
+                if (argType == MessagePattern.ArgType.SIMPLE) {
+                    if (style != null) {
+                        sb.append(',').append(style);
+                    }
+                } else {
+                    sb.append(',').append(complexStyle.toString());
+                }
+            }
+            return sb.append('}').toString();
+        }
+
+        private ArgNode() {
+            super(Type.ARG);
+        }
+        private static ArgNode createArgNode() {
+            return new ArgNode();
+        }
+
+        private MessagePattern.ArgType argType;
+        private String name;
+        private int number = -1;
+        private String typeName;
+        private String style;
+        private ComplexArgStyleNode complexStyle;
+    }
+
+    /**
+     * A Node representing details of the argument style of a complex argument.
+     * (Which is a choice/plural/select argument which selects among nested messages.)
+     * @draft ICU 49
+     * @provisional This API might change or be removed in a future release.
+     */
+    public static class ComplexArgStyleNode extends Node {
+        /**
+         * @return the argument type (same as getArgType() on the parent ArgNode)
+         * @draft ICU 49
+         * @provisional This API might change or be removed in a future release.
+         */
+        public MessagePattern.ArgType getArgType() {
+            return argType;
+        }
+        /**
+         * @return true if this is a plural style with an explicit offset
+         * @draft ICU 49
+         * @provisional This API might change or be removed in a future release.
+         */
+        public boolean hasExplicitOffset() {
+            return explicitOffset;
+        }
+        /**
+         * @return the plural offset, or 0 if this is not a plural style or
+         *         the offset is explicitly or implicitly 0
+         * @draft ICU 49
+         * @provisional This API might change or be removed in a future release.
+         */
+        public double getOffset() {
+            return offset;
+        }
+        /**
+         * @return the list of variants: the nested messages with their selection criteria
+         * @draft ICU 49
+         * @provisional This API might change or be removed in a future release.
+         */
+        public List<VariantNode> getVariants() {
+            return list;
+        }
+        /**
+         * Separates the variants by type.
+         * Intended for use with plural and select argument styles,
+         * not useful for choice argument styles.
+         *
+         * <p>Both parameters are used only for output, and are first cleared.
+         * @param numericVariants Variants with numeric-value selectors (if any) are added here.
+         *        Can be null for a select argument style.
+         * @param keywordVariants Variants with keyword selectors, except "other", are added here.
+         *        For a plural argument, if this list is empty after the call, then
+         *        all variants except "other" have explicit values
+         *        and PluralRules need not be called.
+         * @return the "other" variant (the first one if there are several),
+         *         null if none (choice style)
+         * @draft ICU 49
+         * @provisional This API might change or be removed in a future release.
+         */
+        public VariantNode getVariantsByType(List<VariantNode> numericVariants,
+                                             List<VariantNode> keywordVariants) {
+            if (numericVariants != null) {
+                numericVariants.clear();
+            }
+            keywordVariants.clear();
+            VariantNode other = null;
+            for (VariantNode variant : list) {
+                if (variant.isSelectorNumeric()) {
+                    numericVariants.add(variant);
+                } else if ("other".equals(variant.getSelector())) {
+                    if (other == null) {
+                        // Return the first "other" variant. (MessagePattern allows duplicates.)
+                        other = variant;
+                    }
+                } else {
+                    keywordVariants.add(variant);
+                }
+            }
+            return other;
+        }
+        /**
+         * {@inheritDoc}
+         * @draft ICU 49
+         * @provisional This API might change or be removed in a future release.
+         */
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            sb.append('(').append(argType.toString()).append(" style) ");
+            if (hasExplicitOffset()) {
+                sb.append("offset:").append(offset).append(' ');
+            }
+            return sb.append(list.toString()).toString();
+        }
+
+        private ComplexArgStyleNode(MessagePattern.ArgType argType) {
+            super();
+            this.argType = argType;
+        }
+        private void addVariant(VariantNode variant) {
+            list.add(variant);
+        }
+        private ComplexArgStyleNode freeze() {
+            list = Collections.unmodifiableList(list);
+            return this;
+        }
+
+        private MessagePattern.ArgType argType;
+        private double offset;
+        private boolean explicitOffset;
+        private List<VariantNode> list = new ArrayList<VariantNode>();
+    }
+
+    /**
+     * A Node representing a nested message (nested inside an argument)
+     * with its selection criterium.
+     * @draft ICU 49
+     * @provisional This API might change or be removed in a future release.
+     */
+    public static class VariantNode extends Node {
+        /**
+         * Returns the selector string.
+         * For example: A plural/select keyword ("few"), a plural explicit value ("=1"),
+         * a choice comparison operator ("#").
+         * @return the selector string
+         * @draft ICU 49
+         * @provisional This API might change or be removed in a future release.
+         */
+        public String getSelector() {
+            return selector;
+        }
+        /**
+         * @return true for choice variants and for plural explicit values
+         * @draft ICU 49
+         * @provisional This API might change or be removed in a future release.
+         */
+        public boolean isSelectorNumeric() {
+            return numericValue != MessagePattern.NO_NUMERIC_VALUE;
+        }
+        /**
+         * @return the selector's numeric value, or NO_NUMERIC_VALUE if !isSelectorNumeric()
+         * @draft ICU 49
+         * @provisional This API might change or be removed in a future release.
+         */
+        public double getSelectorValue() {
+            return numericValue;
+        }
+        /**
+         * @return the nested message
+         * @draft ICU 49
+         * @provisional This API might change or be removed in a future release.
+         */
+        public MessageNode getMessage() {
+            return msgNode;
+        }
+        /**
+         * {@inheritDoc}
+         * @draft ICU 49
+         * @provisional This API might change or be removed in a future release.
+         */
+        @Override
+        public String toString() {
+            StringBuilder sb = new StringBuilder();
+            if (isSelectorNumeric()) {
+                sb.append(numericValue).append(" (").append(selector).append(") {");
+            } else {
+                sb.append(selector).append(" {");
+            }
+            return sb.append(msgNode.toString()).append('}').toString();
+        }
+
+        private VariantNode() {
+            super();
+        }
+
+        private String selector;
+        private double numericValue = MessagePattern.NO_NUMERIC_VALUE;
+        private MessageNode msgNode;
+    }
+
+    private static MessageNode buildMessageNode(MessagePattern pattern, int start, int limit) {
+        int prevPatternIndex = pattern.getPart(start).getLimit();
+        MessageNode node = new MessageNode();
+        for (int i = start + 1;; ++i) {
+            MessagePattern.Part part = pattern.getPart(i);
+            int patternIndex = part.getIndex();
+            if (prevPatternIndex < patternIndex) {
+                node.addContentsNode(
+                        new TextNode(pattern.getPatternString().substring(prevPatternIndex,
+                                     patternIndex)));
+            }
+            if (i == limit) {
+                break;
+            }
+            MessagePattern.Part.Type partType = part.getType();
+            if (partType == MessagePattern.Part.Type.ARG_START) {
+                int argLimit = pattern.getLimitPartIndex(i);
+                node.addContentsNode(buildArgNode(pattern, i, argLimit));
+                i = argLimit;
+                part = pattern.getPart(i);
+            } else if (partType == MessagePattern.Part.Type.REPLACE_NUMBER) {
+                node.addContentsNode(MessageContentsNode.createReplaceNumberNode());
+                // else: ignore SKIP_SYNTAX and INSERT_CHAR parts.
+            }
+            prevPatternIndex = part.getLimit();
+        }
+        return node.freeze();
+    }
+
+    private static ArgNode buildArgNode(MessagePattern pattern, int start, int limit) {
+        ArgNode node = ArgNode.createArgNode();
+        MessagePattern.Part part = pattern.getPart(start);
+        MessagePattern.ArgType argType = node.argType = part.getArgType();
+        part = pattern.getPart(++start);  // ARG_NAME or ARG_NUMBER
+        node.name = pattern.getSubstring(part);
+        if (part.getType() == MessagePattern.Part.Type.ARG_NUMBER) {
+            node.number = part.getValue();
+        }
+        ++start;
+        switch(argType) {
+        case SIMPLE:
+            // ARG_TYPE
+            node.typeName = pattern.getSubstring(pattern.getPart(start++));
+            if (start < limit) {
+                // ARG_STYLE
+                node.style = pattern.getSubstring(pattern.getPart(start));
+            }
+            break;
+        case CHOICE:
+            node.typeName = "choice";
+            node.complexStyle = buildChoiceStyleNode(pattern, start, limit);
+            break;
+        case PLURAL:
+            node.typeName = "plural";
+            node.complexStyle = buildPluralStyleNode(pattern, start, limit);
+            break;
+        case SELECT:
+            node.typeName = "select";
+            node.complexStyle = buildSelectStyleNode(pattern, start, limit);
+            break;
+        default:
+            // NONE type, nothing else to do
+            break;
+        }
+        return node;
+    }
+
+    private static ComplexArgStyleNode buildChoiceStyleNode(MessagePattern pattern,
+                                                            int start, int limit) {
+        ComplexArgStyleNode node = new ComplexArgStyleNode(MessagePattern.ArgType.CHOICE);
+        while (start < limit) {
+            int valueIndex = start;
+            MessagePattern.Part part = pattern.getPart(start);
+            double value = pattern.getNumericValue(part);
+            start += 2;
+            int msgLimit = pattern.getLimitPartIndex(start);
+            VariantNode variant = new VariantNode();
+            variant.selector = pattern.getSubstring(pattern.getPart(valueIndex + 1));
+            variant.numericValue = value;
+            variant.msgNode = buildMessageNode(pattern, start, msgLimit);
+            node.addVariant(variant);
+            start = msgLimit + 1;
+        }
+        return node.freeze();
+    }
+
+    private static ComplexArgStyleNode buildPluralStyleNode(MessagePattern pattern,
+                                                            int start, int limit) {
+        ComplexArgStyleNode node = new ComplexArgStyleNode(MessagePattern.ArgType.PLURAL);
+        MessagePattern.Part offset = pattern.getPart(start);
+        if (offset.getType().hasNumericValue()) {
+            node.explicitOffset = true;
+            node.offset = pattern.getNumericValue(offset);
+            ++start;
+        }
+        while (start < limit) {
+            MessagePattern.Part selector = pattern.getPart(start++);
+            double value = MessagePattern.NO_NUMERIC_VALUE;
+            MessagePattern.Part part = pattern.getPart(start);
+            if (part.getType().hasNumericValue()) {
+                value = pattern.getNumericValue(part);
+                ++start;
+            }
+            int msgLimit = pattern.getLimitPartIndex(start);
+            VariantNode variant = new VariantNode();
+            variant.selector = pattern.getSubstring(selector);
+            variant.numericValue = value;
+            variant.msgNode = buildMessageNode(pattern, start, msgLimit);
+            node.addVariant(variant);
+            start = msgLimit + 1;
+        }
+        return node.freeze();
+    }
+
+    private static ComplexArgStyleNode buildSelectStyleNode(MessagePattern pattern,
+                                                            int start, int limit) {
+        ComplexArgStyleNode node = new ComplexArgStyleNode(MessagePattern.ArgType.SELECT);
+        while (start < limit) {
+            MessagePattern.Part selector = pattern.getPart(start++);
+            int msgLimit = pattern.getLimitPartIndex(start);
+            VariantNode variant = new VariantNode();
+            variant.selector = pattern.getSubstring(selector);
+            variant.msgNode = buildMessageNode(pattern, start, msgLimit);
+            node.addVariant(variant);
+            start = msgLimit + 1;
+        }
+        return node.freeze();
+    }
+}
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/MessagePatternUtilTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/format/MessagePatternUtilTest.java
new file mode 100644 (file)
index 0000000..148f24d
--- /dev/null
@@ -0,0 +1,508 @@
+/*
+*******************************************************************************
+*   Copyright (C) 2011, International Business Machines
+*   Corporation and others.  All Rights Reserved.
+*******************************************************************************
+*   created on: 2011aug12
+*   created by: Markus W. Scherer
+*/
+
+package com.ibm.icu.dev.test.format;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+import com.ibm.icu.text.MessagePattern;
+import com.ibm.icu.text.MessagePatternUtil;
+import com.ibm.icu.text.MessagePatternUtil.MessageNode;
+import com.ibm.icu.text.MessagePatternUtil.MessageContentsNode;
+import com.ibm.icu.text.MessagePatternUtil.TextNode;
+import com.ibm.icu.text.MessagePatternUtil.ArgNode;
+import com.ibm.icu.text.MessagePatternUtil.ComplexArgStyleNode;
+import com.ibm.icu.text.MessagePatternUtil.VariantNode;
+
+/**
+ * Test MessagePatternUtil (MessagePattern-as-tree-of-nodes API)
+ * by building parallel trees of nodes and verifying that they match.
+ */
+public final class MessagePatternUtilTest extends com.ibm.icu.dev.test.TestFmwk {
+    public static void main(String[] args) throws Exception {
+        new MessagePatternUtilTest().run(args);
+    }
+
+    // The following nested "Expect..." classes are used to build
+    // a tree structure parallel to what the MessagePatternUtil class builds.
+    // These nested test classes are not static so that they have access to TestFmwk methods.
+
+    private class ExpectMessageNode {
+        private ExpectMessageNode expectTextThatContains(String s) {
+            contents.add(new ExpectTextNode(s));
+            return this;
+        }
+        private ExpectMessageNode expectReplaceNumber() {
+            contents.add(new ExpectMessageContentsNode());
+            return this;
+        }
+        private ExpectMessageNode expectNoneArg(Object name) {
+            contents.add(new ExpectArgNode(name));
+            return this;
+        }
+        private ExpectMessageNode expectSimpleArg(Object name, String type) {
+            contents.add(new ExpectArgNode(name, type));
+            return this;
+        }
+        private ExpectMessageNode expectSimpleArg(Object name, String type, String style) {
+            contents.add(new ExpectArgNode(name, type, style));
+            return this;
+        }
+        private ExpectComplexArgNode expectChoiceArg(Object name) {
+            return expectComplexArg(name, MessagePattern.ArgType.CHOICE);
+        }
+        private ExpectComplexArgNode expectPluralArg(Object name) {
+            return expectComplexArg(name, MessagePattern.ArgType.PLURAL);
+        }
+        private ExpectComplexArgNode expectSelectArg(Object name) {
+            return expectComplexArg(name, MessagePattern.ArgType.SELECT);
+        }
+        private ExpectComplexArgNode expectComplexArg(Object name, MessagePattern.ArgType argType) {
+            ExpectComplexArgNode complexArg = new ExpectComplexArgNode(this, name, argType);
+            contents.add(complexArg);
+            return complexArg;
+        }
+        private ExpectComplexArgNode finishVariant() {
+            return parent;
+        }
+        private void checkMatches(MessageNode msg) {
+            // matches() prints all errors.
+            matches(msg);
+        }
+        private boolean matches(MessageNode msg) {
+            List<MessageContentsNode> msgContents = msg.getContents();
+            boolean ok = assertEquals("different numbers of MessageContentsNode",
+                                      contents.size(), msgContents.size());
+            if (ok) {
+                Iterator<MessageContentsNode> msgIter = msgContents.iterator();
+                for (ExpectMessageContentsNode ec : contents) {
+                    ok &= ec.matches(msgIter.next());
+                }
+            }
+            if (!ok) {
+                errln("error in message: " + msg.toString());
+            }
+            return ok;
+        }
+        private ExpectComplexArgNode parent;  // for finishVariant()
+        private List<ExpectMessageContentsNode> contents =
+            new ArrayList<ExpectMessageContentsNode>();
+    }
+
+    /**
+     * Base class for message contents nodes.
+     * Used directly for REPLACE_NUMBER nodes, subclassed for others.
+     */
+    private class ExpectMessageContentsNode {
+        protected boolean matches(MessageContentsNode c) {
+            return assertEquals("not a REPLACE_NUMBER node",
+                                MessageContentsNode.Type.REPLACE_NUMBER, c.getType());
+        }
+    }
+
+    private class ExpectTextNode extends ExpectMessageContentsNode {
+        private ExpectTextNode(String subString) {
+            this.subString = subString;
+        }
+        @Override
+        protected boolean matches(MessageContentsNode c) {
+            return
+                assertEquals("not a TextNode",
+                             MessageContentsNode.Type.TEXT, c.getType()) &&
+                assertTrue("TextNode does not contain \"" + subString + "\"",
+                           ((TextNode)c).getText().contains(subString));
+        }
+        private String subString;
+    }
+
+    private class ExpectArgNode extends ExpectMessageContentsNode {
+        private ExpectArgNode(Object name) {
+            this(name, null, null);
+        }
+        private ExpectArgNode(Object name, String type) {
+            this(name, type, null);
+        }
+        private ExpectArgNode(Object name, String type, String style) {
+            if (name instanceof String) {
+                this.name = (String)name;
+                this.number = -1;
+            } else {
+                this.number = (Integer)name;
+                this.name = Integer.toString(this.number);
+            }
+            if (type == null) {
+                argType = MessagePattern.ArgType.NONE;
+            } else {
+                argType = MessagePattern.ArgType.SIMPLE;
+            }
+            this.type = type;
+            this.style = style;
+        }
+        @Override
+        protected boolean matches(MessageContentsNode c) {
+            boolean ok =
+                assertEquals("not an ArgNode",
+                             MessageContentsNode.Type.ARG, c.getType());
+            if (!ok) {
+                return ok;
+            }
+            ArgNode arg = (ArgNode)c;
+            ok &= assertEquals("unexpected ArgNode argType",
+                               argType, arg.getArgType());
+            ok &= assertEquals("unexpected ArgNode arg name",
+                               name, arg.getName());
+            ok &= assertEquals("unexpected ArgNode arg number",
+                               number, arg.getNumber());
+            ok &= assertEquals("unexpected ArgNode arg type name",
+                               type, arg.getTypeName());
+            ok &= assertEquals("unexpected ArgNode arg style",
+                               style, arg.getSimpleStyle());
+            if (argType == MessagePattern.ArgType.NONE || argType == MessagePattern.ArgType.SIMPLE) {
+                ok &= assertNull("unexpected non-null complex style", arg.getComplexStyle());
+            }
+            return ok;
+        }
+        private String name;
+        private int number;
+        protected MessagePattern.ArgType argType;
+        private String type;
+        private String style;
+    }
+
+    private class ExpectComplexArgNode extends ExpectArgNode {
+        private ExpectComplexArgNode(ExpectMessageNode parent,
+                                     Object name, MessagePattern.ArgType argType) {
+            super(name,
+                  argType == MessagePattern.ArgType.CHOICE ? "choice" :
+                  argType == MessagePattern.ArgType.PLURAL ? "plural" : "select");
+            this.argType = argType;
+            this.parent = parent;
+        }
+        private ExpectComplexArgNode expectOffset(double offset) {
+            this.offset = offset;
+            explicitOffset = true;
+            return this;
+        }
+        private ExpectMessageNode expectVariant(String selector) {
+            ExpectVariantNode variant = new ExpectVariantNode(this, selector);
+            variants.add(variant);
+            return variant.msg;
+        }
+        private ExpectMessageNode expectVariant(String selector, double value) {
+            ExpectVariantNode variant = new ExpectVariantNode(this, selector, value);
+            variants.add(variant);
+            return variant.msg;
+        }
+        private ExpectMessageNode finishComplexArg() {
+            return parent;
+        }
+        @Override
+        protected boolean matches(MessageContentsNode c) {
+            boolean ok = super.matches(c);
+            if (!ok) {
+                return ok;
+            }
+            ArgNode arg = (ArgNode)c;
+            ComplexArgStyleNode complexStyle = arg.getComplexStyle();
+            ok &= assertNotNull("unexpected null complex style", complexStyle);
+            if (!ok) {
+                return ok;
+            }
+            ok &= assertEquals("unexpected complex-style argType",
+                               argType, complexStyle.getArgType());
+            ok &= assertEquals("unexpected complex-style hasExplicitOffset()",
+                               explicitOffset, complexStyle.hasExplicitOffset());
+            ok &= assertEquals("unexpected complex-style offset",
+                               offset, complexStyle.getOffset());
+            List<VariantNode> complexVariants = complexStyle.getVariants();
+            ok &= assertEquals("different number of variants",
+                               variants.size(), complexVariants.size());
+            if (!ok) {
+                return ok;
+            }
+            Iterator<VariantNode> complexIter = complexVariants.iterator();
+            for (ExpectVariantNode variant : variants) {
+                ok &= variant.matches(complexIter.next());
+            }
+            return ok;
+        }
+        private ExpectMessageNode parent;  // for finishComplexArg()
+        private boolean explicitOffset;
+        private double offset;
+        private List<ExpectVariantNode> variants = new ArrayList<ExpectVariantNode>();
+    }
+
+    private class ExpectVariantNode {
+        private ExpectVariantNode(ExpectComplexArgNode parent, String selector) {
+            this(parent, selector, MessagePattern.NO_NUMERIC_VALUE);
+        }
+        private ExpectVariantNode(ExpectComplexArgNode parent, String selector, double value) {
+            this.selector = selector;
+            numericValue = value;
+            msg = new ExpectMessageNode();
+            msg.parent = parent;
+        }
+        private boolean matches(VariantNode v) {
+            boolean ok = assertEquals("different selector strings",
+                                      selector, v.getSelector());
+            ok &= assertEquals("different selector strings",
+                               isSelectorNumeric(), v.isSelectorNumeric());
+            ok &= assertEquals("different selector strings",
+                               numericValue, v.getSelectorValue());
+            return ok & msg.matches(v.getMessage());
+        }
+        private boolean isSelectorNumeric() {
+            return numericValue != MessagePattern.NO_NUMERIC_VALUE;
+        }
+        private String selector;
+        private double numericValue;
+        private ExpectMessageNode msg;
+    }
+
+    // The actual tests start here. ---------------------------------------- ***
+    // Sample message strings are mostly from the MessagePatternUtilDemo.
+
+    public void TestHello() {
+        // No syntax.
+        MessageNode msg = MessagePatternUtil.buildMessageNode("Hello!");
+        ExpectMessageNode expect = new ExpectMessageNode().expectTextThatContains("Hello");
+        expect.checkMatches(msg);
+    }
+
+    public void TestHelloWithApos() {
+        // Literal ASCII apostrophe.
+        MessageNode msg = MessagePatternUtil.buildMessageNode("Hel'lo!");
+        ExpectMessageNode expect = new ExpectMessageNode().expectTextThatContains("Hel'lo");
+        expect.checkMatches(msg);
+    }
+
+    public void TestHelloWithQuote() {
+        // Apostrophe starts quoted literal text.
+        MessageNode msg = MessagePatternUtil.buildMessageNode("Hel'{o!");
+        ExpectMessageNode expect = new ExpectMessageNode().expectTextThatContains("Hel{o");
+        expect.checkMatches(msg);
+        // Terminating the quote should yield the same result.
+        msg = MessagePatternUtil.buildMessageNode("Hel'{'o!");
+        expect.checkMatches(msg);
+        // Double apostrophe inside quoted literal text still encodes a single apostrophe.
+        msg = MessagePatternUtil.buildMessageNode("a'{bc''de'f");
+        expect = new ExpectMessageNode().expectTextThatContains("a{bc'def");
+        expect.checkMatches(msg);
+    }
+
+    public void TestNoneArg() {
+        // Numbered argument.
+        MessageNode msg = MessagePatternUtil.buildMessageNode("abc{0}def");
+        ExpectMessageNode expect = new ExpectMessageNode().
+            expectTextThatContains("abc").expectNoneArg(0).expectTextThatContains("def");
+        expect.checkMatches(msg);
+        // Named argument.
+        msg = MessagePatternUtil.buildMessageNode("abc{ arg }def");
+        expect = new ExpectMessageNode().
+            expectTextThatContains("abc").expectNoneArg("arg").expectTextThatContains("def");
+        expect.checkMatches(msg);
+        // Numbered and named arguments.
+        msg = MessagePatternUtil.buildMessageNode("abc{1}def{arg}ghi");
+        expect = new ExpectMessageNode().
+            expectTextThatContains("abc").expectNoneArg(1).expectTextThatContains("def").
+            expectNoneArg("arg").expectTextThatContains("ghi");
+        expect.checkMatches(msg);
+    }
+
+    public void TestSimpleArg() {
+        MessageNode msg = MessagePatternUtil.buildMessageNode("a'{bc''de'f{0,number,g'hi''jk'l#}");
+        ExpectMessageNode expect = new ExpectMessageNode().
+            expectTextThatContains("a{bc'def").expectSimpleArg(0, "number", "g'hi''jk'l#");
+        expect.checkMatches(msg);
+    }
+
+    public void TestSelectArg() {
+        MessageNode msg = MessagePatternUtil.buildMessageNode(
+                "abc{2, number}ghi{3, select, xx {xxx} other {ooo}} xyz");
+        ExpectMessageNode expect = new ExpectMessageNode().
+            expectTextThatContains("abc").expectSimpleArg(2, "number").
+            expectTextThatContains("ghi").
+            expectSelectArg(3).
+                expectVariant("xx").expectTextThatContains("xxx").finishVariant().
+                expectVariant("other").expectTextThatContains("ooo").finishVariant().
+                finishComplexArg().
+            expectTextThatContains(" xyz");
+        expect.checkMatches(msg);
+    }
+
+    public void TestPluralArg() {
+        // Plural with only keywords.
+        MessageNode msg = MessagePatternUtil.buildMessageNode(
+                "abc{num_people, plural, offset:17 few{fff} other {oooo}}xyz");
+        ExpectMessageNode expect = new ExpectMessageNode().
+            expectTextThatContains("abc").
+            expectPluralArg("num_people").
+                expectOffset(17).
+                expectVariant("few").expectTextThatContains("fff").finishVariant().
+                expectVariant("other").expectTextThatContains("oooo").finishVariant().
+                finishComplexArg().
+            expectTextThatContains("xyz");
+        expect.checkMatches(msg);
+        // Plural with explicit-value selectors.
+        msg = MessagePatternUtil.buildMessageNode(
+                "abc{ num , plural , offset: 2 =1 {1} =-1 {-1} =3.14 {3.14} other {oo} }xyz");
+        expect = new ExpectMessageNode().
+            expectTextThatContains("abc").
+            expectPluralArg("num").
+                expectOffset(2).
+                expectVariant("=1", 1).expectTextThatContains("1").finishVariant().
+                expectVariant("=-1", -1).expectTextThatContains("-1").finishVariant().
+                expectVariant("=3.14", 3.14).expectTextThatContains("3.14").finishVariant().
+                expectVariant("other").expectTextThatContains("oo").finishVariant().
+                finishComplexArg().
+            expectTextThatContains("xyz");
+        expect.checkMatches(msg);
+        // Plural with number replacement.
+        msg = MessagePatternUtil.buildMessageNode(
+                "a_{0,plural,other{num=#'#'=#'#'={1,number,##}!}}_z");
+        expect = new ExpectMessageNode().
+            expectTextThatContains("a_").
+            expectPluralArg(0).
+                expectVariant("other").
+                    expectTextThatContains("num=").expectReplaceNumber().
+                    expectTextThatContains("#=").expectReplaceNumber().
+                    expectTextThatContains("#=").expectSimpleArg(1, "number", "##").
+                    expectTextThatContains("!").finishVariant().
+                finishComplexArg().
+            expectTextThatContains("_z");
+        expect.checkMatches(msg);
+    }
+
+    public void TestChoiceArg() {
+        MessageNode msg = MessagePatternUtil.buildMessageNode(
+                "a_{0,choice,-∞ #-inf|  5≤ five | 99 # ninety'|'nine  }_z");
+        ExpectMessageNode expect = new ExpectMessageNode().
+            expectTextThatContains("a_").
+            expectChoiceArg(0).
+                expectVariant("#", Double.NEGATIVE_INFINITY).
+                    expectTextThatContains("-inf").finishVariant().
+                expectVariant("≤", 5).expectTextThatContains(" five ").finishVariant().
+                expectVariant("#", 99).expectTextThatContains(" ninety|nine  ").finishVariant().
+                finishComplexArg().
+            expectTextThatContains("_z");
+        expect.checkMatches(msg);
+    }
+
+    public void TestComplexArgs() {
+        MessageNode msg = MessagePatternUtil.buildMessageNode(
+                "I don't {a,plural,other{w'{'on't #'#'}} and "+
+                "{b,select,other{shan't'}'}} '{'''know'''}' and "+
+                "{c,choice,0#can't'|'}"+
+                "{z,number,#'#'###.00'}'}.");
+        ExpectMessageNode expect = new ExpectMessageNode().
+            expectTextThatContains("I don't ").
+            expectPluralArg("a").
+                expectVariant("other").
+                    expectTextThatContains("w{on't ").expectReplaceNumber().
+                    expectTextThatContains("#").finishVariant().
+                finishComplexArg().
+            expectTextThatContains(" and ").
+            expectSelectArg("b").
+                expectVariant("other").expectTextThatContains("shan't}").finishVariant().
+                finishComplexArg().
+            expectTextThatContains(" {'know'} and ").
+            expectChoiceArg("c").
+                expectVariant("#", 0).expectTextThatContains("can't|").finishVariant().
+                finishComplexArg().
+            expectSimpleArg("z", "number", "#'#'###.00'}'").
+            expectTextThatContains(".");
+        expect.checkMatches(msg);
+    }
+
+    /**
+     * @return the text string of the VariantNode's message;
+     *         assumes that its message consists of only text
+     */
+    private String variantText(VariantNode v) {
+        return ((TextNode)v.getMessage().getContents().get(0)).getText();
+    }
+
+    public void TestPluralVariantsByType() {
+        MessageNode msg = MessagePatternUtil.buildMessageNode(
+                "{p,plural,a{A}other{O}=4{iv}b{B}other{U}=2{ii}}");
+        ExpectMessageNode expect = new ExpectMessageNode().
+        expectPluralArg("p").
+            expectVariant("a").expectTextThatContains("A").finishVariant().
+            expectVariant("other").expectTextThatContains("O").finishVariant().
+            expectVariant("=4", 4).expectTextThatContains("iv").finishVariant().
+            expectVariant("b").expectTextThatContains("B").finishVariant().
+            expectVariant("other").expectTextThatContains("U").finishVariant().
+            expectVariant("=2", 2).expectTextThatContains("ii").finishVariant().
+            finishComplexArg();
+        if (!expect.matches(msg)) {
+            return;
+        }
+        List<VariantNode> numericVariants = new ArrayList<VariantNode>();
+        List<VariantNode> keywordVariants = new ArrayList<VariantNode>();
+        VariantNode other =
+            ((ArgNode)msg.getContents().get(0)).getComplexStyle().
+            getVariantsByType(numericVariants, keywordVariants);
+        assertEquals("'other' selector", "other", other.getSelector());
+        assertEquals("message string of first 'other'", "O", variantText(other));
+
+        assertEquals("numericVariants.size()", 2, numericVariants.size());
+        VariantNode v = numericVariants.get(0);
+        assertEquals("numericVariants[0] selector", "=4", v.getSelector());
+        assertEquals("numericVariants[0] selector value", 4., v.getSelectorValue());
+        assertEquals("numericVariants[0] text", "iv", variantText(v));
+        v = numericVariants.get(1);
+        assertEquals("numericVariants[1] selector", "=2", v.getSelector());
+        assertEquals("numericVariants[1] selector value", 2., v.getSelectorValue());
+        assertEquals("numericVariants[1] text", "ii", variantText(v));
+
+        assertEquals("keywordVariants.size()", 2, keywordVariants.size());
+        v = keywordVariants.get(0);
+        assertEquals("keywordVariants[0] selector", "a", v.getSelector());
+        assertFalse("keywordVariants[0].isSelectorNumeric()", v.isSelectorNumeric());
+        assertEquals("keywordVariants[0] text", "A", variantText(v));
+        v = keywordVariants.get(1);
+        assertEquals("keywordVariants[1] selector", "b", v.getSelector());
+        assertFalse("keywordVariants[1].isSelectorNumeric()", v.isSelectorNumeric());
+        assertEquals("keywordVariants[1] text", "B", variantText(v));
+    }
+
+    public void TestSelectVariantsByType() {
+        MessageNode msg = MessagePatternUtil.buildMessageNode(
+                "{s,select,a{A}other{O}b{B}other{U}}");
+        ExpectMessageNode expect = new ExpectMessageNode().
+        expectSelectArg("s").
+            expectVariant("a").expectTextThatContains("A").finishVariant().
+            expectVariant("other").expectTextThatContains("O").finishVariant().
+            expectVariant("b").expectTextThatContains("B").finishVariant().
+            expectVariant("other").expectTextThatContains("U").finishVariant().
+            finishComplexArg();
+        if (!expect.matches(msg)) {
+            return;
+        }
+        // Check that we can use numericVariants = null.
+        List<VariantNode> keywordVariants = new ArrayList<VariantNode>();
+        VariantNode other =
+            ((ArgNode)msg.getContents().get(0)).getComplexStyle().
+            getVariantsByType(null, keywordVariants);
+        assertEquals("'other' selector", "other", other.getSelector());
+        assertEquals("message string of first 'other'", "O", variantText(other));
+
+        assertEquals("keywordVariants.size()", 2, keywordVariants.size());
+        VariantNode v = keywordVariants.get(0);
+        assertEquals("keywordVariants[0] selector", "a", v.getSelector());
+        assertFalse("keywordVariants[0].isSelectorNumeric()", v.isSelectorNumeric());
+        assertEquals("keywordVariants[0] text", "A", variantText(v));
+        v = keywordVariants.get(1);
+        assertEquals("keywordVariants[1] selector", "b", v.getSelector());
+        assertFalse("keywordVariants[1].isSelectorNumeric()", v.isSelectorNumeric());
+        assertEquals("keywordVariants[1] text", "B", variantText(v));
+    }
+}
index e8865bd25ff63c589512c7b9f63882c2ba8c909e..7d50f03ad720e34aa9eb0062f6e539ed837fb905 100644 (file)
@@ -1,6 +1,6 @@
 /*
  *******************************************************************************
- * Copyright (c) 2004-2010, International Business Machines
+ * Copyright (c) 2004-2011, International Business Machines
  * Corporation and others.  All Rights Reserved.
  * Copyright (C) 2010 , Yahoo! Inc.                                            
  *******************************************************************************
@@ -122,6 +122,7 @@ public class TestAll extends TestGroup {
             super(new String[] {
                 "TestMessageFormat",
                 "MessageRegression",
+                "MessagePatternUtilTest",
             });
         }
     }