variableOptions) {
+ return format(toFormat, variableOptions).toString();
+ }
+ }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/message2/MessageFormatter.java b/icu4j/main/classes/core/src/com/ibm/icu/message2/MessageFormatter.java
new file mode 100644
index 000000000000..7c9265ad03e7
--- /dev/null
+++ b/icu4j/main/classes/core/src/com/ibm/icu/message2/MessageFormatter.java
@@ -0,0 +1,338 @@
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package com.ibm.icu.message2;
+
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * {@code MessageFormatter} is the next iteration of {@link com.ibm.icu.text.MessageFormat}.
+ *
+ * This new version builds on what we learned from using {@code MessageFormat} for 20 years
+ * in various environments, either exposed "as is" or as a base for other public APIs.
+ *
+ * It is more modular, easier to backport, and provides extension points to add new
+ * formatters and selectors without having to modify the specification.
+ *
+ * We will be able to add formatters for intervals, relative times, lists, measurement units,
+ * people names, and more, and support custom formatters implemented by developers
+ * outside of ICU itself, for company or even product specific needs.
+ *
+ * MessageFormat 2 will support more complex grammatical features, such as gender, inflections,
+ * and tagging parts of the message for style changes or speech.
+ *
+ * The reasoning for this effort is shared in the
+ * “Why
+ * MessageFormat needs a successor” document.
+ *
+ * The “MessageFormat 2” project, which develops the new data model, semantics, and syntax,
+ * is hosted on GitHub.
+ *
+ * The current specification for the syntax and data model can be found
+ * here.
+ *
+ * This tech preview implements enough of the {@code MessageFormat} functions to be useful,
+ * but the final set of functions and the parameters accepted by those functions is not yet finalized.
+ *
+ * These are the functions interpreted right now:
+ *
+ *
+ *
+ * | {@code datetime} |
+ * Similar to the ICU {@code "date"} and {@code "time"}. |
+ *
+ *
+ * {@code datestyle} and {@code timestyle}
+ * Similar to {@code argStyle : short | medium | long | full}.
+ * Same values are accepted, but we can use both in one placeholder,
+ * for example {$due :datetime datestyle=full timestyle=long}.
+ * |
+ *
+ * {@code pattern}
+ * Similar to {@code argStyle = argStyleText}.
+ * This is bad i18n practice, and will probably be dropped.
+ * This is included just to support migration to MessageFormat 2.
+ * |
+ *
+ * {@code skeleton}
+ * Same as {@code argStyle = argSkeletonText}.
+ * These are the date/time skeletons as supported by {@link com.ibm.icu.text.SimpleDateFormat}.
+ * |
+ *
+ *
+ * | {@code number} |
+ * Similar to the ICU "number". |
+ *
+ *
+ * {@code skeleton}
+ * These are the number skeletons as supported by {@link com.ibm.icu.number.NumberFormatter}. |
+ *
+ * {@code minimumFractionDigits}
+ * Only implemented to be able to pass the unit tests from the ECMA tech preview implementation,
+ * which prefers options bags to skeletons.
+ * TBD if the final {@number} function will support skeletons, option backs, or both. |
+ *
+ * {@code offset}
+ * Used to support plural with an offset. |
+ *
+ * | {@code identity} | Returns the direct string value of the argument (calling {@code toString()}). |
+ *
+ *
+ * | {@code plural} |
+ * Similar to the ICU {@code "plural"}. |
+ *
+ *
+ * {@code skeleton}
+ * These are the number skeletons as supported by {@link com.ibm.icu.number.NumberFormatter}.
+ * Can also be indirect, from a local variable of type {@code number} (recommended). |
+ *
+ * {@code offset}
+ * Used to support plural with an offset.
+ * Can also be indirect, from a local variable of type {@code number} (recommended). |
+ *
+ *
+ * | {@code selectordinal} |
+ * Similar to the ICU {@code "selectordinal"}.
+ * For now it accepts the same parameters as "plural", although there is no use case for them.
+ * TBD if this will be merged into "plural" (with some {@code kind} option) or not. |
+ *
+ * | {@code select} | Literal match, same as the ICU4 {@code "select"}. |
+ *
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+@Deprecated
+public class MessageFormatter {
+ private final Locale locale;
+ private final String pattern;
+ private final Mf2FunctionRegistry functionRegistry;
+ private final Mf2DataModel dataModel;
+ private final Mf2DataModelFormatter modelFormatter;
+
+ private MessageFormatter(Builder builder) {
+ this.locale = builder.locale;
+ this.functionRegistry = builder.functionRegistry;
+ if ((builder.pattern == null && builder.dataModel == null)
+ || (builder.pattern != null && builder.dataModel != null)) {
+ throw new IllegalArgumentException("You need to set either a pattern, or a dataModel, but not both.");
+ }
+
+ if (builder.dataModel != null) {
+ this.dataModel = builder.dataModel;
+ this.pattern = Mf2Serializer.dataModelToString(this.dataModel);
+ } else {
+ this.pattern = builder.pattern;
+ Mf2Serializer tree = new Mf2Serializer();
+ Mf2Parser parser = new Mf2Parser(pattern, tree);
+ try {
+ parser.parse_Message();
+ dataModel = tree.build();
+ } catch (Mf2Parser.ParseException pe) {
+ throw new IllegalArgumentException(
+ "Parse error:\n"
+ + "Message: <<" + pattern + ">>\n"
+ + "Error:" + parser.getErrorMessage(pe) + "\n");
+ }
+ }
+ modelFormatter = new Mf2DataModelFormatter(dataModel, locale, functionRegistry);
+ }
+
+ /**
+ * Creates a builder.
+ *
+ * @return the Builder.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Get the locale to use for all the formatting and selections in
+ * the current {@code MessageFormatter}.
+ *
+ * @return the locale.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Locale getLocale() {
+ return locale;
+ }
+
+ /**
+ * Get the pattern (the serialized message in MessageFormat 2 syntax) of
+ * the current {@code MessageFormatter}.
+ *
+ * If the {@code MessageFormatter} was created from an {@link Mf2DataModel}
+ * the this string is generated from that model.
+ *
+ * @return the pattern.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public String getPattern() {
+ return pattern;
+ }
+
+ /**
+ * Give public access to the message data model.
+ *
+ * This data model is similar to the functionality we have today
+ * in {@link com.ibm.icu.text.MessagePatternUtil} maybe even a bit more higher level.
+ *
+ * We can also imagine a model where one parses the string syntax, takes the data model,
+ * modifies it, and then uses that modified model to create a {@code MessageFormatter}.
+ *
+ * @return the data model.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Mf2DataModel getDataModel() {
+ return dataModel;
+ }
+
+ /**
+ * Formats a map of objects by iterating over the MessageFormat's pattern,
+ * with the plain text “as is” and the arguments replaced by the formatted objects.
+ *
+ * @param arguments a map of objects to be formatted and substituted.
+ * @return the string representing the message with parameters replaced.
+ *
+ * @throws IllegalArgumentException when something goes wrong
+ * (for example wrong argument type, or null arguments, etc.)
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public String formatToString(Map arguments) {
+ return modelFormatter.format(arguments);
+ }
+
+ /**
+ * Not yet implemented: formats a map of objects by iterating over the MessageFormat's
+ * pattern, with the plain text “as is” and the arguments replaced by the formatted objects.
+ *
+ * @param arguments a map of objects to be formatted and substituted.
+ * @return the {@link FormattedMessage} class representing the message with parameters replaced.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public FormattedMessage format(Map arguments) {
+ throw new RuntimeException("Not yet implemented.");
+ }
+
+ /**
+ * A {@code Builder} used to build instances of {@link MessageFormatter}.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public static class Builder {
+ private Locale locale = Locale.getDefault(Locale.Category.FORMAT);
+ private String pattern = null;
+ private Mf2FunctionRegistry functionRegistry = Mf2FunctionRegistry.builder().build();
+ private Mf2DataModel dataModel = null;
+
+ // Prevent direct creation
+ private Builder() {
+ }
+
+ /**
+ * Sets the locale to use for all formatting and selection operations.
+ *
+ * @param locale the locale to set.
+ * @return the builder, for fluent use.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Builder setLocale(Locale locale) {
+ this.locale = locale;
+ return this;
+ }
+
+ /**
+ * Sets the pattern (in MessageFormat 2 syntax) used to create the message.
+ * It conflicts with the data model, so it will reset it (the last call on setter wins).
+ *
+ * @param pattern the pattern to set.
+ * @return the builder, for fluent use.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Builder setPattern(String pattern) {
+ this.pattern = pattern;
+ this.dataModel = null;
+ return this;
+ }
+
+ /**
+ * Sets an instance of {@link Mf2FunctionRegistry} that should register any
+ * custom functions used by the message.
+ *
+ * There is no need to do this in order to use standard functions
+ * (for example date / time / number formatting, plural / ordinal / literal selection).
+ * The exact set of standard functions, with the types they format and the options
+ * they accept is still TBD.
+ *
+ * @param functionRegistry the function registry to set.
+ * @return the builder, for fluent use.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Builder setFunctionRegistry(Mf2FunctionRegistry functionRegistry) {
+ this.functionRegistry = functionRegistry;
+ return this;
+ }
+
+ /**
+ * Sets the data model used to create the message.
+ * It conflicts with the pattern, so it will reset it (the last call on setter wins).
+ *
+ * @param dataModel the pattern to set.
+ * @return the builder, for fluent use.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Builder setDataModel(Mf2DataModel dataModel) {
+ this.dataModel = dataModel;
+ this.pattern = null;
+ return this;
+ }
+
+ /**
+ * Builds an instance of {@link MessageFormatter}.
+ *
+ * @return the {@link MessageFormatter} created.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public MessageFormatter build() {
+ return new MessageFormatter(this);
+ }
+ }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/message2/Mf2DataModel.java b/icu4j/main/classes/core/src/com/ibm/icu/message2/Mf2DataModel.java
new file mode 100644
index 000000000000..7fb30e5247d9
--- /dev/null
+++ b/icu4j/main/classes/core/src/com/ibm/icu/message2/Mf2DataModel.java
@@ -0,0 +1,856 @@
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package com.ibm.icu.message2;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.StringJoiner;
+
+/**
+ * This maps closely to the official specification.
+ * Since it is not final, we will not add javadoc everywhere.
+ *
+ * See the
+ * description of the syntax with examples and use cases and the corresponding
+ * EBNF.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+@Deprecated
+@SuppressWarnings("javadoc")
+public class Mf2DataModel {
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public static class SelectorKeys {
+ private final List keys;
+
+ private SelectorKeys(Builder builder) {
+ keys = new ArrayList<>();
+ keys.addAll(builder.keys);
+ }
+
+ /**
+ * Creates a builder.
+ *
+ * @return the Builder.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public List getKeys() {
+ return Collections.unmodifiableList(keys);
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ @Override
+ public String toString() {
+ StringJoiner result = new StringJoiner(" ");
+ for (String key : keys) {
+ result.add(key);
+ }
+ return result.toString();
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public static class Builder {
+ private final List keys = new ArrayList<>();
+
+ // Prevent direct creation
+ private Builder() {
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Builder add(String key) {
+ keys.add(key);
+ return this;
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Builder addAll(Collection otherKeys) {
+ this.keys.addAll(otherKeys);
+ return this;
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public SelectorKeys build() {
+ return new SelectorKeys(this);
+ }
+ }
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public static class Pattern {
+ private final List parts;
+
+ private Pattern(Builder builder) {
+ parts = new ArrayList<>();
+ parts.addAll(builder.parts);
+ }
+
+ /**
+ * Creates a builder.
+ *
+ * @return the Builder.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public List getParts() {
+ return parts;
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ result.append("{");
+ for (Part part : parts) {
+ result.append(part);
+ }
+ result.append("}");
+ return result.toString();
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public static class Builder {
+ private final List parts = new ArrayList<>();
+
+ // Prevent direct creation
+ private Builder() {
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Builder add(Part part) {
+ parts.add(part);
+ return this;
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Builder addAll(Collection otherParts) {
+ parts.addAll(otherParts);
+ return this;
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Pattern build() {
+ return new Pattern(this);
+ }
+
+ }
+ }
+
+ /**
+ * No functional role, this is only to be able to say that a message is a sequence of Part(s),
+ * and that plain text {@link Text} and {@link Expression} are Part(s).
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public interface Part {
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public static class Text implements Part {
+ private final String value;
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ private Text(Builder builder) {
+ this(builder.value);
+ }
+
+ /**
+ * Creates a builder.
+ *
+ * @return the Builder.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Text(String value) {
+ this.value = value;
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public String getValue() {
+ return value;
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ @Override
+ public String toString() {
+ return value;
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public static class Builder {
+ private String value;
+
+ // Prevent direct creation
+ private Builder() {
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Builder setValue(String value) {
+ this.value = value;
+ return this;
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Text build() {
+ return new Text(this);
+ }
+ }
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public static class Expression implements Part {
+ private final Value operand; // Literal | Variable
+ private final String functionName;
+ private final Map options;
+ Formatter formatter = null;
+
+ private Expression(Builder builder) {
+ this.operand = builder.operand;
+ this.functionName = builder.functionName;
+ this.options = builder.options;
+ }
+
+ /**
+ * Creates a builder.
+ *
+ * @return the Builder.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Value getOperand() {
+ return operand;
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public String getFunctionName() {
+ return functionName;
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Map getOptions() {
+ return options;
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ result.append("{");
+ if (operand != null) {
+ result.append(operand);
+ }
+ if (functionName != null) {
+ result.append(" :").append(functionName);
+ }
+ for (Entry option : options.entrySet()) {
+ result.append(" ").append(option.getKey()).append("=").append(option.getValue());
+ }
+ result.append("}");
+ return result.toString();
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public static class Builder {
+ private Value operand = null;
+ private String functionName = null;
+ private final OrderedMap options = new OrderedMap<>();
+
+ // Prevent direct creation
+ private Builder() {
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Builder setOperand(Value operand) {
+ this.operand = operand;
+ return this;
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Builder setFunctionName(String functionName) {
+ this.functionName = functionName;
+ return this;
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Builder addOption(String key, Value value) {
+ options.put(key, value);
+ return this;
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Builder addOptions(Map otherOptions) {
+ options.putAll(otherOptions);
+ return this;
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Expression build() {
+ return new Expression(this);
+ }
+ }
+ }
+
+// public static class Placeholder extends Expression implements Part {
+// public Placeholder(Builder builder) {
+// super(builder);
+// }
+// }
+
+ /**
+ * A Value can be either a Literal, or a Variable, but not both.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public static class Value {
+ private final String literal;
+ private final String variableName;
+
+ private Value(Builder builder) {
+ this.literal = builder.literal;
+ this.variableName = builder.variableName;
+// this(builder.literal, builder.variableName);
+ }
+
+ /**
+ * Creates a builder.
+ *
+ * @return the Builder.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public String getLiteral() {
+ return literal;
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public String getVariableName() {
+ return variableName;
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public boolean isLiteral() {
+ return literal != null;
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public boolean isVariable() {
+ return variableName != null;
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ @Override
+ public String toString() {
+ return isLiteral() ? "(" + literal + ")" : "$" + variableName;
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public static class Builder {
+ private String literal;
+ private String variableName;
+
+ // Prevent direct creation
+ private Builder() {
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Builder setLiteral(String literal) {
+ this.literal = literal;
+ this.variableName = null;
+ return this;
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Builder setVariableName(String variableName) {
+ this.variableName = variableName;
+ this.literal = null;
+ return this;
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Value build() {
+ return new Value(this);
+ }
+ }
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public static class Variable {
+ private final String name;
+
+ private Variable(Builder builder) {
+ this.name = builder.name;
+ }
+
+ /**
+ * Creates a builder.
+ *
+ * @return the Builder.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public static class Builder {
+ private String name;
+
+ // Prevent direct creation
+ private Builder() {
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Builder setName(String name) {
+ this.name = name;
+ return this;
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Variable build() {
+ return new Variable(this);
+ }
+ }
+ }
+
+ /**
+ * This is only to not force LinkedHashMap on the public API.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public static class OrderedMap extends LinkedHashMap {
+ private static final long serialVersionUID = -7049361727790825496L;
+ }
+
+ private final OrderedMap localVariables;
+ private final List selectors;
+ private final OrderedMap variants;
+ private final Pattern pattern;
+
+ private Mf2DataModel(Builder builder) {
+ this.localVariables = builder.localVariables;
+ this.selectors = builder.selectors;
+ this.variants = builder.variants;
+ this.pattern = builder.pattern;
+ }
+
+ /**
+ * Creates a builder.
+ *
+ * @return the Builder.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public OrderedMap getLocalVariables() {
+ return localVariables;
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public List getSelectors() {
+ return selectors;
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public OrderedMap getVariants() {
+ return variants;
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Pattern getPattern() {
+ return pattern;
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ @Override
+ public String toString() {
+ StringBuilder result = new StringBuilder();
+ for (Entry lv : localVariables.entrySet()) {
+ result.append("let $").append(lv.getKey());
+ result.append(" = ");
+ result.append(lv.getValue());
+ result.append("\n");
+ }
+ if (!selectors.isEmpty()) {
+ result.append("match");
+ for (Expression e : this.selectors) {
+ result.append(" ").append(e);
+ }
+ result.append("\n");
+ for (Entry variant : variants.entrySet()) {
+ result.append(" when ").append(variant.getKey());
+ result.append(" ");
+ result.append(variant.getValue());
+ result.append("\n");
+ }
+ } else {
+ result.append(pattern);
+ }
+ return result.toString();
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public static class Builder {
+ private final OrderedMap localVariables = new OrderedMap<>(); // declaration*
+ private final List selectors = new ArrayList<>();
+ private final OrderedMap variants = new OrderedMap<>();
+ private Pattern pattern = Pattern.builder().build();
+
+ // Prevent direct creation
+ private Builder() {
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Builder addLocalVariable(String variableName, Expression expression) {
+ this.localVariables.put(variableName, expression);
+ return this;
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Builder addLocalVariables(OrderedMap otherLocalVariables) {
+ this.localVariables.putAll(otherLocalVariables);
+ return this;
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Builder addSelector(Expression otherSelector) {
+ this.selectors.add(otherSelector);
+ return this;
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Builder addSelectors(List otherSelectors) {
+ this.selectors.addAll(otherSelectors);
+ return this;
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Builder addVariant(SelectorKeys keys, Pattern newPattern) {
+ this.variants.put(keys, newPattern);
+ return this;
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Builder addVariants(OrderedMap otherVariants) {
+ this.variants.putAll(otherVariants);
+ return this;
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Builder setPattern(Pattern pattern) {
+ this.pattern = pattern;
+ return this;
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Mf2DataModel build() {
+ return new Mf2DataModel(this);
+ }
+ }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/message2/Mf2DataModelFormatter.java b/icu4j/main/classes/core/src/com/ibm/icu/message2/Mf2DataModelFormatter.java
new file mode 100644
index 000000000000..bf73791d2004
--- /dev/null
+++ b/icu4j/main/classes/core/src/com/ibm/icu/message2/Mf2DataModelFormatter.java
@@ -0,0 +1,280 @@
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package com.ibm.icu.message2;
+
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import com.ibm.icu.message2.Mf2DataModel.Expression;
+import com.ibm.icu.message2.Mf2DataModel.Part;
+import com.ibm.icu.message2.Mf2DataModel.Pattern;
+import com.ibm.icu.message2.Mf2DataModel.SelectorKeys;
+import com.ibm.icu.message2.Mf2DataModel.Text;
+import com.ibm.icu.message2.Mf2DataModel.Value;
+import com.ibm.icu.util.Calendar;
+import com.ibm.icu.util.CurrencyAmount;
+
+/**
+ * Takes an {@link Mf2DataModel} and formats it to a {@link String}
+ * (and later on we will also implement formatting to a {@code FormattedMessage}).
+ */
+// TODO: move this in the MessageFormatter
+class Mf2DataModelFormatter {
+ private final Locale locale;
+ private final Mf2DataModel dm;
+
+ final Mf2FunctionRegistry standardFunctions;
+ final Mf2FunctionRegistry customFunctions;
+ private static final Mf2FunctionRegistry EMPTY_REGISTY = Mf2FunctionRegistry.builder().build();
+
+ Mf2DataModelFormatter(Mf2DataModel dm, Locale locale, Mf2FunctionRegistry customFunctionRegistry) {
+ this.locale = locale;
+ this.dm = dm;
+ this.customFunctions = customFunctionRegistry == null ? EMPTY_REGISTY : customFunctionRegistry;
+
+ standardFunctions = Mf2FunctionRegistry.builder()
+ // Date/time formatting
+ .setFormatter("datetime", new DateTimeFormatterFactory())
+ .setDefaultFormatterNameForType(Date.class, "datetime")
+ .setDefaultFormatterNameForType(Calendar.class, "datetime")
+
+ // Number formatting
+ .setFormatter("number", new NumberFormatterFactory())
+ .setDefaultFormatterNameForType(Integer.class, "number")
+ .setDefaultFormatterNameForType(Double.class, "number")
+ .setDefaultFormatterNameForType(Number.class, "number")
+ .setDefaultFormatterNameForType(CurrencyAmount.class, "number")
+
+ // Format that returns "to string"
+ .setFormatter("identity", new IdentityFormatterFactory())
+ .setDefaultFormatterNameForType(String.class, "identity")
+ .setDefaultFormatterNameForType(CharSequence.class, "identity")
+
+ // Register the standard selectors
+ .setSelector("plural", new PluralSelectorFactory("cardinal"))
+ .setSelector("selectordinal", new PluralSelectorFactory("ordinal"))
+ .setSelector("select", new TextSelectorFactory())
+ .setSelector("gender", new TextSelectorFactory())
+
+ .build();
+ }
+
+ private static Map mf2OptToFixedOptions(Map options) {
+ Map result = new HashMap<>();
+ for (Entry option : options.entrySet()) {
+ Value value = option.getValue();
+ if (value.isLiteral()) {
+ result.put(option.getKey(), value.getLiteral());
+ }
+ }
+ return result;
+ }
+
+ private Map mf2OptToVariableOptions(Map options, Map arguments) {
+ Map result = new HashMap<>();
+ for (Entry option : options.entrySet()) {
+ Value value = option.getValue();
+ if (value.isVariable()) {
+ result.put(option.getKey(), variableToObjectEx(value, arguments));
+ }
+ }
+ return result;
+ }
+
+ FormatterFactory getFormattingFunctionFactoryByName(Object toFormat, String functionName) {
+ // Get a function name from the type of the object to format
+ if (functionName == null || functionName.isEmpty()) {
+ if (toFormat == null) {
+ // The object to format is null, and no function provided.
+ return null;
+ }
+ Class> clazz = toFormat.getClass();
+ functionName = standardFunctions.getDefaultFormatterNameForType(clazz);
+ if (functionName == null) {
+ functionName = customFunctions.getDefaultFormatterNameForType(clazz);
+ }
+ if (functionName == null) {
+ throw new IllegalArgumentException("Object to format without a function, and unknown type: "
+ + toFormat.getClass().getName());
+ }
+ }
+
+ FormatterFactory func = standardFunctions.getFormatter(functionName);
+ if (func == null) {
+ func = customFunctions.getFormatter(functionName);
+ if (func == null) {
+ throw new IllegalArgumentException("Can't find an implementation for function: '"
+ + functionName + "'");
+ }
+ }
+ return func;
+ }
+
+ String format(Map arguments) {
+ List selectors = dm.getSelectors();
+ Pattern patternToRender = selectors.isEmpty()
+ ? dm.getPattern()
+ : findBestMatchingPattern(selectors, arguments);
+
+ StringBuilder result = new StringBuilder();
+ for (Part part : patternToRender.getParts()) {
+ if (part instanceof Text) {
+ result.append(part);
+ } else if (part instanceof Expression) { // Placeholder is an Expression
+ FormattedPlaceholder fp = formatPlaceholder((Expression) part, arguments, false);
+ result.append(fp.toString());
+ } else {
+ throw new IllegalArgumentException("Unknown part type: " + part);
+ }
+ }
+ return result.toString();
+ }
+
+ private Pattern findBestMatchingPattern(List selectors, Map arguments) {
+ Pattern patternToRender = null;
+
+ // Collect all the selector functions in an array, to reuse
+ List selectorFunctions = new ArrayList<>(selectors.size());
+ for (Expression selector : selectors) {
+ String functionName = selector.getFunctionName();
+ SelectorFactory funcFactory = standardFunctions.getSelector(functionName);
+ if (funcFactory == null) {
+ funcFactory = customFunctions.getSelector(functionName);
+ }
+ if (funcFactory != null) {
+ Map opt = mf2OptToFixedOptions(selector.getOptions());
+ selectorFunctions.add(funcFactory.createSelector(locale, opt));
+ } else {
+ throw new IllegalArgumentException("Unknown selector type: " + functionName);
+ }
+ }
+ // This should not be possible, we added one function for each selector, or we have thrown an exception.
+ // But just in case someone removes the throw above?
+ if (selectorFunctions.size() != selectors.size()) {
+ throw new IllegalArgumentException("Something went wrong, not enough selector functions, "
+ + selectorFunctions.size() + " vs. " + selectors.size());
+ }
+
+ // Iterate "vertically", through all variants
+ for (Entry variant : dm.getVariants().entrySet()) {
+ int maxCount = selectors.size();
+ List keysToCheck = variant.getKey().getKeys();
+ if (selectors.size() != keysToCheck.size()) {
+ throw new IllegalArgumentException("Mismatch between the number of selectors and the number of keys: "
+ + selectors.size() + " vs. " + keysToCheck.size());
+ }
+ boolean matches = true;
+ // Iterate "horizontally", through all matching functions and keys
+ for (int i = 0; i < maxCount; i++) {
+ Expression selector = selectors.get(i);
+ String valToCheck = keysToCheck.get(i);
+ Selector func = selectorFunctions.get(i);
+ Map options = mf2OptToVariableOptions(selector.getOptions(), arguments);
+ if (!func.matches(variableToObjectEx(selector.getOperand(), arguments), valToCheck, options)) {
+ matches = false;
+ break;
+ }
+ }
+ if (matches) {
+ patternToRender = variant.getValue();
+ break;
+ }
+ }
+
+ // TODO: check that there was an entry with all the keys set to `*`
+ // And should do that only once, when building the data model.
+ if (patternToRender == null) {
+ // If there was a case with all entries in the keys `*` this should not happen
+ throw new IllegalArgumentException("The selection went wrong, cannot select any option.");
+ }
+
+ return patternToRender;
+ }
+
+ /*
+ * Pass a level to prevent local variables calling each-other recursively:
+ *
+ *
+ * let $l1 = {$l4 :number}
+ * let $l2 = {$l1 :number}
+ * let $l3 = {$l2 :number}
+ * let $l4 = {$l3 :number}
+ *
+ *
+ * We can keep track of the calls (complicated and expensive).
+ * Or we can forbid the use of variables before they are declared, but that is not in the spec (yet?).
+ */
+ private Object variableToObjectEx(Value value, Map arguments) {
+ if (value == null) { // function only
+ return null;
+ }
+ // We have an operand. Can be literal, local var, or argument.
+ if (value.isLiteral()) {
+ return value.getLiteral();
+ } else if (value.isVariable()) {
+ String varName = value.getVariableName();
+ Expression localPh = dm.getLocalVariables().get(varName);
+ if (localPh != null) {
+ return formatPlaceholder(localPh, arguments, false);
+ }
+ return arguments.get(varName);
+ } else {
+ throw new IllegalArgumentException("Invalid operand type " + value);
+ }
+ }
+
+ private FormattedPlaceholder formatPlaceholder(Expression ph, Map arguments, boolean localExpression) {
+ Object toFormat;
+ Value operand = ph.getOperand();
+ if (operand == null) { // function only, "...{:currentOs option=value}..."
+ toFormat = null;
+ } else {
+ // We have an operand. Can be literal, local var, or argument.
+ if (operand.isLiteral()) { // "...{(1234.56) :number}..."
+ // If it is a literal, return the string itself
+ toFormat = operand.getLiteral();
+ } else if (operand.isVariable()) {
+ String varName = operand.getVariableName();
+ if (!localExpression) {
+ Expression localPh = dm.getLocalVariables().get(varName);
+ if (localPh != null) {
+ // If it is a local variable, we need to format that (recursive)
+ // TODO: See if there is any danger to eval the local variables only once
+ // (on demand in case the local var is not used, for example in a select)
+ return formatPlaceholder(localPh, arguments, true);
+ }
+ }
+ // Return the object in the argument bag.
+ toFormat = arguments.get(varName);
+ // toFormat might still be null here.
+ } else {
+ throw new IllegalArgumentException("Invalid operand type " + ph.getOperand());
+ }
+ }
+
+ if (ph.formatter == null) {
+ FormatterFactory funcFactory = getFormattingFunctionFactoryByName(toFormat, ph.getFunctionName());
+ if (funcFactory != null) {
+ Map fixedOptions = mf2OptToFixedOptions(ph.getOptions());
+ Formatter ff = funcFactory.createFormatter(locale, fixedOptions);
+ ph.formatter = ff;
+ }
+ }
+ if (ph.formatter != null) {
+ Map variableOptions = mf2OptToVariableOptions(ph.getOptions(), arguments);
+ try {
+ return ph.formatter.format(toFormat, variableOptions);
+ } catch (IllegalArgumentException e) {
+ // Fall-through to the name of the placeholder without replacement.
+ }
+ }
+
+ return new FormattedPlaceholder(toFormat, new PlainStringFormattedValue("{" + ph.getOperand() + "}"));
+ }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/message2/Mf2FunctionRegistry.java b/icu4j/main/classes/core/src/com/ibm/icu/message2/Mf2FunctionRegistry.java
new file mode 100644
index 000000000000..3560e42891dd
--- /dev/null
+++ b/icu4j/main/classes/core/src/com/ibm/icu/message2/Mf2FunctionRegistry.java
@@ -0,0 +1,347 @@
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package com.ibm.icu.message2;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * This class is used to register mappings between various function
+ * names and the factories that can create those functions.
+ *
+ * For example to add formatting for a {@code Person} object one would need to:
+ *
+ * - write a function (class, lambda, etc.) that does the formatting proper
+ * (implementing {@link Formatter})
+ * - write a factory that creates such a function
+ * (implementing {@link FormatterFactory})
+ * - add a mapping from the function name as used in the syntax
+ * (for example {@code "person"}) to the factory
+ * - optionally add a mapping from the class to format ({@code ...Person.class}) to
+ * the formatter name ({@code "person"}), so that one can use a placeholder in the message
+ * without specifying a function (for example {@code "... {$me} ..."} instead of
+ * {@code "... {$me :person} ..."}, if the class of {@code $me} is an {@code instanceof Person}).
+ *
+ *
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+@Deprecated
+public class Mf2FunctionRegistry {
+ private final Map formattersMap;
+ private final Map selectorsMap;
+ private final Map, String> classToFormatter;
+
+ private Mf2FunctionRegistry(Builder builder) {
+ this.formattersMap = new HashMap<>(builder.formattersMap);
+ this.selectorsMap = new HashMap<>(builder.selectorsMap);
+ this.classToFormatter = new HashMap<>(builder.classToFormatter);
+ }
+
+ /**
+ * Creates a builder.
+ *
+ * @return the Builder.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ /**
+ * Returns the formatter factory used to create the formatter for function
+ * named {@code name}.
+ *
+ * Note: function name here means the name used to refer to the function in the
+ * MessageFormat 2 syntax, for example {@code "... {$exp :datetime} ..."}
+ * The function name here is {@code "datetime"}, and does not have to correspond to the
+ * name of the methods / classes used to implement the functionality.
+ *
+ * For example one might write a {@code PersonFormatterFactory} returning a {@code PersonFormatter},
+ * and map that to the MessageFormat function named {@code "person"}.
+ * The only name visible to the users of MessageFormat syntax will be {@code "person"}.
+ *
+ * @param formatterName the function name.
+ * @return the factory creating formatters for {@code name}. Returns {@code null} if none is registered.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public FormatterFactory getFormatter(String formatterName) {
+ return formattersMap.get(formatterName);
+ }
+
+ /**
+ * Get all know names that have a mappings from name to {@link FormatterFactory}.
+ *
+ * @return a set of all the known formatter names.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Set getFormatterNames() {
+ return formattersMap.keySet();
+ }
+
+ /**
+ * Returns the name of the formatter used to format an object of type {@code clazz}.
+ *
+ * @param clazz the class of the object to format.
+ * @return the name of the formatter class, if registered. Returns {@code null} otherwise.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public String getDefaultFormatterNameForType(Class> clazz) {
+ // Search for the class "as is", to save time.
+ // If we don't find it then we iterate the registered classes and check
+ // if the class is an instanceof the ones registered.
+ // For example a BuddhistCalendar when we only registered Calendar
+ String result = classToFormatter.get(clazz);
+ if (result != null) {
+ return result;
+ }
+ // We didn't find the class registered explicitly "as is"
+ for (Map.Entry, String> e : classToFormatter.entrySet()) {
+ if (e.getKey().isAssignableFrom(clazz)) {
+ return e.getValue();
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Get all know classes that have a mappings from class to function name.
+ *
+ * @return a set of all the known classes that have mapping to function names.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Set> getDefaultFormatterTypes() {
+ return classToFormatter.keySet();
+ }
+
+ /**
+ * Returns the selector factory used to create the selector for function
+ * named {@code name}.
+ *
+ * Note: the same comments about naming as the ones on {@code getFormatter} apply.
+ *
+ * @param selectorName the selector name.
+ * @return the factory creating selectors for {@code name}. Returns {@code null} if none is registered.
+ * @see #getFormatter(String)
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public SelectorFactory getSelector(String selectorName) {
+ return selectorsMap.get(selectorName);
+ }
+
+ /**
+ * Get all know names that have a mappings from name to {@link SelectorFactory}.
+ *
+ * @return a set of all the known selector names.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Set getSelectorNames() {
+ return selectorsMap.keySet();
+ }
+
+ /**
+ * A {@code Builder} used to build instances of {@link Mf2FunctionRegistry}.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public static class Builder {
+ private final Map formattersMap = new HashMap<>();
+ private final Map selectorsMap = new HashMap<>();
+ private final Map, String> classToFormatter = new HashMap<>();
+
+ // Prevent direct creation
+ private Builder() {
+ }
+
+ /**
+ * Adds all the mapping from another registry to this one.
+ *
+ * @param functionRegistry the registry to copy from.
+ * @return the builder, for fluent use.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Builder addAll(Mf2FunctionRegistry functionRegistry) {
+ formattersMap.putAll(functionRegistry.formattersMap);
+ selectorsMap.putAll(functionRegistry.selectorsMap);
+ classToFormatter.putAll(functionRegistry.classToFormatter);
+ return this;
+ }
+
+ /**
+ * Adds a mapping from a formatter name to a {@link FormatterFactory}
+ *
+ * @param formatterName the function name (as used in the MessageFormat 2 syntax).
+ * @param formatterFactory the factory that handles the name.
+ * @return the builder, for fluent use.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Builder setFormatter(String formatterName, FormatterFactory formatterFactory) {
+ formattersMap.put(formatterName, formatterFactory);
+ return this;
+ }
+
+ /**
+ * Remove the formatter associated with the name.
+ *
+ * @param formatterName the name of the formatter to remove.
+ * @return the builder, for fluent use.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Builder removeFormatter(String formatterName) {
+ formattersMap.remove(formatterName);
+ return this;
+ }
+
+ /**
+ * Remove all the formatter mappings.
+ *
+ * @return the builder, for fluent use.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Builder clearFormatters() {
+ formattersMap.clear();
+ return this;
+ }
+
+ /**
+ * Adds a mapping from a type to format to a {@link FormatterFactory} formatter name.
+ *
+ * @param clazz the class of the type to format.
+ * @param formatterName the formatter name (as used in the MessageFormat 2 syntax).
+ * @return the builder, for fluent use.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Builder setDefaultFormatterNameForType(Class> clazz, String formatterName) {
+ classToFormatter.put(clazz, formatterName);
+ return this;
+ }
+
+ /**
+ * Remove the function name associated with the class.
+ *
+ * @param clazz the class to remove the mapping for.
+ * @return the builder, for fluent use.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Builder removeDefaultFormatterNameForType(Class> clazz) {
+ classToFormatter.remove(clazz);
+ return this;
+ }
+
+ /**
+ * Remove all the class to formatter-names mappings.
+ *
+ * @return the builder, for fluent use.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Builder clearDefaultFormatterNames() {
+ classToFormatter.clear();
+ return this;
+ }
+
+ /**
+ * Adds a mapping from a selector name to a {@link SelectorFactory}
+ *
+ * @param selectorName the function name (as used in the MessageFormat 2 syntax).
+ * @param selectorFactory the factory that handles the name.
+ * @return the builder, for fluent use.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Builder setSelector(String selectorName, SelectorFactory selectorFactory) {
+ selectorsMap.put(selectorName, selectorFactory);
+ return this;
+ }
+
+ /**
+ * Remove the selector associated with the name.
+ *
+ * @param selectorName the name of the selector to remove.
+ * @return the builder, for fluent use.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Builder removeSelector(String selectorName) {
+ selectorsMap.remove(selectorName);
+ return this;
+ }
+
+ /**
+ * Remove all the selector mappings.
+ *
+ * @return the builder, for fluent use.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Builder clearSelectors() {
+ selectorsMap.clear();
+ return this;
+ }
+
+ /**
+ * Builds an instance of {@link Mf2FunctionRegistry}.
+ *
+ * @return the function registry created.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public Mf2FunctionRegistry build() {
+ return new Mf2FunctionRegistry(this);
+ }
+ }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/message2/Mf2Parser.java b/icu4j/main/classes/core/src/com/ibm/icu/message2/Mf2Parser.java
new file mode 100644
index 000000000000..48b1062d0cd0
--- /dev/null
+++ b/icu4j/main/classes/core/src/com/ibm/icu/message2/Mf2Parser.java
@@ -0,0 +1,754 @@
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package com.ibm.icu.message2;
+
+import java.util.Arrays;
+
+/**
+ * Class generated from EBNF.
+ */
+@SuppressWarnings("all") // Disable all warnings in the generated file
+class Mf2Parser
+{
+ static class ParseException extends RuntimeException
+ {
+ private static final long serialVersionUID = 1L;
+ private int begin, end, offending, expected, state;
+
+ public ParseException(int b, int e, int s, int o, int x)
+ {
+ begin = b;
+ end = e;
+ state = s;
+ offending = o;
+ expected = x;
+ }
+
+ @Override
+ public String getMessage()
+ {
+ return offending < 0
+ ? "lexical analysis failed"
+ : "syntax error";
+ }
+
+ public void serialize(EventHandler eventHandler)
+ {
+ }
+
+ public int getBegin() {return begin;}
+ public int getEnd() {return end;}
+ public int getState() {return state;}
+ public int getOffending() {return offending;}
+ public int getExpected() {return expected;}
+ public boolean isAmbiguousInput() {return false;}
+ }
+
+ public interface EventHandler
+ {
+ public void reset(CharSequence string);
+ public void startNonterminal(String name, int begin);
+ public void endNonterminal(String name, int end);
+ public void terminal(String name, int begin, int end);
+ public void whitespace(int begin, int end);
+ }
+
+ public static class TopDownTreeBuilder implements EventHandler
+ {
+ private CharSequence input = null;
+ public Nonterminal[] stack = new Nonterminal[64];
+ private int top = -1;
+
+ @Override
+ public void reset(CharSequence input)
+ {
+ this.input = input;
+ top = -1;
+ }
+
+ @Override
+ public void startNonterminal(String name, int begin)
+ {
+ Nonterminal nonterminal = new Nonterminal(name, begin, begin, new Symbol[0]);
+ if (top >= 0) addChild(nonterminal);
+ if (++top >= stack.length) stack = Arrays.copyOf(stack, stack.length << 1);
+ stack[top] = nonterminal;
+ }
+
+ @Override
+ public void endNonterminal(String name, int end)
+ {
+ stack[top].end = end;
+ if (top > 0) --top;
+ }
+
+ @Override
+ public void terminal(String name, int begin, int end)
+ {
+ addChild(new Terminal(name, begin, end));
+ }
+
+ @Override
+ public void whitespace(int begin, int end)
+ {
+ }
+
+ private void addChild(Symbol s)
+ {
+ Nonterminal current = stack[top];
+ current.children = Arrays.copyOf(current.children, current.children.length + 1);
+ current.children[current.children.length - 1] = s;
+ }
+
+ public void serialize(EventHandler e)
+ {
+ e.reset(input);
+ stack[0].send(e);
+ }
+ }
+
+ public static abstract class Symbol
+ {
+ public String name;
+ public int begin;
+ public int end;
+
+ protected Symbol(String name, int begin, int end)
+ {
+ this.name = name;
+ this.begin = begin;
+ this.end = end;
+ }
+
+ public abstract void send(EventHandler e);
+ }
+
+ public static class Terminal extends Symbol
+ {
+ public Terminal(String name, int begin, int end)
+ {
+ super(name, begin, end);
+ }
+
+ @Override
+ public void send(EventHandler e)
+ {
+ e.terminal(name, begin, end);
+ }
+ }
+
+ public static class Nonterminal extends Symbol
+ {
+ public Symbol[] children;
+
+ public Nonterminal(String name, int begin, int end, Symbol[] children)
+ {
+ super(name, begin, end);
+ this.children = children;
+ }
+
+ @Override
+ public void send(EventHandler e)
+ {
+ e.startNonterminal(name, begin);
+ int pos = begin;
+ for (Symbol c : children)
+ {
+ if (pos < c.begin) e.whitespace(pos, c.begin);
+ c.send(e);
+ pos = c.end;
+ }
+ if (pos < end) e.whitespace(pos, end);
+ e.endNonterminal(name, end);
+ }
+ }
+
+ public Mf2Parser(CharSequence string, EventHandler t)
+ {
+ initialize(string, t);
+ }
+
+ public void initialize(CharSequence source, EventHandler parsingEventHandler)
+ {
+ eventHandler = parsingEventHandler;
+ input = source;
+ size = source.length();
+ reset(0, 0, 0);
+ }
+
+ public CharSequence getInput()
+ {
+ return input;
+ }
+
+ public int getTokenOffset()
+ {
+ return b0;
+ }
+
+ public int getTokenEnd()
+ {
+ return e0;
+ }
+
+ public final void reset(int l, int b, int e)
+ {
+ b0 = b; e0 = b;
+ l1 = l; b1 = b; e1 = e;
+ end = e;
+ eventHandler.reset(input);
+ }
+
+ public void reset()
+ {
+ reset(0, 0, 0);
+ }
+
+ public static String getOffendingToken(ParseException e)
+ {
+ return e.getOffending() < 0 ? null : TOKEN[e.getOffending()];
+ }
+
+ public static String[] getExpectedTokenSet(ParseException e)
+ {
+ String[] expected;
+ if (e.getExpected() >= 0)
+ {
+ expected = new String[]{TOKEN[e.getExpected()]};
+ }
+ else
+ {
+ expected = getTokenSet(- e.getState());
+ }
+ return expected;
+ }
+
+ public String getErrorMessage(ParseException e)
+ {
+ String message = e.getMessage();
+ String[] tokenSet = getExpectedTokenSet(e);
+ String found = getOffendingToken(e);
+ int size = e.getEnd() - e.getBegin();
+ message += (found == null ? "" : ", found " + found)
+ + "\nwhile expecting "
+ + (tokenSet.length == 1 ? tokenSet[0] : java.util.Arrays.toString(tokenSet))
+ + "\n"
+ + (size == 0 || found != null ? "" : "after successfully scanning " + size + " characters beginning ");
+ String prefix = input.subSequence(0, e.getBegin()).toString();
+ int line = prefix.replaceAll("[^\n]", "").length() + 1;
+ int column = prefix.length() - prefix.lastIndexOf('\n');
+ return message
+ + "at line " + line + ", column " + column + ":\n..."
+ + input.subSequence(e.getBegin(), Math.min(input.length(), e.getBegin() + 64))
+ + "...";
+ }
+
+ public void parse_Message()
+ {
+ eventHandler.startNonterminal("Message", e0);
+ for (;;)
+ {
+ lookahead1W(12); // WhiteSpace | 'let' | 'match' | '{'
+ if (l1 != 13) // 'let'
+ {
+ break;
+ }
+ whitespace();
+ parse_Declaration();
+ }
+ switch (l1)
+ {
+ case 16: // '{'
+ whitespace();
+ parse_Pattern();
+ break;
+ default:
+ whitespace();
+ parse_Selector();
+ for (;;)
+ {
+ whitespace();
+ parse_Variant();
+ lookahead1W(4); // END | WhiteSpace | 'when'
+ if (l1 != 15) // 'when'
+ {
+ break;
+ }
+ }
+ }
+ eventHandler.endNonterminal("Message", e0);
+ }
+
+ private void parse_Declaration()
+ {
+ eventHandler.startNonterminal("Declaration", e0);
+ consume(13); // 'let'
+ lookahead1W(0); // WhiteSpace | Variable
+ consume(4); // Variable
+ lookahead1W(1); // WhiteSpace | '='
+ consume(12); // '='
+ lookahead1W(2); // WhiteSpace | '{'
+ consume(16); // '{'
+ lookahead1W(9); // WhiteSpace | Variable | Function | Literal
+ whitespace();
+ parse_Expression();
+ consume(17); // '}'
+ eventHandler.endNonterminal("Declaration", e0);
+ }
+
+ private void parse_Selector()
+ {
+ eventHandler.startNonterminal("Selector", e0);
+ consume(14); // 'match'
+ for (;;)
+ {
+ lookahead1W(2); // WhiteSpace | '{'
+ consume(16); // '{'
+ lookahead1W(9); // WhiteSpace | Variable | Function | Literal
+ whitespace();
+ parse_Expression();
+ consume(17); // '}'
+ lookahead1W(7); // WhiteSpace | 'when' | '{'
+ if (l1 != 16) // '{'
+ {
+ break;
+ }
+ }
+ eventHandler.endNonterminal("Selector", e0);
+ }
+
+ private void parse_Variant()
+ {
+ eventHandler.startNonterminal("Variant", e0);
+ consume(15); // 'when'
+ for (;;)
+ {
+ lookahead1W(11); // WhiteSpace | Nmtoken | Literal | '*'
+ whitespace();
+ parse_VariantKey();
+ lookahead1W(13); // WhiteSpace | Nmtoken | Literal | '*' | '{'
+ if (l1 == 16) // '{'
+ {
+ break;
+ }
+ }
+ whitespace();
+ parse_Pattern();
+ eventHandler.endNonterminal("Variant", e0);
+ }
+
+ private void parse_VariantKey()
+ {
+ eventHandler.startNonterminal("VariantKey", e0);
+ switch (l1)
+ {
+ case 10: // Literal
+ consume(10); // Literal
+ break;
+ case 9: // Nmtoken
+ consume(9); // Nmtoken
+ break;
+ default:
+ consume(11); // '*'
+ }
+ eventHandler.endNonterminal("VariantKey", e0);
+ }
+
+ private void parse_Pattern()
+ {
+ eventHandler.startNonterminal("Pattern", e0);
+ consume(16); // '{'
+ for (;;)
+ {
+ lookahead1(8); // Text | '{' | '}'
+ if (l1 == 17) // '}'
+ {
+ break;
+ }
+ switch (l1)
+ {
+ case 3: // Text
+ consume(3); // Text
+ break;
+ default:
+ parse_Placeholder();
+ }
+ }
+ consume(17); // '}'
+ eventHandler.endNonterminal("Pattern", e0);
+ }
+
+ private void parse_Placeholder()
+ {
+ eventHandler.startNonterminal("Placeholder", e0);
+ consume(16); // '{'
+ lookahead1W(14); // WhiteSpace | Variable | Function | MarkupStart | MarkupEnd | Literal | '}'
+ if (l1 != 17) // '}'
+ {
+ switch (l1)
+ {
+ case 6: // MarkupStart
+ whitespace();
+ parse_Markup();
+ break;
+ case 7: // MarkupEnd
+ consume(7); // MarkupEnd
+ break;
+ default:
+ whitespace();
+ parse_Expression();
+ }
+ }
+ lookahead1W(3); // WhiteSpace | '}'
+ consume(17); // '}'
+ eventHandler.endNonterminal("Placeholder", e0);
+ }
+
+ private void parse_Expression()
+ {
+ eventHandler.startNonterminal("Expression", e0);
+ switch (l1)
+ {
+ case 5: // Function
+ parse_Annotation();
+ break;
+ default:
+ parse_Operand();
+ lookahead1W(5); // WhiteSpace | Function | '}'
+ if (l1 == 5) // Function
+ {
+ whitespace();
+ parse_Annotation();
+ }
+ }
+ eventHandler.endNonterminal("Expression", e0);
+ }
+
+ private void parse_Operand()
+ {
+ eventHandler.startNonterminal("Operand", e0);
+ switch (l1)
+ {
+ case 10: // Literal
+ consume(10); // Literal
+ break;
+ default:
+ consume(4); // Variable
+ }
+ eventHandler.endNonterminal("Operand", e0);
+ }
+
+ private void parse_Annotation()
+ {
+ eventHandler.startNonterminal("Annotation", e0);
+ consume(5); // Function
+ for (;;)
+ {
+ lookahead1W(6); // WhiteSpace | Name | '}'
+ if (l1 != 8) // Name
+ {
+ break;
+ }
+ whitespace();
+ parse_Option();
+ }
+ eventHandler.endNonterminal("Annotation", e0);
+ }
+
+ private void parse_Option()
+ {
+ eventHandler.startNonterminal("Option", e0);
+ consume(8); // Name
+ lookahead1W(1); // WhiteSpace | '='
+ consume(12); // '='
+ lookahead1W(10); // WhiteSpace | Variable | Nmtoken | Literal
+ switch (l1)
+ {
+ case 10: // Literal
+ consume(10); // Literal
+ break;
+ case 9: // Nmtoken
+ consume(9); // Nmtoken
+ break;
+ default:
+ consume(4); // Variable
+ }
+ eventHandler.endNonterminal("Option", e0);
+ }
+
+ private void parse_Markup()
+ {
+ eventHandler.startNonterminal("Markup", e0);
+ consume(6); // MarkupStart
+ for (;;)
+ {
+ lookahead1W(6); // WhiteSpace | Name | '}'
+ if (l1 != 8) // Name
+ {
+ break;
+ }
+ whitespace();
+ parse_Option();
+ }
+ eventHandler.endNonterminal("Markup", e0);
+ }
+
+ private void consume(int t)
+ {
+ if (l1 == t)
+ {
+ whitespace();
+ eventHandler.terminal(TOKEN[l1], b1, e1);
+ b0 = b1; e0 = e1; l1 = 0;
+ }
+ else
+ {
+ error(b1, e1, 0, l1, t);
+ }
+ }
+
+ private void whitespace()
+ {
+ if (e0 != b1)
+ {
+ eventHandler.whitespace(e0, b1);
+ e0 = b1;
+ }
+ }
+
+ private int matchW(int tokenSetId)
+ {
+ int code;
+ for (;;)
+ {
+ code = match(tokenSetId);
+ if (code != 2) // WhiteSpace
+ {
+ break;
+ }
+ }
+ return code;
+ }
+
+ private void lookahead1W(int tokenSetId)
+ {
+ if (l1 == 0)
+ {
+ l1 = matchW(tokenSetId);
+ b1 = begin;
+ e1 = end;
+ }
+ }
+
+ private void lookahead1(int tokenSetId)
+ {
+ if (l1 == 0)
+ {
+ l1 = match(tokenSetId);
+ b1 = begin;
+ e1 = end;
+ }
+ }
+
+ private int error(int b, int e, int s, int l, int t)
+ {
+ throw new ParseException(b, e, s, l, t);
+ }
+
+ private int b0, e0;
+ private int l1, b1, e1;
+ private EventHandler eventHandler = null;
+ private CharSequence input = null;
+ private int size = 0;
+ private int begin = 0;
+ private int end = 0;
+
+ private int match(int tokenSetId)
+ {
+ begin = end;
+ int current = end;
+ int result = INITIAL[tokenSetId];
+ int state = 0;
+
+ for (int code = result & 63; code != 0; )
+ {
+ int charclass;
+ int c0 = current < size ? input.charAt(current) : 0;
+ ++current;
+ if (c0 < 0x80)
+ {
+ charclass = MAP0[c0];
+ }
+ else if (c0 < 0xd800)
+ {
+ int c1 = c0 >> 4;
+ charclass = MAP1[(c0 & 15) + MAP1[(c1 & 31) + MAP1[c1 >> 5]]];
+ }
+ else
+ {
+ if (c0 < 0xdc00)
+ {
+ int c1 = current < size ? input.charAt(current) : 0;
+ if (c1 >= 0xdc00 && c1 < 0xe000)
+ {
+ ++current;
+ c0 = ((c0 & 0x3ff) << 10) + (c1 & 0x3ff) + 0x10000;
+ }
+ }
+
+ int lo = 0, hi = 6;
+ for (int m = 3; ; m = (hi + lo) >> 1)
+ {
+ if (MAP2[m] > c0) {hi = m - 1;}
+ else if (MAP2[7 + m] < c0) {lo = m + 1;}
+ else {charclass = MAP2[14 + m]; break;}
+ if (lo > hi) {charclass = 0; break;}
+ }
+ }
+
+ state = code;
+ int i0 = (charclass << 6) + code - 1;
+ code = TRANSITION[(i0 & 7) + TRANSITION[i0 >> 3]];
+
+ if (code > 63)
+ {
+ result = code;
+ code &= 63;
+ end = current;
+ }
+ }
+
+ result >>= 6;
+ if (result == 0)
+ {
+ end = current - 1;
+ int c1 = end < size ? input.charAt(end) : 0;
+ if (c1 >= 0xdc00 && c1 < 0xe000)
+ {
+ --end;
+ }
+ return error(begin, end, state, -1, -1);
+ }
+
+ if (end > size) end = size;
+ return (result & 31) - 1;
+ }
+
+ private static String[] getTokenSet(int tokenSetId)
+ {
+ java.util.ArrayList expected = new java.util.ArrayList<>();
+ int s = tokenSetId < 0 ? - tokenSetId : INITIAL[tokenSetId] & 63;
+ for (int i = 0; i < 18; i += 32)
+ {
+ int j = i;
+ int i0 = (i >> 5) * 38 + s - 1;
+ int f = EXPECTED[i0];
+ for ( ; f != 0; f >>>= 1, ++j)
+ {
+ if ((f & 1) != 0)
+ {
+ expected.add(TOKEN[j]);
+ }
+ }
+ }
+ return expected.toArray(new String[]{});
+ }
+
+ private static final int[] MAP0 =
+ {
+ /* 0 */ 24, 24, 24, 24, 24, 24, 24, 24, 24, 1, 1, 24, 24, 1, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24,
+ /* 27 */ 24, 24, 24, 24, 24, 1, 24, 24, 24, 2, 24, 24, 24, 3, 4, 5, 6, 24, 7, 8, 24, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
+ /* 58 */ 9, 24, 24, 10, 24, 24, 24, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11,
+ /* 85 */ 11, 11, 11, 11, 11, 11, 24, 12, 24, 24, 11, 24, 13, 11, 14, 11, 15, 11, 11, 16, 11, 11, 11, 17, 18, 19,
+ /* 111 */ 11, 11, 11, 11, 11, 20, 11, 11, 21, 11, 11, 11, 22, 24, 23, 24, 24
+ };
+
+ private static final int[] MAP1 =
+ {
+ /* 0 */ 108, 124, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 156, 181, 181, 181, 181,
+ /* 21 */ 181, 214, 215, 213, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214,
+ /* 42 */ 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214,
+ /* 63 */ 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214,
+ /* 84 */ 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214, 214,
+ /* 105 */ 214, 214, 214, 383, 330, 396, 353, 291, 262, 247, 308, 330, 330, 330, 322, 292, 284, 292, 284, 292, 292,
+ /* 126 */ 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 347, 347, 347, 347, 347, 347, 347,
+ /* 147 */ 277, 292, 292, 292, 292, 292, 292, 292, 292, 369, 330, 330, 331, 329, 330, 330, 292, 292, 292, 292, 292,
+ /* 168 */ 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 330, 330, 330, 330, 330, 330, 330, 330,
+ /* 189 */ 330, 330, 330, 330, 330, 330, 330, 330, 330, 330, 330, 330, 330, 330, 330, 330, 330, 330, 330, 330, 330,
+ /* 210 */ 330, 330, 330, 291, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292,
+ /* 231 */ 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 292, 330, 24, 13, 11, 14, 11, 15,
+ /* 253 */ 11, 11, 16, 11, 11, 11, 17, 18, 19, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 24, 12, 24, 24, 11, 11,
+ /* 279 */ 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 24, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11, 11,
+ /* 305 */ 11, 11, 11, 11, 11, 11, 11, 20, 11, 11, 21, 11, 11, 11, 22, 24, 23, 24, 24, 24, 24, 24, 24, 24, 8, 24, 24,
+ /* 332 */ 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8,
+ /* 363 */ 9, 24, 24, 10, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 24, 11, 11, 24, 24, 24, 24, 24, 24, 24,
+ /* 390 */ 24, 24, 1, 1, 24, 24, 1, 24, 24, 24, 2, 24, 24, 24, 3, 4, 5, 6, 24, 7, 8, 24
+ };
+
+ private static final int[] MAP2 =
+ {
+ /* 0 */ 55296, 63744, 64976, 65008, 65534, 65536, 983040, 63743, 64975, 65007, 65533, 65535, 983039, 1114111, 24,
+ /* 15 */ 11, 24, 11, 24, 11, 24
+ };
+
+ private static final int[] INITIAL =
+ {
+ /* 0 */ 1, 2, 3, 4, 133, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15
+ };
+
+ private static final int[] TRANSITION =
+ {
+ /* 0 */ 237, 237, 237, 237, 237, 237, 237, 237, 200, 208, 455, 237, 237, 237, 237, 237, 236, 230, 455, 237, 237,
+ /* 21 */ 237, 237, 237, 237, 245, 376, 382, 237, 237, 237, 237, 237, 380, 314, 382, 237, 237, 237, 237, 237, 263,
+ /* 42 */ 455, 237, 237, 237, 237, 237, 237, 295, 455, 237, 237, 237, 237, 237, 237, 322, 287, 281, 252, 237, 237,
+ /* 63 */ 237, 237, 344, 287, 281, 252, 237, 237, 237, 255, 358, 455, 237, 237, 237, 237, 237, 417, 380, 455, 237,
+ /* 84 */ 237, 237, 237, 237, 419, 390, 215, 329, 252, 237, 237, 237, 237, 398, 275, 382, 237, 237, 237, 237, 419,
+ /* 105 */ 390, 215, 410, 252, 237, 237, 237, 419, 390, 215, 329, 309, 237, 237, 237, 419, 390, 222, 365, 252, 237,
+ /* 126 */ 237, 237, 419, 390, 427, 329, 302, 237, 237, 237, 419, 435, 215, 329, 252, 237, 237, 237, 419, 443, 215,
+ /* 147 */ 329, 252, 237, 237, 237, 419, 390, 215, 329, 372, 237, 237, 237, 419, 390, 215, 336, 451, 237, 237, 237,
+ /* 168 */ 402, 390, 215, 329, 252, 237, 237, 237, 350, 463, 269, 237, 237, 237, 237, 237, 474, 471, 269, 237, 237,
+ /* 189 */ 237, 237, 237, 237, 380, 455, 237, 237, 237, 237, 237, 192, 192, 192, 192, 192, 192, 192, 192, 277, 192,
+ /* 210 */ 192, 192, 192, 192, 192, 0, 414, 595, 0, 277, 22, 663, 0, 414, 595, 0, 277, 22, 663, 32, 277, 16, 16, 0,
+ /* 234 */ 0, 0, 16, 0, 0, 0, 0, 0, 0, 0, 0, 277, 22, 22, 22, 0, 22, 22, 0, 482, 547, 0, 0, 0, 0, 0, 18, 0, 0, 277,
+ /* 264 */ 0, 0, 768, 0, 768, 0, 0, 0, 277, 0, 22, 0, 0, 0, 277, 20, 31, 0, 0, 0, 348, 0, 414, 0, 0, 595, 0, 277, 22,
+ /* 293 */ 663, 0, 277, 0, 0, 0, 0, 0, 26, 0, 482, 547, 0, 0, 960, 0, 0, 482, 547, 0, 38, 0, 0, 0, 0, 277, 704, 0, 0,
+ /* 322 */ 277, 0, 663, 663, 0, 663, 27, 0, 482, 547, 348, 0, 414, 0, 0, 482, 547, 348, 0, 414, 0, 896, 277, 0, 663,
+ /* 347 */ 663, 0, 663, 0, 0, 1088, 0, 0, 0, 0, 1088, 277, 18, 0, 0, 0, 0, 18, 0, 482, 547, 348, 36, 414, 0, 0, 482,
+ /* 374 */ 547, 1024, 0, 0, 0, 0, 277, 0, 0, 0, 0, 0, 0, 0, 22, 0, 277, 0, 663, 663, 0, 663, 0, 348, 20, 0, 0, 0, 0,
+ /* 403 */ 0, 0, 0, 17, 0, 595, 17, 33, 482, 547, 348, 0, 414, 0, 0, 832, 0, 0, 0, 0, 0, 0, 595, 0, 29, 414, 595, 0,
+ /* 431 */ 277, 22, 663, 0, 277, 0, 663, 663, 24, 663, 0, 348, 277, 0, 663, 663, 25, 663, 0, 348, 37, 482, 547, 0, 0,
+ /* 456 */ 0, 0, 0, 277, 22, 0, 0, 1088, 0, 0, 0, 1088, 1088, 0, 0, 1152, 0, 0, 0, 0, 0, 1152, 0, 1152, 1152, 0
+ };
+
+ private static final int[] EXPECTED =
+ {
+ /* 0 */ 20, 4100, 65540, 131076, 32772, 131108, 131332, 98308, 196616, 1076, 1556, 3588, 90116, 69124, 132340, 16,
+ /* 16 */ 32768, 32, 256, 8, 8, 1024, 512, 8192, 16384, 64, 128, 16, 32768, 32, 1024, 8192, 16384, 64, 128, 32768,
+ /* 36 */ 16384, 16384
+ };
+
+ private static final String[] TOKEN =
+ {
+ "(0)",
+ "END",
+ "WhiteSpace",
+ "Text",
+ "Variable",
+ "Function",
+ "MarkupStart",
+ "MarkupEnd",
+ "Name",
+ "Nmtoken",
+ "Literal",
+ "'*'",
+ "'='",
+ "'let'",
+ "'match'",
+ "'when'",
+ "'{'",
+ "'}'"
+ };
+}
+
+// End
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/message2/Mf2Serializer.java b/icu4j/main/classes/core/src/com/ibm/icu/message2/Mf2Serializer.java
new file mode 100644
index 000000000000..b7bff50e06dd
--- /dev/null
+++ b/icu4j/main/classes/core/src/com/ibm/icu/message2/Mf2Serializer.java
@@ -0,0 +1,522 @@
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package com.ibm.icu.message2;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import com.ibm.icu.message2.Mf2DataModel.Expression;
+import com.ibm.icu.message2.Mf2DataModel.Pattern;
+import com.ibm.icu.message2.Mf2DataModel.SelectorKeys;
+import com.ibm.icu.message2.Mf2DataModel.Text;
+import com.ibm.icu.message2.Mf2DataModel.Value;
+import com.ibm.icu.message2.Mf2Parser.EventHandler;
+import com.ibm.icu.message2.Mf2Serializer.Token.Type;
+
+// TODO: find a better name for this class
+class Mf2Serializer implements EventHandler {
+ private String input;
+ private final List tokens = new ArrayList<>();
+
+ static class Token {
+ final String name;
+ final int begin;
+ final int end;
+ final Kind kind;
+ private final Type type;
+ private final String input;
+
+ enum Kind {
+ TERMINAL,
+ NONTERMINAL_START,
+ NONTERMINAL_END
+ }
+
+ enum Type {
+ MESSAGE,
+ PATTERN,
+ TEXT,
+ PLACEHOLDER,
+ EXPRESSION,
+ OPERAND,
+ VARIABLE,
+ IGNORE,
+ FUNCTION,
+ OPTION,
+ NAME,
+ NMTOKEN,
+ LITERAL,
+ SELECTOR,
+ VARIANT,
+ DECLARATION, VARIANTKEY, DEFAULT,
+ }
+
+ Token(Kind kind, String name, int begin, int end, String input) {
+ this.kind = kind;
+ this.name = name;
+ this.begin = begin;
+ this.end = end;
+ this.input = input;
+ switch (name) {
+ case "Message": type = Type.MESSAGE; break;
+ case "Pattern": type = Type.PATTERN; break;
+ case "Text": type = Type.TEXT; break;
+ case "Placeholder": type = Type.PLACEHOLDER; break;
+ case "Expression": type = Type.EXPRESSION; break;
+ case "Operand": type = Type.OPERAND; break;
+ case "Variable": type = Type.VARIABLE; break;
+ case "Function": type = Type.FUNCTION; break;
+ case "Option": type = Type.OPTION; break;
+ case "Annotation": type = Type.IGNORE; break;
+ case "Name": type = Type.NAME; break;
+ case "Nmtoken": type = Type.NMTOKEN; break;
+ case "Literal": type = Type.LITERAL; break;
+ case "Selector": type = Type.SELECTOR; break;
+ case "Variant": type = Type.VARIANT; break;
+ case "VariantKey": type = Type.VARIANTKEY; break;
+ case "Declaration": type = Type.DECLARATION; break;
+
+ case "Markup": type = Type.IGNORE; break;
+ case "MarkupStart": type = Type.IGNORE; break;
+ case "MarkupEnd": type = Type.IGNORE; break;
+
+ case "'['": type = Type.IGNORE; break;
+ case "']'": type = Type.IGNORE; break;
+ case "'{'": type = Type.IGNORE; break;
+ case "'}'": type = Type.IGNORE; break;
+ case "'='": type = Type.IGNORE; break;
+ case "'match'": type = Type.IGNORE; break;
+ case "'when'": type = Type.IGNORE; break;
+ case "'let'": type = Type.IGNORE; break;
+ case "'*'": type = Type.DEFAULT; break;
+ default:
+ throw new IllegalArgumentException("Parse error: Unknown token \"" + name + "\"");
+ }
+ }
+
+ boolean isStart() {
+ return Kind.NONTERMINAL_START.equals(kind);
+ }
+
+ boolean isEnd() {
+ return Kind.NONTERMINAL_END.equals(kind);
+ }
+
+ boolean isTerminal() {
+ return Kind.TERMINAL.equals(kind);
+ }
+
+ @Override
+ public String toString() {
+ int from = begin == -1 ? 0 : begin;
+ String strval = end == -1 ? input.substring(from) : input.substring(from, end);
+ return String.format("Token(\"%s\", [%d, %d], %s) // \"%s\"", name, begin, end, kind, strval);
+ }
+ }
+
+ Mf2Serializer() {}
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ @Override
+ public void reset(CharSequence input) {
+ this.input = input.toString();
+ tokens.clear();
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ @Override
+ public void startNonterminal(String name, int begin) {
+ tokens.add(new Token(Token.Kind.NONTERMINAL_START, name, begin, -1, input));
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ @Override
+ public void endNonterminal(String name, int end) {
+ tokens.add(new Token(Token.Kind.NONTERMINAL_END, name, -1, end, input));
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ @Override
+ public void terminal(String name, int begin, int end) {
+ tokens.add(new Token(Token.Kind.TERMINAL, name, begin, end, input));
+ }
+
+ /**
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ @Override
+ public void whitespace(int begin, int end) {
+ }
+
+ Mf2DataModel build() {
+ if (!tokens.isEmpty()) {
+ Token firstToken = tokens.get(0);
+ if (Type.MESSAGE.equals(firstToken.type) && firstToken.isStart()) {
+ return parseMessage();
+ }
+ }
+ return null;
+ }
+
+ private Mf2DataModel parseMessage() {
+ Mf2DataModel.Builder result = Mf2DataModel.builder();
+
+ for (int i = 0; i < tokens.size(); i++) {
+ Token token = tokens.get(i);
+ switch (token.type) {
+ case MESSAGE:
+ if (token.isStart() && i == 0) {
+ // all good
+ } else if (token.isEnd() && i == tokens.size() - 1) {
+ // We check if this last token is at the end of the input
+ if (token.end != input.length()) {
+ String leftover = input.substring(token.end)
+ .replace("\n", "")
+ .replace("\r", "")
+ .replace(" ", "")
+ .replace("\t", "")
+ ;
+ if (!leftover.isEmpty()) {
+ throw new IllegalArgumentException("Parse error: Content detected after the end of the message: '"
+ + input.substring(token.end) + "'");
+ }
+ }
+ return result.build();
+ } else {
+ // End of message, we ignore the rest
+ throw new IllegalArgumentException("Parse error: Extra tokens at the end of the message");
+ }
+ break;
+ case PATTERN:
+ ParseResult patternResult = parsePattern(i);
+ i = patternResult.skipLen;
+ result.setPattern(patternResult.resultValue);
+ break;
+ case DECLARATION:
+ Declaration declaration = new Declaration();
+ i = parseDeclaration(i, declaration);
+ result.addLocalVariable(declaration.variableName, declaration.expr);
+ break;
+ case SELECTOR:
+ ParseResult> selectorResult = parseSelector(i);
+ result.addSelectors(selectorResult.resultValue);
+ i = selectorResult.skipLen;
+ break;
+ case VARIANT:
+ ParseResult variantResult = parseVariant(i);
+ i = variantResult.skipLen;
+ Variant variant = variantResult.resultValue;
+ result.addVariant(variant.getSelectorKeys(), variant.getPattern());
+ break;
+ case IGNORE:
+ break;
+ default:
+ throw new IllegalArgumentException("Parse error: parseMessage UNEXPECTED TOKEN: '" + token + "'");
+ }
+ }
+ throw new IllegalArgumentException("Parse error: Error parsing MessageFormatter");
+ }
+
+ private ParseResult parseVariant(int startToken) {
+ Variant.Builder result = Variant.builder();
+
+ for (int i = startToken; i < tokens.size(); i++) {
+ Token token = tokens.get(i);
+ switch (token.type) {
+ case VARIANT:
+ if (token.isStart()) { // all good
+ } else if (token.isEnd()) {
+ return new ParseResult<>(i, result.build());
+ }
+ break;
+ case LITERAL:
+ result.addSelectorKey(input.substring(token.begin + 1, token.end - 1));
+ break;
+ case NMTOKEN:
+ result.addSelectorKey(input.substring(token.begin, token.end));
+ break;
+ case DEFAULT:
+ result.addSelectorKey("*");
+ break;
+ case PATTERN:
+ ParseResult patternResult = parsePattern(i);
+ i = patternResult.skipLen;
+ result.setPattern(patternResult.resultValue);
+ break;
+ case VARIANTKEY:
+// variant.variantKey = new VariantKey(input.substring(token.begin, token.end));
+ break;
+ case IGNORE:
+ break;
+ default:
+ throw new IllegalArgumentException("Parse error: parseVariant UNEXPECTED TOKEN: '" + token + "'");
+ }
+ }
+ throw new IllegalArgumentException("Parse error: Error parsing Variant");
+ }
+
+ private ParseResult> parseSelector(int startToken) {
+ List result = new ArrayList<>();
+
+ for (int i = startToken; i < tokens.size(); i++) {
+ Token token = tokens.get(i);
+ switch (token.type) {
+ case SELECTOR:
+ if (token.isStart()) { // all good, do nothing
+ } else if (token.isEnd()) {
+ return new ParseResult<>(i, result);
+ }
+ break;
+ case EXPRESSION:
+ ParseResult exprResult = parseExpression(i);
+ i = exprResult.skipLen;
+ result.add(exprResult.resultValue);
+ break;
+ case IGNORE:
+ break;
+ default:
+ throw new IllegalArgumentException("Parse error: parseSelector UNEXPECTED TOKEN: '" + token + "'");
+ }
+ }
+ throw new IllegalArgumentException("Parse error: Error parsing selectors");
+ }
+
+ private int parseDeclaration(int startToken, Declaration declaration) {
+ for (int i = startToken; i < tokens.size(); i++) {
+ Token token = tokens.get(i);
+ switch (token.type) {
+ case DECLARATION:
+ if (token.isStart()) { // all good
+ } else if (token.isEnd()) {
+ return i;
+ }
+ break;
+ case VARIABLE:
+ declaration.variableName = input.substring(token.begin + 1, token.end);
+ break;
+ case EXPRESSION:
+ ParseResult exprResult = parseExpression(i);
+ i = exprResult.skipLen;
+ declaration.expr = exprResult.resultValue;
+ break;
+ case IGNORE:
+ break;
+ default:
+ throw new IllegalArgumentException("Parse error: parseDeclaration UNEXPECTED TOKEN: '" + token + "'");
+ }
+ }
+ throw new IllegalArgumentException("Parse error: Error parsing Declaration");
+ }
+
+ private ParseResult parsePattern(int startToken) {
+ Pattern.Builder result = Pattern.builder();
+
+ for (int i = startToken; i < tokens.size(); i++) {
+ Token token = tokens.get(i);
+ switch (token.type) {
+ case TEXT:
+ Text text = new Text(input.substring(token.begin, token.end));
+ result.add(text);
+ break;
+ case PLACEHOLDER:
+ break;
+ case EXPRESSION:
+ ParseResult exprResult = parseExpression(i);
+ i = exprResult.skipLen;
+ result.add(exprResult.resultValue);
+ break;
+ case VARIABLE:
+ case IGNORE:
+ break;
+ case PATTERN:
+ if (token.isStart() && i == startToken) { // all good, do nothing
+ } else if (token.isEnd()) {
+ return new ParseResult<>(i, result.build());
+ }
+ break;
+ default:
+ throw new IllegalArgumentException("Parse error: parsePattern UNEXPECTED TOKEN: '" + token + "'");
+ }
+ }
+ throw new IllegalArgumentException("Parse error: Error parsing Pattern");
+ }
+
+ static class Option {
+ String name;
+ Value value;
+ }
+
+ static class Declaration {
+ String variableName;
+ Expression expr;
+ }
+
+ static class Variant {
+ private final SelectorKeys selectorKeys;
+ private final Pattern pattern;
+
+ private Variant(Builder builder) {
+ this.selectorKeys = builder.selectorKeys.build();
+ this.pattern = builder.pattern;
+ }
+
+ /**
+ * Creates a builder.
+ *
+ * @return the Builder.
+ */
+ public static Builder builder() {
+ return new Builder();
+ }
+
+ public SelectorKeys getSelectorKeys() {
+ return selectorKeys;
+ }
+
+ public Pattern getPattern() {
+ return pattern;
+ }
+
+ public static class Builder {
+ private final SelectorKeys.Builder selectorKeys = SelectorKeys.builder();
+ private Pattern pattern = Pattern.builder().build();
+
+ // Prevent direct creation
+ private Builder() {
+ }
+
+ public Builder setSelectorKeys(SelectorKeys selectorKeys) {
+ this.selectorKeys.addAll(selectorKeys.getKeys());
+ return this;
+ }
+
+ public Builder addSelectorKey(String selectorKey) {
+ this.selectorKeys.add(selectorKey);
+ return this;
+ }
+
+ public Builder setPattern(Pattern pattern) {
+ this.pattern = pattern;
+ return this;
+ }
+
+ public Variant build() {
+ return new Variant(this);
+ }
+ }
+ }
+
+ static class ParseResult {
+ final int skipLen;
+ final T resultValue;
+
+ public ParseResult(int skipLen, T resultValue) {
+ this.skipLen = skipLen;
+ this.resultValue = resultValue;
+ }
+ }
+
+ private ParseResult parseExpression(int startToken) {
+ Expression.Builder result = Expression.builder();
+
+ for (int i = startToken; i < tokens.size(); i++) {
+ Token token = tokens.get(i);
+ switch (token.type) {
+ case EXPRESSION: // intentional fall-through
+ case PLACEHOLDER:
+ if (token.isStart() && i == startToken) {
+ // all good
+ } else if (token.isEnd()) {
+ return new ParseResult<>(i, result.build());
+ }
+ break;
+ case FUNCTION:
+ result.setFunctionName(input.substring(token.begin + 1, token.end));
+ break;
+ case LITERAL:
+ result.setOperand(Value.builder()
+ .setLiteral(input.substring(token.begin + 1, token.end - 1))
+ .build());
+ break;
+ case VARIABLE:
+ result.setOperand(Value.builder()
+ .setVariableName(input.substring(token.begin + 1, token.end))
+ .build());
+ break;
+ case OPTION:
+ Option option = new Option();
+ i = parseOptions(i, option);
+ result.addOption(option.name, option.value);
+ break;
+ case OPERAND:
+ break;
+ case IGNORE:
+ break;
+ default:
+ throw new IllegalArgumentException("Parse error: parseExpression UNEXPECTED TOKEN: '" + token + "'");
+ }
+ }
+ throw new IllegalArgumentException("Parse error: Error parsing Expression");
+ }
+
+ private int parseOptions(int startToken, Option option) {
+ for (int i = startToken; i < tokens.size(); i++) {
+ Token token = tokens.get(i);
+ switch (token.type) {
+ case OPTION:
+ if (token.isStart() && i == startToken) {
+ // all good
+ } else if (token.isEnd()) {
+ return i;
+ }
+ break;
+ case NAME:
+ option.name = input.substring(token.begin, token.end);
+ break;
+ case LITERAL:
+ option.value = Value.builder()
+ .setLiteral(input.substring(token.begin + 1, token.end - 1))
+ .build();
+ break;
+ case NMTOKEN:
+ option.value = Value.builder()
+ .setLiteral(input.substring(token.begin, token.end))
+ .build();
+ break;
+ case VARIABLE:
+ option.value = Value.builder()
+ .setVariableName(input.substring(token.begin + 1, token.end))
+ .build();
+ break;
+ case IGNORE:
+ break;
+ default:
+ throw new IllegalArgumentException("Parse error: parseOptions UNEXPECTED TOKEN: '" + token + "'");
+ }
+ }
+ throw new IllegalArgumentException("Parse error: Error parsing Option");
+ }
+
+ static String dataModelToString(Mf2DataModel dataModel) {
+ return dataModel.toString();
+ }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/message2/NumberFormatterFactory.java b/icu4j/main/classes/core/src/com/ibm/icu/message2/NumberFormatterFactory.java
new file mode 100644
index 000000000000..c7fd15249f68
--- /dev/null
+++ b/icu4j/main/classes/core/src/com/ibm/icu/message2/NumberFormatterFactory.java
@@ -0,0 +1,132 @@
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package com.ibm.icu.message2;
+
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+
+import com.ibm.icu.math.BigDecimal;
+import com.ibm.icu.number.LocalizedNumberFormatter;
+import com.ibm.icu.number.NumberFormatter;
+import com.ibm.icu.number.Precision;
+import com.ibm.icu.number.UnlocalizedNumberFormatter;
+import com.ibm.icu.text.FormattedValue;
+import com.ibm.icu.util.CurrencyAmount;
+
+
+/**
+ * Creates a {@link Formatter} doing numeric formatting, similar to {exp, number}
+ * in {@link com.ibm.icu.text.MessageFormat}.
+ */
+class NumberFormatterFactory implements FormatterFactory {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Formatter createFormatter(Locale locale, Map fixedOptions) {
+ return new NumberFormatterImpl(locale, fixedOptions);
+ }
+
+ static class NumberFormatterImpl implements Formatter {
+ private final Locale locale;
+ private final Map fixedOptions;
+ private final LocalizedNumberFormatter icuFormatter;
+ final boolean advanced;
+
+ private static LocalizedNumberFormatter formatterForOptions(Locale locale, Map fixedOptions) {
+ UnlocalizedNumberFormatter nf;
+ String skeleton = OptUtils.getString(fixedOptions, "skeleton");
+ if (skeleton != null) {
+ nf = NumberFormatter.forSkeleton(skeleton);
+ } else {
+ nf = NumberFormatter.with();
+ Integer minFractionDigits = OptUtils.getInteger(fixedOptions, "minimumFractionDigits");
+ if (minFractionDigits != null) {
+ nf = nf.precision(Precision.minFraction(minFractionDigits));
+ }
+ }
+ return nf.locale(locale);
+ }
+
+ NumberFormatterImpl(Locale locale, Map fixedOptions) {
+ this.locale = locale;
+ this.fixedOptions = new HashMap<>(fixedOptions);
+ String skeleton = OptUtils.getString(fixedOptions, "skeleton");
+ boolean fancy = skeleton != null;
+ this.icuFormatter = formatterForOptions(locale, fixedOptions);
+ this.advanced = fancy;
+ }
+
+ LocalizedNumberFormatter getIcuFormatter() {
+ return icuFormatter;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public String formatToString(Object toFormat, Map variableOptions) {
+ return format(toFormat, variableOptions).toString();
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public FormattedPlaceholder format(Object toFormat, Map variableOptions) {
+ LocalizedNumberFormatter realFormatter;
+ if (variableOptions.isEmpty()) {
+ realFormatter = this.icuFormatter;
+ } else {
+ Map mergedOptions = new HashMap<>(fixedOptions);
+ mergedOptions.putAll(variableOptions);
+ // This is really wasteful, as we don't use the existing
+ // formatter if even one option is variable.
+ // We can optimize, but for now will have to do.
+ realFormatter = formatterForOptions(locale, mergedOptions);
+ }
+
+ Integer offset = OptUtils.getInteger(variableOptions, "offset");
+ if (offset == null && fixedOptions != null) {
+ offset = OptUtils.getInteger(fixedOptions, "offset");
+ }
+ if (offset == null) {
+ offset = 0;
+ }
+
+ FormattedValue result = null;
+ if (toFormat == null) {
+ // This is also what MessageFormat does.
+ throw new NullPointerException("Argument to format can't be null");
+ } else if (toFormat instanceof Double) {
+ result = realFormatter.format((double) toFormat - offset);
+ } else if (toFormat instanceof Long) {
+ result = realFormatter.format((long) toFormat - offset);
+ } else if (toFormat instanceof Integer) {
+ result = realFormatter.format((int) toFormat - offset);
+ } else if (toFormat instanceof BigDecimal) {
+ BigDecimal bd = (BigDecimal) toFormat;
+ result = realFormatter.format(bd.subtract(BigDecimal.valueOf(offset)));
+ } else if (toFormat instanceof Number) {
+ result = realFormatter.format(((Number) toFormat).doubleValue() - offset);
+ } else if (toFormat instanceof CurrencyAmount) {
+ result = realFormatter.format((CurrencyAmount) toFormat);
+ } else {
+ // The behavior is not in the spec, will be in the registry.
+ // We can return "NaN", or try to parse the string as a number
+ String strValue = Objects.toString(toFormat);
+ Number nrValue = OptUtils.asNumber(strValue);
+ if (nrValue != null) {
+ result = realFormatter.format(nrValue.doubleValue() - offset);
+ } else {
+ result = new PlainStringFormattedValue("NaN");
+ }
+ }
+ return new FormattedPlaceholder(toFormat, result);
+ }
+ }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/message2/OptUtils.java b/icu4j/main/classes/core/src/com/ibm/icu/message2/OptUtils.java
new file mode 100644
index 000000000000..21a839bd8b6d
--- /dev/null
+++ b/icu4j/main/classes/core/src/com/ibm/icu/message2/OptUtils.java
@@ -0,0 +1,52 @@
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package com.ibm.icu.message2;
+
+import java.util.Map;
+
+class OptUtils {
+ private OptUtils() {}
+
+ static Number asNumber(Object value) {
+ if (value instanceof Number) {
+ return (Number) value;
+ }
+ if (value instanceof CharSequence) {
+ String strValue = value.toString();
+ try {
+ return Double.parseDouble(strValue);
+ } catch (NumberFormatException e) {
+ }
+ try {
+ return Integer.decode(strValue);
+ } catch (NumberFormatException e) {
+ }
+ }
+ return null;
+ }
+
+ static Integer getInteger(Map options, String key) {
+ Object value = options.get(key);
+ if (value == null) {
+ return null;
+ }
+ Number nrValue = asNumber(value);
+ if (nrValue != null) {
+ return nrValue.intValue();
+ }
+ return null;
+ }
+
+ static String getString(Map options, String key) {
+ Object value = options.get(key);
+ if (value == null) {
+ return null;
+ }
+ if (value instanceof CharSequence) {
+ return value.toString();
+ }
+ return null;
+ }
+
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/message2/PlainStringFormattedValue.java b/icu4j/main/classes/core/src/com/ibm/icu/message2/PlainStringFormattedValue.java
new file mode 100644
index 000000000000..d11c43cc639d
--- /dev/null
+++ b/icu4j/main/classes/core/src/com/ibm/icu/message2/PlainStringFormattedValue.java
@@ -0,0 +1,132 @@
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package com.ibm.icu.message2;
+
+import java.io.IOException;
+import java.io.UncheckedIOException;
+import java.text.AttributedCharacterIterator;
+
+import com.ibm.icu.text.ConstrainedFieldPosition;
+import com.ibm.icu.text.FormattedValue;
+
+/**
+ * Very-very rough implementation of FormattedValue, packaging a string.
+ * Expect it to change.
+ *
+ * @internal ICU 72 technology preview. Visible For Testing.
+ * @deprecated This API is for ICU internal use only.
+ */
+@Deprecated
+public class PlainStringFormattedValue implements FormattedValue {
+ private final String value;
+
+ /**
+ * Constructor, taking the string to store.
+ *
+ * @param value the string value to store
+ *
+ * @internal ICU 72 technology preview. Visible For Testing.
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ public PlainStringFormattedValue(String value) {
+ if (value == null) {
+ throw new IllegalAccessError("Should not try to wrap a null in a formatted value");
+ }
+ this.value = value;
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @internal ICU 72 technology preview. Visible For Testing.
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ @Override
+ public int length() {
+ return value == null ? 0 : value.length();
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @internal ICU 72 technology preview. Visible For Testing.
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ @Override
+ public char charAt(int index) {
+ return value.charAt(index);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @internal ICU 72 technology preview. Visible For Testing.
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ @Override
+ public CharSequence subSequence(int start, int end) {
+ return value.subSequence(start, end);
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @internal ICU 72 technology preview. Visible For Testing.
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ @Override
+ public A appendTo(A appendable) {
+ try {
+ appendable.append(value);
+ } catch (IOException e) {
+ throw new UncheckedIOException("problem appending", e);
+ }
+ return appendable;
+ }
+
+ /**
+ * Not yet implemented.
+ *
+ * {@inheritDoc}
+ *
+ * @internal ICU 72 technology preview. Visible For Testing.
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ @Override
+ public boolean nextPosition(ConstrainedFieldPosition cfpos) {
+ throw new RuntimeException("nextPosition not yet implemented");
+ }
+
+ /**
+ * Not yet implemented.
+ *
+ * {@inheritDoc}
+ *
+ * @internal ICU 72 technology preview. Visible For Testing.
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ @Override
+ public AttributedCharacterIterator toCharacterIterator() {
+ throw new RuntimeException("toCharacterIterator not yet implemented");
+ }
+
+ /**
+ * {@inheritDoc}
+ *
+ * @internal ICU 72 technology preview. Visible For Testing.
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ @Override
+ public String toString() {
+ return value;
+ }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/message2/PluralSelectorFactory.java b/icu4j/main/classes/core/src/com/ibm/icu/message2/PluralSelectorFactory.java
new file mode 100644
index 000000000000..34458ac65744
--- /dev/null
+++ b/icu4j/main/classes/core/src/com/ibm/icu/message2/PluralSelectorFactory.java
@@ -0,0 +1,110 @@
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package com.ibm.icu.message2;
+
+import java.util.Locale;
+import java.util.Map;
+
+import com.ibm.icu.number.FormattedNumber;
+import com.ibm.icu.text.FormattedValue;
+import com.ibm.icu.text.PluralRules;
+import com.ibm.icu.text.PluralRules.PluralType;
+
+/**
+ * Creates a {@link Selector} doing plural selection, similar to {exp, plural}
+ * in {@link com.ibm.icu.text.MessageFormat}.
+ */
+class PluralSelectorFactory implements SelectorFactory {
+ private final PluralType pluralType;
+
+ /**
+ * Creates a {@code PluralSelectorFactory} of the desired type.
+ *
+ * @param type the kind of plural selection we want
+ */
+ // TODO: Use an enum
+ PluralSelectorFactory(String type) {
+ switch (type) {
+ case "ordinal":
+ pluralType = PluralType.ORDINAL;
+ break;
+ case "cardinal": // intentional fallthrough
+ default:
+ pluralType = PluralType.CARDINAL;
+ }
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Selector createSelector(Locale locale, Map fixedOptions) {
+ PluralRules rules = PluralRules.forLocale(locale, pluralType);
+ return new PluralSelectorImpl(rules, fixedOptions);
+ }
+
+ private static class PluralSelectorImpl implements Selector {
+ private final PluralRules rules;
+ private Map fixedOptions;
+
+ private PluralSelectorImpl(PluralRules rules, Map fixedOptions) {
+ this.rules = rules;
+ this.fixedOptions = fixedOptions;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean matches(Object value, String key, Map variableOptions) {
+ if (value == null) {
+ return false;
+ }
+ if ("*".equals(key)) {
+ return true;
+ }
+
+ Integer offset = OptUtils.getInteger(variableOptions, "offset");
+ if (offset == null && fixedOptions != null) {
+ offset = OptUtils.getInteger(fixedOptions, "offset");
+ }
+ if (offset == null) {
+ offset = 0;
+ }
+
+ double valToCheck = Double.MIN_VALUE;
+ FormattedValue formattedValToCheck = null;
+ if (value instanceof FormattedPlaceholder) {
+ FormattedPlaceholder fph = (FormattedPlaceholder) value;
+ value = fph.getInput();
+ formattedValToCheck = fph.getFormattedValue();
+ }
+
+ if (value instanceof Double) {
+ valToCheck = (double) value;
+ } else if (value instanceof Integer) {
+ valToCheck = (Integer) value;
+ } else {
+ return false;
+ }
+
+ // If there is nothing "tricky" about the formatter part we compare values directly.
+ // Right now ICU4J checks if the formatter is a DecimalFormt, which also feels "hacky".
+ // We need something better.
+ if (!fixedOptions.containsKey("skeleton") && !variableOptions.containsKey("skeleton")) {
+ try { // for match exact.
+ if (Double.parseDouble(key) == valToCheck) {
+ return true;
+ }
+ } catch (NumberFormatException e) {
+ }
+ }
+
+ String match = formattedValToCheck instanceof FormattedNumber
+ ? rules.select((FormattedNumber) formattedValToCheck)
+ : rules.select(valToCheck - offset);
+ return match.equals(key);
+ }
+ }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/message2/Selector.java b/icu4j/main/classes/core/src/com/ibm/icu/message2/Selector.java
new file mode 100644
index 000000000000..8eb9a2635d0d
--- /dev/null
+++ b/icu4j/main/classes/core/src/com/ibm/icu/message2/Selector.java
@@ -0,0 +1,37 @@
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package com.ibm.icu.message2;
+
+import java.util.Map;
+
+/**
+ * The interface that must be implemented by all selectors
+ * that can be used from {@link MessageFormatter}.
+ *
+ * Selectors are used to choose between different message variants,
+ * similar to plural, selectordinal,
+ * and select in {@link com.ibm.icu.text.MessageFormat}.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+@Deprecated
+public interface Selector {
+ /**
+ * A method that is invoked for the object to match and each key.
+ *
+ * For example an English plural {@code matches} would return {@code true}
+ * for {@code matches(1, "1")}, {@code matches(1, "one")}, and {@code matches(1, "*")}.
+ *
+ * @param value the value to select on.
+ * @param key the key to test for matching.
+ * @param variableOptions options that are not know at build time.
+ * @return the formatted string.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ boolean matches(Object value, String key, Map variableOptions);
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/message2/SelectorFactory.java b/icu4j/main/classes/core/src/com/ibm/icu/message2/SelectorFactory.java
new file mode 100644
index 000000000000..43f44066c537
--- /dev/null
+++ b/icu4j/main/classes/core/src/com/ibm/icu/message2/SelectorFactory.java
@@ -0,0 +1,32 @@
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package com.ibm.icu.message2;
+
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * The interface that must be implemented for each selection function
+ * that can be used from {@link MessageFormatter}.
+ *
+ * The we use it to create and cache various selectors with various options.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+@Deprecated
+public interface SelectorFactory {
+ /**
+ * The method that is called to create a selector.
+ *
+ * @param locale the locale to use for selection.
+ * @param fixedOptions the options to use for selection. The keys and values are function dependent.
+ * @return The Selector.
+ *
+ * @internal ICU 72 technology preview
+ * @deprecated This API is for ICU internal use only.
+ */
+ @Deprecated
+ Selector createSelector(Locale locale, Map fixedOptions);
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/message2/TextSelectorFactory.java b/icu4j/main/classes/core/src/com/ibm/icu/message2/TextSelectorFactory.java
new file mode 100644
index 000000000000..6c9cf5031f9c
--- /dev/null
+++ b/icu4j/main/classes/core/src/com/ibm/icu/message2/TextSelectorFactory.java
@@ -0,0 +1,36 @@
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package com.ibm.icu.message2;
+
+import java.util.Locale;
+import java.util.Map;
+
+
+/**
+ * Creates a {@link Selector} doing literal selection, similar to {exp, select}
+ * in {@link com.ibm.icu.text.MessageFormat}.
+ */
+class TextSelectorFactory implements SelectorFactory {
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public Selector createSelector(Locale locale, Map fixedOptions) {
+ return new TextSelector();
+ }
+
+ private static class TextSelector implements Selector {
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public boolean matches(Object value, String key, Map variableOptions) {
+ if ("*".equals(key)) {
+ return true;
+ }
+ return key.equals(value);
+ }
+ }
+}
diff --git a/icu4j/main/classes/core/src/com/ibm/icu/message2/package.html b/icu4j/main/classes/core/src/com/ibm/icu/message2/package.html
new file mode 100644
index 000000000000..95e8f7ae8db1
--- /dev/null
+++ b/icu4j/main/classes/core/src/com/ibm/icu/message2/package.html
@@ -0,0 +1,15 @@
+
+
+
+
+ICU4J com.ibm.icu.message2 Package Overview
+
+
+
+Tech Preview implementation of the
+MessageFormat v2 specification.
+
+
+
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/message2/Args.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/message2/Args.java
new file mode 100644
index 000000000000..815384d0e0ee
--- /dev/null
+++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/message2/Args.java
@@ -0,0 +1,183 @@
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package com.ibm.icu.dev.test.message2;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.ibm.icu.message2.MessageFormatter;
+
+/**
+ * Convenience class that provides the same functionality as
+ * Map.of introduced in JDK 11, which can't be used yet for ICU4J.
+ *
+ * The returned Map is immutable, to prove that the {@link MessageFormatter}
+ * does not change it
+ */
+@SuppressWarnings("javadoc")
+public class Args {
+
+ public static final Map NONE = new HashMap<>();
+
+ public static Map of(
+ String argName0, Object argValue0) {
+ Map result = new HashMap<>();
+ result.put(argName0, argValue0);
+ return Collections.unmodifiableMap(result);
+ }
+
+ public static Map of(
+ String argName0, Object argValue0,
+ String argName1, Object argValue1) {
+ Map result = new HashMap<>();
+ result.put(argName0, argValue0);
+ result.put(argName1, argValue1);
+ return Collections.unmodifiableMap(result);
+ }
+
+ public static Map of(
+ String argName0, Object argValue0,
+ String argName1, Object argValue1,
+ String argName2, Object argValue2) {
+ Map result = new HashMap<>();
+ result.put(argName0, argValue0);
+ result.put(argName1, argValue1);
+ result.put(argName2, argValue2);
+ return Collections.unmodifiableMap(result);
+ }
+
+ public static Map of(
+ String argName0, Object argValue0,
+ String argName1, Object argValue1,
+ String argName2, Object argValue2,
+ String argName3, Object argValue3) {
+ Map result = new HashMap<>();
+ result.put(argName0, argValue0);
+ result.put(argName1, argValue1);
+ result.put(argName2, argValue2);
+ result.put(argName3, argValue3);
+ return Collections.unmodifiableMap(result);
+ }
+
+ public static Map of(
+ String argName0, Object argValue0,
+ String argName1, Object argValue1,
+ String argName2, Object argValue2,
+ String argName3, Object argValue3,
+ String argName4, Object argValue4) {
+ Map result = new HashMap<>();
+ result.put(argName0, argValue0);
+ result.put(argName1, argValue1);
+ result.put(argName2, argValue2);
+ result.put(argName3, argValue3);
+ result.put(argName4, argValue4);
+ return Collections.unmodifiableMap(result);
+ }
+
+ public static Map of(
+ String argName0, Object argValue0,
+ String argName1, Object argValue1,
+ String argName2, Object argValue2,
+ String argName3, Object argValue3,
+ String argName4, Object argValue4,
+ String argName5, Object argValue5) {
+ Map result = new HashMap<>();
+ result.put(argName0, argValue0);
+ result.put(argName1, argValue1);
+ result.put(argName2, argValue2);
+ result.put(argName3, argValue3);
+ result.put(argName4, argValue4);
+ result.put(argName5, argValue5);
+ return Collections.unmodifiableMap(result);
+ }
+
+ public static Map of(
+ String argName0, Object argValue0,
+ String argName1, Object argValue1,
+ String argName2, Object argValue2,
+ String argName3, Object argValue3,
+ String argName4, Object argValue4,
+ String argName5, Object argValue5,
+ String argName6, Object argValue6) {
+ Map result = new HashMap<>();
+ result.put(argName0, argValue0);
+ result.put(argName1, argValue1);
+ result.put(argName2, argValue2);
+ result.put(argName3, argValue3);
+ result.put(argName4, argValue4);
+ result.put(argName5, argValue5);
+ result.put(argName6, argValue6);
+ return Collections.unmodifiableMap(result);
+ }
+
+ public static Map of(
+ String argName0, Object argValue0,
+ String argName1, Object argValue1,
+ String argName2, Object argValue2,
+ String argName3, Object argValue3,
+ String argName4, Object argValue4,
+ String argName5, Object argValue5,
+ String argName6, Object argValue6,
+ String argName7, Object argValue7) {
+ Map result = new HashMap<>();
+ result.put(argName0, argValue0);
+ result.put(argName1, argValue1);
+ result.put(argName2, argValue2);
+ result.put(argName3, argValue3);
+ result.put(argName4, argValue4);
+ result.put(argName5, argValue5);
+ result.put(argName6, argValue6);
+ result.put(argName7, argValue7);
+ return Collections.unmodifiableMap(result);
+ }
+
+ public static Map of(
+ String argName0, Object argValue0,
+ String argName1, Object argValue1,
+ String argName2, Object argValue2,
+ String argName3, Object argValue3,
+ String argName4, Object argValue4,
+ String argName5, Object argValue5,
+ String argName6, Object argValue6,
+ String argName7, Object argValue7,
+ String argName8, Object argValue8) {
+ Map result = new HashMap<>();
+ result.put(argName0, argValue0);
+ result.put(argName1, argValue1);
+ result.put(argName2, argValue2);
+ result.put(argName3, argValue3);
+ result.put(argName4, argValue4);
+ result.put(argName5, argValue5);
+ result.put(argName6, argValue6);
+ result.put(argName7, argValue7);
+ result.put(argName8, argValue8);
+ return Collections.unmodifiableMap(result);
+ }
+
+ public static Map of(
+ String argName0, Object argValue0,
+ String argName1, Object argValue1,
+ String argName2, Object argValue2,
+ String argName3, Object argValue3,
+ String argName4, Object argValue4,
+ String argName5, Object argValue5,
+ String argName6, Object argValue6,
+ String argName7, Object argValue7,
+ String argName8, Object argValue8,
+ String argName9, Object argValue9) {
+ Map result = new HashMap<>();
+ result.put(argName0, argValue0);
+ result.put(argName1, argValue1);
+ result.put(argName2, argValue2);
+ result.put(argName3, argValue3);
+ result.put(argName4, argValue4);
+ result.put(argName5, argValue5);
+ result.put(argName6, argValue6);
+ result.put(argName7, argValue7);
+ result.put(argName8, argValue8);
+ result.put(argName9, argValue9);
+ return Collections.unmodifiableMap(result);
+ }
+}
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/message2/CustomFormatterGrammarCaseTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/message2/CustomFormatterGrammarCaseTest.java
new file mode 100644
index 000000000000..065f0cbd7a1e
--- /dev/null
+++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/message2/CustomFormatterGrammarCaseTest.java
@@ -0,0 +1,114 @@
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package com.ibm.icu.dev.test.message2;
+
+import java.util.Locale;
+import java.util.Map;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import com.ibm.icu.dev.test.TestFmwk;
+import com.ibm.icu.message2.FormattedPlaceholder;
+import com.ibm.icu.message2.Formatter;
+import com.ibm.icu.message2.FormatterFactory;
+import com.ibm.icu.message2.MessageFormatter;
+import com.ibm.icu.message2.Mf2FunctionRegistry;
+import com.ibm.icu.message2.PlainStringFormattedValue;
+
+/**
+ * Showing a custom formatter that can handle grammatical cases.
+ */
+@RunWith(JUnit4.class)
+@SuppressWarnings("javadoc")
+public class CustomFormatterGrammarCaseTest extends TestFmwk {
+
+ static class GrammarCasesFormatterFactory implements FormatterFactory {
+
+ @Override
+ public Formatter createFormatter(Locale locale, Map fixedOptions) {
+ Object grammarCase = fixedOptions.get("case");
+ return new GrammarCasesFormatterImpl(grammarCase == null ? "" : grammarCase.toString());
+ }
+
+ static class GrammarCasesFormatterImpl implements Formatter {
+ final String grammarCase;
+
+ GrammarCasesFormatterImpl(String grammarCase) {
+ this.grammarCase = grammarCase;
+ }
+
+ // Romanian naive and incomplete rules, just to make things work for testing.
+ private static String getDativeAndGenitive(String value) {
+ if (value.endsWith("ana"))
+ return value.substring(0, value.length() - 3) + "nei";
+ if (value.endsWith("ca"))
+ return value.substring(0, value.length() - 2) + "căi";
+ if (value.endsWith("ga"))
+ return value.substring(0, value.length() - 2) + "găi";
+ if (value.endsWith("a"))
+ return value.substring(0, value.length() - 1) + "ei";
+ return "lui " + value;
+ }
+
+ @Override
+ public String formatToString(Object toFormat, Map variableOptions) {
+ return format(toFormat, variableOptions).toString();
+ }
+
+ @Override
+ public FormattedPlaceholder format(Object toFormat, Map variableOptions) {
+ String result;
+ if (toFormat == null) {
+ result = null;
+ } else if (toFormat instanceof CharSequence) {
+ String value = (String) toFormat;
+ switch (grammarCase) {
+ case "dative": // intentional fallback
+ case "genitive":
+ result = getDativeAndGenitive(value);
+ // and so on for other cases, but I don't care to add more for now
+ break;
+ default:
+ result = value;
+ }
+ } else {
+ result = toFormat.toString();
+ }
+ return new FormattedPlaceholder(toFormat, new PlainStringFormattedValue(result));
+ }
+ }
+
+ }
+
+ static final Mf2FunctionRegistry REGISTRY = Mf2FunctionRegistry.builder()
+ .setFormatter("grammarBB", new GrammarCasesFormatterFactory())
+ .build();
+
+ @Test
+ public void test() {
+ MessageFormatter mf = MessageFormatter.builder()
+ .setFunctionRegistry(REGISTRY)
+ .setLocale(Locale.forLanguageTag("ro"))
+ .setPattern("{Cartea {$owner :grammarBB case=genitive}}")
+ .build();
+
+ assertEquals("case - genitive", "Cartea Mariei", mf.formatToString(Args.of("owner", "Maria")));
+ assertEquals("case - genitive", "Cartea Rodicăi", mf.formatToString(Args.of("owner", "Rodica")));
+ assertEquals("case - genitive", "Cartea Ilenei", mf.formatToString(Args.of("owner", "Ileana")));
+ assertEquals("case - genitive", "Cartea lui Petre", mf.formatToString(Args.of("owner", "Petre")));
+
+ mf = MessageFormatter.builder()
+ .setFunctionRegistry(REGISTRY)
+ .setLocale(Locale.forLanguageTag("ro"))
+ .setPattern("{M-a sunat {$owner :grammarBB case=nominative}}")
+ .build();
+
+ assertEquals("case - nominative", "M-a sunat Maria", mf.formatToString(Args.of("owner", "Maria")));
+ assertEquals("case - nominative", "M-a sunat Rodica", mf.formatToString(Args.of("owner", "Rodica")));
+ assertEquals("case - nominative", "M-a sunat Ileana", mf.formatToString(Args.of("owner", "Ileana")));
+ assertEquals("case - nominative", "M-a sunat Petre", mf.formatToString(Args.of("owner", "Petre")));
+ }
+}
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/message2/CustomFormatterListTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/message2/CustomFormatterListTest.java
new file mode 100644
index 000000000000..1b3bac2ff765
--- /dev/null
+++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/message2/CustomFormatterListTest.java
@@ -0,0 +1,98 @@
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package com.ibm.icu.dev.test.message2;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Locale;
+import java.util.Map;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import com.ibm.icu.dev.test.TestFmwk;
+import com.ibm.icu.message2.FormattedPlaceholder;
+import com.ibm.icu.message2.Formatter;
+import com.ibm.icu.message2.FormatterFactory;
+import com.ibm.icu.message2.Mf2FunctionRegistry;
+import com.ibm.icu.message2.PlainStringFormattedValue;
+import com.ibm.icu.text.ListFormatter;
+import com.ibm.icu.text.ListFormatter.Type;
+import com.ibm.icu.text.ListFormatter.Width;
+
+/**
+ * Showing a custom formatter for a list, using the existing ICU {@link ListFormatter}.
+ */
+@RunWith(JUnit4.class)
+@SuppressWarnings("javadoc")
+public class CustomFormatterListTest extends TestFmwk {
+
+ static class ListFormatterFactory implements FormatterFactory {
+
+ @Override
+ public Formatter createFormatter(Locale locale, Map fixedOptions) {
+ return new ListFormatterImpl(locale, fixedOptions);
+ }
+
+ static class ListFormatterImpl implements Formatter {
+ private final ListFormatter lf;
+
+ ListFormatterImpl(Locale locale, Map fixedOptions) {
+ Object oType = fixedOptions.get("type");
+ Type type = oType == null
+ ? ListFormatter.Type.AND
+ : ListFormatter.Type.valueOf(oType.toString());
+ Object oWidth = fixedOptions.get("width");
+ Width width = oWidth == null
+ ? ListFormatter.Width.WIDE
+ : ListFormatter.Width.valueOf(oWidth.toString());
+ lf = ListFormatter.getInstance(locale, type, width);
+ }
+
+ @Override
+ public String formatToString(Object toFormat, Map variableOptions) {
+ return format(toFormat, variableOptions).toString();
+ }
+
+ @Override
+ public FormattedPlaceholder format(Object toFormat, Map variableOptions) {
+ String result;
+ if (toFormat instanceof Object[]) {
+ result = lf.format((Object[]) toFormat);
+ } else if (toFormat instanceof Collection>) {
+ result = lf.format((Collection>) toFormat);
+ } else {
+ result = toFormat == null ? "null" : toFormat.toString();
+ }
+ return new FormattedPlaceholder(toFormat, new PlainStringFormattedValue(result));
+ }
+ }
+ }
+
+ static final Mf2FunctionRegistry REGISTRY = Mf2FunctionRegistry.builder()
+ .setFormatter("listformat", new ListFormatterFactory())
+ .build();
+
+ @Test
+ public void test() {
+ String [] progLanguages = {
+ "C/C++",
+ "Java",
+ "Python"
+ };
+
+ TestUtils.runTestCase(REGISTRY, new TestCase.Builder()
+ .pattern("{I know {$languages :listformat type=AND}!}")
+ .arguments(Args.of("languages", progLanguages))
+ .expected("I know C/C++, Java, and Python!")
+ .build());
+
+ TestUtils.runTestCase(REGISTRY, new TestCase.Builder()
+ .pattern("{You are allowed to use {$languages :listformat type=OR}!}")
+ .arguments(Args.of("languages", Arrays.asList(progLanguages)))
+ .expected("You are allowed to use C/C++, Java, or Python!")
+ .build());
+ }
+}
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/message2/CustomFormatterMessageRefTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/message2/CustomFormatterMessageRefTest.java
new file mode 100644
index 000000000000..9b6b153fdaca
--- /dev/null
+++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/message2/CustomFormatterMessageRefTest.java
@@ -0,0 +1,129 @@
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package com.ibm.icu.dev.test.message2;
+
+import java.util.Locale;
+import java.util.Map;
+import java.util.Properties;
+
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import com.ibm.icu.dev.test.TestFmwk;
+import com.ibm.icu.message2.FormattedPlaceholder;
+import com.ibm.icu.message2.Formatter;
+import com.ibm.icu.message2.FormatterFactory;
+import com.ibm.icu.message2.MessageFormatter;
+import com.ibm.icu.message2.Mf2FunctionRegistry;
+import com.ibm.icu.message2.PlainStringFormattedValue;
+
+/**
+ * Showing a custom formatter that can implement message references.
+ *
+ * Supporting this functionality was strongly requested as a part of the core specification.
+ * But this shows that it can be easily implemented as a custom function.
+ */
+@RunWith(JUnit4.class)
+@SuppressWarnings("javadoc")
+public class CustomFormatterMessageRefTest extends TestFmwk {
+
+ static class ResourceManagerFactory implements FormatterFactory {
+
+ @Override
+ public Formatter createFormatter(Locale locale, Map fixedOptions) {
+ return new ResourceManagerFactoryImpl(locale, fixedOptions);
+ }
+
+ static class ResourceManagerFactoryImpl implements Formatter {
+ final Map options;
+
+ ResourceManagerFactoryImpl(Locale locale, Map options) {
+ this.options = options;
+ }
+
+ @Override
+ public FormattedPlaceholder format(Object toFormat, Map variableOptions) {
+ String result = null;
+ Object oProps = options.get("resbundle");
+ // If it was not in the fixed options, try in the variable ones
+ if (oProps == null) {
+ oProps = variableOptions.get("resbundle");
+ }
+ if (oProps != null && oProps instanceof Properties) {
+ Properties props = (Properties) oProps;
+ Object msg = props.get(toFormat.toString());
+ MessageFormatter mf = MessageFormatter.builder()
+ .setPattern(msg.toString())
+ .build();
+ result = mf.formatToString(options);
+ }
+ return new FormattedPlaceholder(toFormat, new PlainStringFormattedValue(result));
+ }
+
+ @Override
+ public String formatToString(Object toFormat, Map variableOptions) {
+ return format(toFormat, variableOptions).toString();
+ }
+ }
+ }
+
+ static final Mf2FunctionRegistry REGISTRY = Mf2FunctionRegistry.builder()
+ .setFormatter("msgRef", new ResourceManagerFactory())
+ .build();
+
+ static final Properties PROPERTIES = new Properties();
+
+ @BeforeClass
+ static public void beforeClass() {
+ PROPERTIES.put("firefox", "match {$gcase :select} when genitive {Firefoxin} when * {Firefox}");
+ PROPERTIES.put("chrome", "match {$gcase :select} when genitive {Chromen} when * {Chrome}");
+ PROPERTIES.put("safari", "match {$gcase :select} when genitive {Safarin} when * {Safari}");
+ }
+
+ @Test
+ public void testSimpleGrammarSelection() {
+ MessageFormatter mf = MessageFormatter.builder()
+ .setPattern(PROPERTIES.getProperty("firefox"))
+ .build();
+ assertEquals("cust-grammar", "Firefox", mf.formatToString(Args.of("gcase", "whatever")));
+ assertEquals("cust-grammar", "Firefoxin", mf.formatToString(Args.of("gcase", "genitive")));
+
+ mf = MessageFormatter.builder()
+ .setPattern(PROPERTIES.getProperty("chrome"))
+ .build();
+ assertEquals("cust-grammar", "Chrome", mf.formatToString(Args.of("gcase", "whatever")));
+ assertEquals("cust-grammar", "Chromen", mf.formatToString(Args.of("gcase", "genitive")));
+ }
+
+ @Test
+ public void test() {
+ StringBuffer browser = new StringBuffer();
+ Map arguments = Args.of(
+ "browser", browser,
+ "res", PROPERTIES);
+
+ MessageFormatter mf1 = MessageFormatter.builder()
+ .setFunctionRegistry(REGISTRY)
+ .setPattern("{Please start {$browser :msgRef gcase=genitive resbundle=$res}}")
+ .build();
+ MessageFormatter mf2 = MessageFormatter.builder()
+ .setFunctionRegistry(REGISTRY)
+ .setPattern("{Please start {$browser :msgRef resbundle=$res}}")
+ .build();
+
+ browser.replace(0, browser.length(), "firefox");
+ assertEquals("cust-grammar", "Please start Firefoxin", mf1.formatToString(arguments));
+ assertEquals("cust-grammar", "Please start Firefox", mf2.formatToString(arguments));
+
+ browser.replace(0, browser.length(), "chrome");
+ assertEquals("cust-grammar", "Please start Chromen", mf1.formatToString(arguments));
+ assertEquals("cust-grammar", "Please start Chrome", mf2.formatToString(arguments));
+
+ browser.replace(0, browser.length(), "safari");
+ assertEquals("cust-grammar", "Please start Safarin", mf1.formatToString(arguments));
+ assertEquals("cust-grammar", "Please start Safari", mf2.formatToString(arguments));
+ }
+}
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/message2/CustomFormatterPersonTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/message2/CustomFormatterPersonTest.java
new file mode 100644
index 000000000000..b60149a8e4ed
--- /dev/null
+++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/message2/CustomFormatterPersonTest.java
@@ -0,0 +1,198 @@
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package com.ibm.icu.dev.test.message2;
+
+import java.util.Locale;
+import java.util.Map;
+import java.util.Objects;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import com.ibm.icu.dev.test.TestFmwk;
+import com.ibm.icu.message2.FormattedPlaceholder;
+import com.ibm.icu.message2.Formatter;
+import com.ibm.icu.message2.FormatterFactory;
+import com.ibm.icu.message2.Mf2FunctionRegistry;
+import com.ibm.icu.message2.PlainStringFormattedValue;
+
+/**
+ * Showing a custom formatter for a user defined class.
+ */
+@RunWith(JUnit4.class)
+@SuppressWarnings("javadoc")
+public class CustomFormatterPersonTest extends TestFmwk {
+
+ public static class Person {
+ final String title;
+ final String firstName;
+ final String lastName;
+
+ public Person(String title, String firstName, String lastName) {
+ this.title = title;
+ this.firstName = firstName;
+ this.lastName = lastName;
+ }
+
+ @Override
+ public String toString() {
+ return "Person {title='" + title + "', firstName='" + firstName + "', lastName='" + lastName + "'}";
+ }
+ }
+
+ public static class PersonNameFormatterFactory implements FormatterFactory {
+ @Override
+ public Formatter createFormatter(Locale locale, Map fixedOptions) {
+ return new PersonNameFormatterImpl(fixedOptions.get("formality"), fixedOptions.get("length"));
+ }
+
+ static class PersonNameFormatterImpl implements Formatter {
+ boolean useFormal = false;
+ final String length;
+
+ public PersonNameFormatterImpl(Object level, Object length) {
+ this.useFormal = "formal".equals(level);
+ this.length = Objects.toString(length);
+ }
+
+ @Override
+ public String formatToString(Object toFormat, Map variableOptions) {
+ return format(toFormat, variableOptions).toString();
+ }
+
+ // Very-very primitive implementation of the "CLDR Person Name Formatting" spec:
+ // https://docs.google.com/document/d/1uvv6gdkuFwtbNV26Pk7ddfZult4unYwR6DnnKYbujUo/
+ @Override
+ public FormattedPlaceholder format(Object toFormat, Map variableOptions) {
+ String result;
+ if (toFormat instanceof Person) {
+ Person person = (Person) toFormat;
+ switch (length) {
+ case "long":
+ result = person.title + " " + person.firstName + " " + person.lastName;
+ break;
+ case "medium":
+ result = useFormal
+ ? person.firstName + " " + person.lastName
+ : person.title + " " + person.firstName;
+ break;
+ case "short": // intentional fall-through
+ default:
+ result = useFormal
+ ? person.title + " " + person.lastName
+ : person.firstName;
+ }
+ } else {
+ result = Objects.toString(toFormat);
+ }
+ return new FormattedPlaceholder(toFormat, new PlainStringFormattedValue(result));
+ }
+ }
+ }
+
+ private static final Mf2FunctionRegistry CUSTOM_FUNCTION_REGISTRY = Mf2FunctionRegistry.builder()
+ .setFormatter("person", new PersonNameFormatterFactory())
+ .setDefaultFormatterNameForType(Person.class, "person")
+ .build();
+
+ @Test
+ public void testCustomFunctions() {
+ Person who = new Person("Mr.", "John", "Doe");
+
+ TestUtils.runTestCase(new TestCase.Builder()
+ .pattern("{Hello {$name :person formality=formal}}")
+ .arguments(Args.of("name", who))
+ .expected("Hello {$name}")
+ .errors("person function unknown when called without a custom registry")
+ .build());
+
+ TestUtils.runTestCase(new TestCase.Builder()
+ .pattern("{Hello {$name :person formality=informal}}")
+ .arguments(Args.of("name", who))
+ .expected("Hello {$name}")
+ .errors("person function unknown when called without a custom registry")
+ .build());
+
+ TestUtils.runTestCase(CUSTOM_FUNCTION_REGISTRY, new TestCase.Builder()
+ .pattern("{Hello {$name :person formality=formal}}")
+ .arguments(Args.of("name", who))
+ .expected("Hello Mr. Doe")
+ .build());
+ TestUtils.runTestCase(CUSTOM_FUNCTION_REGISTRY, new TestCase.Builder()
+ .pattern("{Hello {$name :person formality=informal}}")
+ .arguments(Args.of("name", who))
+ .expected("Hello John")
+ .build());
+ TestUtils.runTestCase(CUSTOM_FUNCTION_REGISTRY, new TestCase.Builder()
+ .pattern("{Hello {$name :person formality=formal length=long}}")
+ .arguments(Args.of("name", who))
+ .expected("Hello Mr. John Doe")
+ .build());
+ TestUtils.runTestCase(CUSTOM_FUNCTION_REGISTRY, new TestCase.Builder()
+ .pattern("{Hello {$name :person formality=formal length=medium}}")
+ .arguments(Args.of("name", who))
+ .expected("Hello John Doe")
+ .build());
+ TestUtils.runTestCase(CUSTOM_FUNCTION_REGISTRY, new TestCase.Builder()
+ .pattern("{Hello {$name :person formality=formal length=short}}")
+ .arguments(Args.of("name", who))
+ .expected("Hello Mr. Doe")
+ .build());
+ }
+
+ @Test
+ public void testCustomFunctionsComplexMessage() {
+ Person femalePerson = new Person("Ms.", "Jane", "Doe");
+ Person malePerson = new Person("Mr.", "John", "Doe");
+ Person unknownPerson = new Person("Mr./Ms.", "Anonymous", "Doe");
+ String message = ""
+ + "let $hostName = {$host :person length=long}\n"
+ + "let $guestName = {$guest :person length=long}\n"
+ + "let $guestsOther = {$guestCount :number offset=1}\n"
+ // + "\n"
+ + "match {$hostGender :gender} {$guestCount :plural}\n"
+ // + "\n"
+ + "when female 0 {{$hostName} does not give a party.}\n"
+ + "when female 1 {{$hostName} invites {$guestName} to her party.}\n"
+ + "when female 2 {{$hostName} invites {$guestName} and one other person to her party.}\n"
+ + "when female * {{$hostName} invites {$guestName} and {$guestsOther} other people to her party.}\n"
+ // + "\n"
+ + "when male 0 {{$hostName} does not give a party.}\n"
+ + "when male 1 {{$hostName} invites {$guestName} to his party.}\n"
+ + "when male 2 {{$hostName} invites {$guestName} and one other person to his party.}\n"
+ + "when male * {{$hostName} invites {$guestName} and {$guestsOther} other people to his party.}\n"
+ // + "\n"
+ + "when * 0 {{$hostName} does not give a party.}\n"
+ + "when * 1 {{$hostName} invites {$guestName} to their party.}\n"
+ + "when * 2 {{$hostName} invites {$guestName} and one other person to their party.}\n"
+ + "when * * {{$hostName} invites {$guestName} and {$guestsOther} other people to their party.}\n";
+
+ TestUtils.runTestCase(CUSTOM_FUNCTION_REGISTRY, new TestCase.Builder()
+ .pattern(message)
+ .arguments(Args.of("hostGender", "female", "host", femalePerson, "guest", malePerson, "guestCount", 3))
+ .expected("Ms. Jane Doe invites Mr. John Doe and 2 other people to her party.")
+ .build());
+ TestUtils.runTestCase(CUSTOM_FUNCTION_REGISTRY, new TestCase.Builder()
+ .pattern(message)
+ .arguments(Args.of("hostGender", "female", "host", femalePerson, "guest", malePerson, "guestCount", 2))
+ .expected("Ms. Jane Doe invites Mr. John Doe and one other person to her party.")
+ .build());
+ TestUtils.runTestCase(CUSTOM_FUNCTION_REGISTRY, new TestCase.Builder()
+ .pattern(message)
+ .arguments(Args.of("hostGender", "female", "host", femalePerson, "guest", malePerson, "guestCount", 1))
+ .expected("Ms. Jane Doe invites Mr. John Doe to her party.")
+ .build());
+ TestUtils.runTestCase(CUSTOM_FUNCTION_REGISTRY, new TestCase.Builder()
+ .pattern(message)
+ .arguments(Args.of("hostGender", "male", "host", malePerson, "guest", femalePerson, "guestCount", 3))
+ .expected("Mr. John Doe invites Ms. Jane Doe and 2 other people to his party.")
+ .build());
+ TestUtils.runTestCase(CUSTOM_FUNCTION_REGISTRY, new TestCase.Builder()
+ .pattern(message)
+ .arguments(Args.of("hostGender", "unknown", "host", unknownPerson, "guest", femalePerson, "guestCount", 2))
+ .expected("Mr./Ms. Anonymous Doe invites Ms. Jane Doe and one other person to their party.")
+ .build());
+ }
+}
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/message2/FromJsonTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/message2/FromJsonTest.java
new file mode 100644
index 000000000000..71030ab5f3fe
--- /dev/null
+++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/message2/FromJsonTest.java
@@ -0,0 +1,422 @@
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package com.ibm.icu.dev.test.message2;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import com.ibm.icu.dev.test.TestFmwk;
+
+/**
+ * These tests come from the test suite created for the JavaScript implementation of MessageFormat v2.
+ *
+ * Original JSON file
+ * here.
+ */
+@RunWith(JUnit4.class)
+@SuppressWarnings("javadoc")
+public class FromJsonTest extends TestFmwk {
+
+ static final TestCase[] TEST_CASES = {
+ new TestCase.Builder()
+ .pattern("{hello}")
+ .expected("hello")
+ .build(),
+ new TestCase.Builder()
+ .pattern("{hello {(world)}}")
+ .expected("hello world")
+ .build(),
+ new TestCase.Builder()
+ .pattern("{hello {()}}")
+ .expected("hello ")
+ .build(),
+ new TestCase.Builder()
+ .pattern("{hello {$place}}")
+ .arguments(Args.of("place", "world"))
+ .expected("hello world")
+ .build(),
+ new TestCase.Builder()
+ .pattern("{hello {$place}}")
+ .expected("hello {$place}")
+ // errorsJs: ["missing-var"]
+ .build(),
+ new TestCase.Builder()
+ .pattern("{{$one} and {$two}}")
+ .arguments(Args.of("one", 1.3, "two", 4.2))
+ .expected("1.3 and 4.2")
+ .build(),
+ new TestCase.Builder()
+ .pattern("{{$one} et {$two}}")
+ .locale("fr")
+ .arguments(Args.of("one", 1.3, "two", 4.2))
+ .expected("1,3 et 4,2")
+ .build(),
+ new TestCase.Builder()
+ .pattern("{hello {(4.2) :number}}")
+ .expected("hello 4.2")
+ .build(),
+ new TestCase.Builder() // not in the original JSON
+ .locale("ar-EG")
+ .pattern("{hello {(4.2) :number}}")
+ .expected("hello \u0664\u066B\u0662")
+ .build(),
+ new TestCase.Builder()
+ .pattern("{hello {(foo) :number}}")
+ .expected("hello NaN")
+ .build(),
+ new TestCase.Builder()
+ .pattern("{hello {:number}}")
+ .expected("hello NaN")
+ // This is different from JS, should be an error.
+ .errors("ICU4J: exception.")
+ .build(),
+ new TestCase.Builder()
+ .pattern("{hello {(4.2) :number minimumFractionDigits=2}}")
+ .expected("hello 4.20")
+ .build(),
+ new TestCase.Builder()
+ .pattern("{hello {(4.2) :number minimumFractionDigits=(2)}}")
+ .expected("hello 4.20")
+ .build(),
+ new TestCase.Builder()
+ .pattern("{hello {(4.2) :number minimumFractionDigits=$foo}}")
+ .arguments(Args.of("foo", 2f))
+ .expected("hello 4.20")
+ .build(),
+ new TestCase.Builder()
+ .pattern("{hello {(4.2) :number minimumFractionDigits=$foo}}")
+ .arguments(Args.of("foo", "2"))
+ .expected("hello 4.20")
+ // errorsJs: ["invalid-type"]
+ .build(),
+ new TestCase.Builder()
+ .pattern("let $foo = {(bar)} {bar {$foo}}")
+ .expected("bar bar")
+ .build(),
+ new TestCase.Builder()
+ .pattern("let $foo = {(bar)} {bar {$foo}}")
+ .arguments(Args.of("foo", "foo"))
+ // expectedJs: "bar foo"
+ // It is undefined if we allow arguments to override local variables, or it is an error.
+ // And undefined who wins if that happens, the local variable of the argument.
+ .expected("bar bar")
+ .build(),
+ new TestCase.Builder()
+ .pattern("let $foo = {$bar} {bar {$foo}}")
+ .arguments(Args.of("bar", "foo"))
+ .expected("bar foo")
+ .build(),
+ new TestCase.Builder()
+ .pattern("let $foo = {$bar :number} {bar {$foo}}")
+ .arguments(Args.of("bar", 4.2))
+ .expected("bar 4.2")
+ .build(),
+ new TestCase.Builder()
+ .pattern("let $foo = {$bar :number minimumFractionDigits=2} {bar {$foo}}")
+ .arguments(Args.of("bar", 4.2))
+ .expected("bar 4.20")
+ .build(),
+ new TestCase.Builder().ignore("Maybe") // Because minimumFractionDigits=foo
+ .pattern("let $foo = {$bar :number minimumFractionDigits=foo} {bar {$foo}}")
+ .arguments(Args.of("bar", 4.2))
+ .expected("bar 4.2")
+ .errors("invalid-type")
+ .build(),
+ new TestCase.Builder().ignore("Maybe. Function specific behavior.")
+ .pattern("let $foo = {$bar :number} {bar {$foo}}")
+ .arguments(Args.of("bar", "foo"))
+ .expected("bar NaN")
+ .build(),
+ new TestCase.Builder()
+ .pattern("let $foo = {$bar} let $bar = {$baz} {bar {$foo}}")
+ .arguments(Args.of("baz", "foo"))
+ // expectedJs: "bar foo"
+ // It is currently undefined if a local variable (like $foo)
+ // can reference a local variable that was not yet defined (like $bar).
+ // That is called hoisting and it is valid in JavaScript or Python.
+ // Not allowing that would prevent circular references.
+ // https://github.com/unicode-org/message-format-wg/issues/292
+ .expected("bar {$bar}")
+ .build(),
+ new TestCase.Builder()
+ .patternJs("match {$foo} when (1) {one} when * {other}")
+ .pattern("match {$foo :select} when (1) {one} when * {other}")
+ .arguments(Args.of("foo", "1"))
+ .expected("one")
+ .build(),
+ new TestCase.Builder()
+ .pattern("match {$foo :plural} when 1 {one} when * {other}")
+ .arguments(Args.of("foo", "1")) // Should this be error? Plural on string?
+ // expectedJs: "one"
+ .expected("other")
+ .build(),
+ new TestCase.Builder()
+ .pattern("match {$foo :select} when (1) {one} when * {other}")
+ .arguments(Args.of("foo", "1"))
+ .expected("one")
+ .build(),
+ new TestCase.Builder()
+ .patternJs("match {$foo} when 1 {one} when * {other}")
+ .pattern("match {$foo :plural} when 1 {one} when * {other}")
+ .arguments(Args.of("foo", 1))
+ .expected("one")
+ .build(),
+ new TestCase.Builder()
+ .pattern("match {$foo :plural} when 1 {one} when * {other}")
+ .arguments(Args.of("foo", 1))
+ .expected("one")
+ .build(),
+ new TestCase.Builder().ignore("not possible to put a null in a map")
+ .pattern("match {$foo} when 1 {one} when * {other}")
+ .arguments(Args.of("foo", null))
+ .expected("other")
+ .build(),
+ new TestCase.Builder()
+ .patternJs("match {$foo} when 1 {one} when * {other}")
+ .pattern("match {$foo :plural} when 1 {one} when * {other}")
+ .expected("other")
+ .errors("missing-var")
+ .build(),
+ new TestCase.Builder()
+ .patternJs("match {$foo} when one {one} when * {other}")
+ .pattern("match {$foo :plural} when one {one} when * {other}")
+ .arguments(Args.of("foo", 1))
+ .expected("one")
+ .build(),
+ new TestCase.Builder()
+ .patternJs("match {$foo} when 1 {=1} when one {one} when * {other}")
+ .pattern("match {$foo :plural} when 1 {=1} when one {one} when * {other}")
+ .arguments(Args.of("foo", 1))
+ .expected("=1")
+ .build(),
+ new TestCase.Builder()
+ .patternJs("match {$foo} when one {one} when 1 {=1} when * {other}")
+ .pattern("match {$foo :plural} when one {one} when 1 {=1} when * {other}")
+ .arguments(Args.of("foo", 1))
+ .expected("one")
+ .build(),
+ new TestCase.Builder()
+ .patternJs("match {$foo} {$bar} when one one {one one} when one * {one other} when * * {other}")
+ .pattern("match {$foo :plural} {$bar :plural} when one one {one one} when one * {one other} when * * {other}")
+ .arguments(Args.of("foo", 1, "bar", 1))
+ .expected("one one")
+ .build(),
+ new TestCase.Builder()
+ .patternJs("match {$foo} {$bar} when one one {one one} when one * {one other} when * * {other}")
+ .pattern("match {$foo :plural} {$bar :plural} when one one {one one} when one * {one other} when * * {other}")
+ .arguments(Args.of("foo", 1, "bar", 2))
+ .expected("one other")
+ .build(),
+ new TestCase.Builder()
+ .patternJs("match {$foo} {$bar} when one one {one one} when one * {one other} when * * {other}")
+ .pattern("match {$foo :plural} {$bar :plural} when one one {one one} when one * {one other} when * * {other}")
+ .arguments(Args.of("foo", 2, "bar", 2))
+ .expected("other")
+ .build(),
+ new TestCase.Builder()
+ .patternJs("let $foo = {$bar} match {$foo} when one {one} when * {other}")
+ .pattern("let $foo = {$bar} match {$foo :plural} when one {one} when * {other}")
+ .arguments(Args.of("bar", 1))
+ .expected("one")
+ .build(),
+ new TestCase.Builder()
+ .patternJs("let $foo = {$bar} match {$foo} when one {one} when * {other}")
+ .pattern("let $foo = {$bar} match {$foo :plural} when one {one} when * {other}")
+ .arguments(Args.of("bar", 2))
+ .expected("other")
+ .build(),
+ new TestCase.Builder()
+ .patternJs("let $bar = {$none} match {$foo} when one {one} when * {{$bar}}")
+ .pattern("let $bar = {$none} match {$foo :plural} when one {one} when * {{$bar}}")
+ .arguments(Args.of("foo", 1))
+ .expected("one")
+ .build(),
+ new TestCase.Builder()
+ .patternJs("let $bar = {$none} match {$foo} when one {one} when * {{$bar}}")
+ .pattern("let $bar = {$none :plural} match {$foo} when one {one} when * {{$bar}}")
+ .arguments(Args.of("foo", 2))
+ .expected("{$bar}")
+ .errors("missing-var")
+ .build(),
+ new TestCase.Builder()
+ .pattern("let bar = {(foo)} {{$bar}}")
+ .expected("{$bar}")
+ .errors("missing-char", "missing-var")
+ .build(),
+ new TestCase.Builder()
+ .pattern("let $bar {(foo)} {{$bar}}")
+ .expected("foo")
+ .errors("missing-char")
+ .build(),
+ new TestCase.Builder()
+ .pattern("let $bar = (foo) {{$bar}}")
+ .expected("{$bar}")
+ .errors("missing-char", "junk-element")
+ .build(),
+ new TestCase.Builder().ignore("no markup support")
+ .pattern("{{+tag}}")
+ .expected("{+tag}")
+ .build(),
+ new TestCase.Builder().ignore("no markup support")
+ .pattern("{{+tag}content}")
+ .expected("{+tag}content")
+ .build(),
+ new TestCase.Builder().ignore("no markup support")
+ .pattern("{{+tag}content{-tag}}")
+ .expected("{+tag}content{-tag}")
+ .build(),
+ new TestCase.Builder().ignore("no markup support")
+ .pattern("{{-tag}content}")
+ .expected("{-tag}content")
+ .build(),
+ new TestCase.Builder().ignore("no markup support")
+ .pattern("{{+tag foo=bar}}")
+ .expected("{+tag foo=bar}")
+ .build(),
+ new TestCase.Builder().ignore("no markup support")
+ .pattern("{{+tag foo=(foo) bar=$bar}}")
+ .arguments(Args.of("bar", "b a r"))
+ .expected("{+tag foo=foo bar=(b a r)}")
+ .build(),
+ new TestCase.Builder()
+ .pattern("{bad {(foo) +markup}}")
+ .expected("bad {+markup}")
+ .errors("extra-content")
+ .build(),
+ new TestCase.Builder()
+ .pattern("{{-tag foo=bar}}")
+ .expected("{-tag}")
+ .errors("extra-content")
+ .build(),
+ new TestCase.Builder()
+ .pattern("no braces")
+ .expected("{no braces}")
+ .errors("parse-error", "junk-element")
+ .build(),
+ new TestCase.Builder()
+ .pattern("no braces {$foo}")
+ .arguments(Args.of("foo", 2))
+ .expected("{no braces {$foo}}")
+ .errors("parse-error", "junk-element")
+ .build(),
+ new TestCase.Builder().ignore("infinite loop!")
+ .pattern("{missing end brace")
+ .expected("missing end brace")
+ .errors("missing-char")
+ .build(),
+ new TestCase.Builder()
+ .pattern("{missing end {$brace")
+ .expected("missing end {$brace}")
+ .errors("missing-char", "missing-char", "missing-var")
+ .build(),
+ new TestCase.Builder()
+ .pattern("{extra} content")
+ .expected("extra")
+ .errors("extra-content")
+ .build(),
+ new TestCase.Builder()
+ .pattern("{empty { }}")
+ .expected("empty ")
+ // errorsJs: ["parse-error", "junk-element"]
+ .build(),
+ new TestCase.Builder()
+ .pattern("{bad {:}}")
+ .expected("bad {:}")
+ .errors("empty-token", "missing-func")
+ .build(),
+ new TestCase.Builder()
+ .pattern("{bad {placeholder}}")
+ .expected("bad {placeholder}")
+ .errors("parse-error", "extra-content", "junk-element")
+ .build(),
+ new TestCase.Builder()
+ .pattern("{no-equal {(42) :number minimumFractionDigits 2}}")
+ .expected( "no-equal 42.00")
+ .errors("missing-char")
+ .build(),
+ new TestCase.Builder()
+ .pattern("{bad {:placeholder option=}}")
+ .expected("bad {:placeholder}")
+ .errors("empty-token", "missing-func")
+ .build(),
+ new TestCase.Builder()
+ .pattern("{bad {:placeholder option value}}")
+ .expected("bad {:placeholder}")
+ .errors("missing-char", "missing-func")
+ .build(),
+ new TestCase.Builder()
+ .pattern("{bad {:placeholder option}}")
+ .expected("bad {:placeholder}")
+ .errors("missing-char", "empty-token", "missing-func")
+ .build(),
+ new TestCase.Builder()
+ .pattern("{bad {$placeholder option}}")
+ .expected("bad {$placeholder}")
+ .errors("extra-content", "extra-content", "missing-var")
+ .build(),
+ new TestCase.Builder()
+ .pattern("{no {$placeholder end}")
+ .expected("no {$placeholder}")
+ .errors("extra-content", "missing-var")
+ .build(),
+ new TestCase.Builder()
+ .pattern("match {} when * {foo}")
+ .expected("foo")
+ .errors("parse-error", "bad-selector", "junk-element")
+ .build(),
+ new TestCase.Builder()
+ .pattern("match {+foo} when * {foo}")
+ .expected("foo")
+ .errors("bad-selector")
+ .build(),
+ new TestCase.Builder()
+ .pattern("match {(foo)} when*{foo}")
+ .expected("foo")
+ .errors("missing-char")
+ .build(),
+ new TestCase.Builder()
+ .pattern("match when * {foo}")
+ .expected("foo")
+ .errors("empty-token")
+ .build(),
+ new TestCase.Builder()
+ .pattern("match {(x)} when * foo")
+ .expected("")
+ .errors("key-mismatch", "missing-char")
+ .build(),
+ new TestCase.Builder()
+ .pattern("match {(x)} when * {foo} extra")
+ .expected("foo")
+ .errors("extra-content")
+ .build(),
+ new TestCase.Builder()
+ .pattern("match (x) when * {foo}")
+ .expected("")
+ .errors("empty-token", "extra-content")
+ .build(),
+ new TestCase.Builder()
+ .pattern("match {$foo} when * * {foo}")
+ .expected("foo")
+ .errors("key-mismatch", "missing-var")
+ .build(),
+ new TestCase.Builder()
+ .pattern("match {$foo} {$bar} when * {foo}")
+ .expected("foo")
+ .errors("key-mismatch", "missing-var", "missing-var")
+ .build()
+ };
+
+ @Test
+ public void test() {
+ int ignoreCount = 0;
+ for (TestCase testCase : TEST_CASES) {
+ if (testCase.ignore)
+ ignoreCount++;
+ TestUtils.runTestCase(testCase);
+ }
+ System.out.printf("Executed %d test cases out of %d, skipped %d%n",
+ TEST_CASES.length - ignoreCount, TEST_CASES.length, ignoreCount);
+ }
+}
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/message2/MessageFormat2Test.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/message2/MessageFormat2Test.java
new file mode 100644
index 000000000000..b16acb63088e
--- /dev/null
+++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/message2/MessageFormat2Test.java
@@ -0,0 +1,546 @@
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package com.ibm.icu.dev.test.message2;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import com.ibm.icu.dev.test.TestFmwk;
+import com.ibm.icu.message2.FormattedPlaceholder;
+import com.ibm.icu.message2.Formatter;
+import com.ibm.icu.message2.FormatterFactory;
+import com.ibm.icu.message2.MessageFormatter;
+import com.ibm.icu.message2.Mf2FunctionRegistry;
+import com.ibm.icu.number.FormattedNumber;
+import com.ibm.icu.number.LocalizedNumberFormatter;
+import com.ibm.icu.number.NumberFormatter;
+import com.ibm.icu.util.BuddhistCalendar;
+import com.ibm.icu.util.Calendar;
+import com.ibm.icu.util.GregorianCalendar;
+import com.ibm.icu.util.Measure;
+import com.ibm.icu.util.MeasureUnit;
+
+/**
+ * Tests migrated from {@link com.ibm.icu.text.MessageFormat}, to show what they look like and that they work.
+ *
+ * It does not include all the tests for edge cases and error handling, only the ones that show real functionality.
+ */
+@RunWith(JUnit4.class)
+@SuppressWarnings("javadoc")
+public class MessageFormat2Test extends TestFmwk {
+
+ @Test
+ public void test() {
+ MessageFormatter mf2 = MessageFormatter.builder()
+ .setPattern("{Hello World!}").build();
+ assertEquals("simple message",
+ "Hello World!",
+ mf2.formatToString(Args.NONE));
+ }
+
+ @Test
+ public void testDateFormat() {
+ Date expiration = new Date(2022 - 1900, java.util.Calendar.OCTOBER, 27);
+ MessageFormatter mf2 = MessageFormatter.builder()
+ .setPattern("{Your card expires on {$exp :datetime skeleton=yMMMdE}!}")
+ .build();
+ assertEquals("date format",
+ "Your card expires on Thu, Oct 27, 2022!",
+ mf2.formatToString(Args.of("exp", expiration)));
+
+ mf2 = MessageFormatter.builder()
+ .setPattern("{Your card expires on {$exp :datetime datestyle=full}!}")
+ .build();
+ assertEquals("date format",
+ "Your card expires on Thursday, October 27, 2022!",
+ mf2.formatToString(Args.of("exp", expiration)));
+ mf2 = MessageFormatter.builder()
+ .setPattern("{Your card expires on {$exp :datetime datestyle=long}!}")
+ .build();
+ assertEquals("date format",
+ "Your card expires on October 27, 2022!",
+ mf2.formatToString(Args.of("exp", expiration)));
+ mf2 = MessageFormatter.builder()
+ .setPattern("{Your card expires on {$exp :datetime datestyle=medium}!}")
+ .build();
+ assertEquals("date format",
+ "Your card expires on Oct 27, 2022!",
+ mf2.formatToString(Args.of("exp", expiration)));
+ mf2 = MessageFormatter.builder()
+ .setPattern("{Your card expires on {$exp :datetime datestyle=short}!}")
+ .build();
+ assertEquals("date format",
+ "Your card expires on 10/27/22!",
+ mf2.formatToString(Args.of("exp", expiration)));
+
+ Calendar cal = new GregorianCalendar(2022, Calendar.OCTOBER, 27);
+ mf2 = MessageFormatter.builder()
+ .setPattern("{Your card expires on {$exp :datetime skeleton=yMMMdE}!}")
+ .build();
+ assertEquals("date format",
+ "Your card expires on Thu, Oct 27, 2022!",
+ mf2.formatToString(Args.of("exp", cal)));
+
+ // Implied function based on type of the object to format
+ mf2 = MessageFormatter.builder()
+ .setPattern("{Your card expires on {$exp}!}")
+ .build();
+ assertEquals("date format",
+ "Your card expires on 10/27/22, 12:00\u202FAM!",
+ mf2.formatToString(Args.of("exp", expiration)));
+ assertEquals("date format",
+ "Your card expires on 10/27/22, 12:00\u202FAM!",
+ mf2.formatToString(Args.of("exp", cal)));
+
+ // Implied function based on type of the object to format
+ // This is a calendar that is not explicitly added to the registry.
+ // But we test to see if it works because it extends Calendar, which is registered.
+ BuddhistCalendar calNotRegistered = new BuddhistCalendar(2022, Calendar.OCTOBER, 27);
+ mf2 = MessageFormatter.builder()
+ .setPattern("{Your card expires on {$exp :datetime skeleton=yMMMdE}!}")
+ .build();
+ assertEquals("date format",
+ "Your card expires on Wed, Oct 27, 1479!",
+ mf2.formatToString(Args.of("exp", calNotRegistered)));
+
+ mf2 = MessageFormatter.builder()
+ .setPattern("{Your card expires on {$exp :datetime skeleton=yMMMdE}!}")
+ .build();
+ assertEquals("date format",
+ "Your card expires on Wed, Oct 27, 1479!",
+ mf2.formatToString(Args.of("exp", calNotRegistered)));
+ }
+
+ @Test
+ public void testPlural() {
+ String message = ""
+ + "match {$count :plural}\n"
+ + " when 1 {You have one notification.}\n"
+ + " when * {You have {$count} notifications.}\n";
+
+ MessageFormatter mf2 = MessageFormatter.builder()
+ .setPattern(message)
+ .build();
+ assertEquals("plural",
+ "You have one notification.",
+ mf2.formatToString(Args.of("count", 1)));
+ assertEquals("plural",
+ "You have 42 notifications.",
+ mf2.formatToString(Args.of("count", 42)));
+ }
+
+ @Test
+ public void testPluralOrdinal() {
+ String message = ""
+ + "match {$place :selectordinal}\n"
+ + " when 1 {You got the gold medal}\n"
+ + " when 2 {You got the silver medal}\n"
+ + " when 3 {You got the bronze medal}\n"
+ + " when one {You got in the {$place}st place}\n"
+ + " when two {You got in the {$place}nd place}\n"
+ + " when few {You got in the {$place}rd place}\n"
+ + " when * {You got in the {$place}th place}\n"
+ ;
+
+ MessageFormatter mf2 = MessageFormatter.builder()
+ .setPattern(message)
+ .build();
+ assertEquals("selectordinal",
+ "You got the gold medal",
+ mf2.formatToString(Args.of("place", 1)));
+ assertEquals("selectordinal",
+ "You got the silver medal",
+ mf2.formatToString(Args.of("place", 2)));
+ assertEquals("selectordinal",
+ "You got the bronze medal",
+ mf2.formatToString(Args.of("place", 3)));
+ assertEquals("selectordinal",
+ "You got in the 21st place",
+ mf2.formatToString(Args.of("place", 21)));
+ assertEquals("selectordinal",
+ "You got in the 32nd place",
+ mf2.formatToString(Args.of("place", 32)));
+ assertEquals("selectordinal",
+ "You got in the 23rd place",
+ mf2.formatToString(Args.of("place", 23)));
+ assertEquals("selectordinal",
+ "You got in the 15th place",
+ mf2.formatToString(Args.of("place", 15)));
+ }
+
+ static class TemperatureFormatterFactory implements FormatterFactory {
+ int constructCount = 0;
+ int formatCount = 0;
+ int fFormatterCount = 0;
+ int cFormatterCount = 0;
+
+ @Override
+ public Formatter createFormatter(Locale locale, Map fixedOptions) {
+ // Check that the formatter can only see the fixed options
+ Assert.assertTrue(fixedOptions.containsKey("skeleton"));
+ Assert.assertFalse(fixedOptions.containsKey("unit"));
+
+ Object valSkeleton = fixedOptions.get("skeleton");
+ LocalizedNumberFormatter nf = valSkeleton != null
+ ? NumberFormatter.forSkeleton(valSkeleton.toString()).locale(locale)
+ : NumberFormatter.withLocale(locale);
+
+ return new TemperatureFormatterImpl(nf, this);
+ }
+
+ static private class TemperatureFormatterImpl implements Formatter {
+ private final TemperatureFormatterFactory formatterFactory;
+ private final LocalizedNumberFormatter nf;
+ private final Map cachedFormatters =
+ new HashMap<>();
+
+ TemperatureFormatterImpl(LocalizedNumberFormatter nf, TemperatureFormatterFactory formatterFactory) {
+ this.nf = nf;
+ this.formatterFactory = formatterFactory;
+ this.formatterFactory.constructCount++;
+ }
+
+ @Override
+ public String formatToString(Object toFormat, Map variableOptions) {
+ return this.format(toFormat, variableOptions).toString();
+ }
+
+ @Override
+ public FormattedPlaceholder format(Object toFormat, Map variableOptions) {
+ // Check that the formatter can only see the variable options
+ Assert.assertFalse(variableOptions.containsKey("skeleton"));
+ Assert.assertTrue(variableOptions.containsKey("unit"));
+ this.formatterFactory.formatCount++;
+
+ String unit = variableOptions.get("unit").toString();
+ LocalizedNumberFormatter realNf = cachedFormatters.get(unit);
+ if (realNf == null) {
+ switch (variableOptions.get("unit").toString()) {
+ case "C":
+ formatterFactory.cFormatterCount++;
+ realNf = nf.unit(MeasureUnit.CELSIUS);
+ break;
+ case "F":
+ formatterFactory.fFormatterCount++;
+ realNf = nf.unit(MeasureUnit.FAHRENHEIT);
+ break;
+ default:
+ realNf = nf;
+ break;
+ }
+ cachedFormatters.put(unit, realNf);
+ }
+
+ FormattedNumber result;
+ if (toFormat instanceof Double) {
+ result = realNf.format((double) toFormat);
+ } else if (toFormat instanceof Long) {
+ result = realNf.format((Long) toFormat);
+ } else if (toFormat instanceof Number) {
+ result = realNf.format((Number) toFormat);
+ } else if (toFormat instanceof Measure) {
+ result = realNf.format((Measure) toFormat);
+ } else {
+ result = null;
+ }
+ return new FormattedPlaceholder(toFormat, result);
+ }
+ }
+ }
+
+ @Test
+ public void testFormatterIsCreatedOnce() {
+ TemperatureFormatterFactory counter = new TemperatureFormatterFactory();
+ Mf2FunctionRegistry registry = Mf2FunctionRegistry.builder()
+ .setFormatter("temp", counter)
+ .build();
+ String message = "{Testing {$count :temp unit=$unit skeleton=(.00/w)}.}";
+ MessageFormatter mf2 = MessageFormatter.builder()
+ .setFunctionRegistry(registry)
+ .setPattern(message)
+ .build();
+
+ final int maxCount = 10;
+ for (int count = 0; count < maxCount; count++) {
+ assertEquals("cached formatter",
+ "Testing " + count + "°C.",
+ mf2.formatToString(Args.of("count", count, "unit", "C")));
+ assertEquals("cached formatter",
+ "Testing " + count + "°F.",
+ mf2.formatToString(Args.of("count", count, "unit", "F")));
+ }
+
+ // Check that the constructor was only called once,
+ // and the formatter as many times as the public call to format.
+ assertEquals("cached formatter", 1, counter.constructCount);
+ assertEquals("cached formatter", maxCount * 2, counter.formatCount);
+ assertEquals("cached formatter", 1, counter.fFormatterCount);
+ assertEquals("cached formatter", 1, counter.cFormatterCount);
+
+ // Check that the skeleton is respected
+ assertEquals("cached formatter",
+ "Testing 12°C.",
+ mf2.formatToString(Args.of("count", 12, "unit", "C")));
+ assertEquals("cached formatter",
+ "Testing 12.50°F.",
+ mf2.formatToString(Args.of("count", 12.5, "unit", "F")));
+ assertEquals("cached formatter",
+ "Testing 12.54°C.",
+ mf2.formatToString(Args.of("count", 12.54, "unit", "C")));
+ assertEquals("cached formatter",
+ "Testing 12.54°F.",
+ mf2.formatToString(Args.of("count", 12.54321, "unit", "F")));
+
+ message = "{Testing {$count :temp unit=$unit skeleton=(.0/w)}.}";
+ mf2 = MessageFormatter.builder()
+ .setFunctionRegistry(registry)
+ .setPattern(message)
+ .build();
+ // Check that the skeleton is respected
+ assertEquals("cached formatter",
+ "Testing 12°C.",
+ mf2.formatToString(Args.of("count", 12, "unit", "C")));
+ assertEquals("cached formatter",
+ "Testing 12.5°F.",
+ mf2.formatToString(Args.of("count", 12.5, "unit", "F")));
+ assertEquals("cached formatter",
+ "Testing 12.5°C.",
+ mf2.formatToString(Args.of("count", 12.54, "unit", "C")));
+ assertEquals("cached formatter",
+ "Testing 12.5°F.",
+ mf2.formatToString(Args.of("count", 12.54321, "unit", "F")));
+ }
+
+ @Test
+ public void testPluralWithOffset() {
+ String message = ""
+ + "match {$count :plural offset=2}\n"
+ + " when 1 {Anna}\n"
+ + " when 2 {Anna and Bob}\n"
+ + " when one {Anna, Bob, and {$count :number offset=2} other guest}\n"
+ + " when * {Anna, Bob, and {$count :number offset=2} other guests}\n";
+ MessageFormatter mf2 = MessageFormatter.builder()
+ .setPattern(message)
+ .build();
+ assertEquals("plural with offset",
+ "Anna",
+ mf2.formatToString(Args.of("count", 1)));
+ assertEquals("plural with offset",
+ "Anna and Bob",
+ mf2.formatToString(Args.of("count", 2)));
+ assertEquals("plural with offset",
+ "Anna, Bob, and 1 other guest",
+ mf2.formatToString(Args.of("count", 3)));
+ assertEquals("plural with offset",
+ "Anna, Bob, and 2 other guests",
+ mf2.formatToString(Args.of("count", 4)));
+ assertEquals("plural with offset",
+ "Anna, Bob, and 10 other guests",
+ mf2.formatToString(Args.of("count", 12)));
+ }
+
+ @Test
+ public void testPluralWithOffsetAndLocalVar() {
+ String message = ""
+ + "let $foo = {$count :number offset=2}"
+ + "match {$foo :plural}\n" // should "inherit" the offset
+ + " when 1 {Anna}\n"
+ + " when 2 {Anna and Bob}\n"
+ + " when one {Anna, Bob, and {$foo} other guest}\n"
+ + " when * {Anna, Bob, and {$foo} other guests}\n";
+ MessageFormatter mf2 = MessageFormatter.builder()
+ .setPattern(message)
+ .build();
+ assertEquals("plural with offset",
+ "Anna",
+ mf2.formatToString(Args.of("count", 1)));
+ assertEquals("plural with offset",
+ "Anna and Bob",
+ mf2.formatToString(Args.of("count", 2)));
+ assertEquals("plural with offset",
+ "Anna, Bob, and 1 other guest",
+ mf2.formatToString(Args.of("count", 3)));
+ assertEquals("plural with offset",
+ "Anna, Bob, and 2 other guests",
+ mf2.formatToString(Args.of("count", 4)));
+ assertEquals("plural with offset",
+ "Anna, Bob, and 10 other guests",
+ mf2.formatToString(Args.of("count", 12)));
+ }
+
+ @Test
+ public void testPluralWithOffsetAndLocalVar2() {
+ String message = ""
+ + "let $foo = {$amount :number skeleton=(.00/w)}\n"
+ + "match {$foo :plural}\n" // should "inherit" the offset
+ + " when 1 {Last dollar}\n"
+ + " when one {{$foo} dollar}\n"
+ + " when * {{$foo} dollars}\n";
+ MessageFormatter mf2 = MessageFormatter.builder()
+ .setPattern(message)
+ .build();
+ assertEquals("plural with offset",
+ "Last dollar",
+ mf2.formatToString(Args.of("amount", 1)));
+ assertEquals("plural with offset",
+ "2 dollars",
+ mf2.formatToString(Args.of("amount", 2)));
+ assertEquals("plural with offset",
+ "3 dollars",
+ mf2.formatToString(Args.of("amount", 3)));
+ }
+
+ @Test
+ public void testLoopOnLocalVars() {
+ String message = ""
+ + "let $foo = {$baz :number}\n"
+ + "let $bar = {$foo}\n"
+ + "let $baz = {$bar}\n"
+ + "{The message uses {$baz} and works}\n";
+ MessageFormatter mf2 = MessageFormatter.builder()
+ .setPattern(message)
+ .build();
+ assertEquals("test local vars loop",
+ "The message uses {$bar} and works",
+ mf2.formatToString(Args.of("amount", 1)));
+ }
+
+ @Test
+ public void testVariableOptionsInSelector() {
+ String messageVar = ""
+ + "match {$count :plural offset=$delta}\n"
+ + " when 1 {A}\n"
+ + " when 2 {A and B}\n"
+ + " when one {A, B, and {$count :number offset=$delta} more character}\n"
+ + " when * {A, B, and {$count :number offset=$delta} more characters}\n";
+ MessageFormatter mfVar = MessageFormatter.builder()
+ .setPattern(messageVar)
+ .build();
+ assertEquals("test local vars loop", "A",
+ mfVar.formatToString(Args.of("count", 1, "delta", 2)));
+ assertEquals("test local vars loop", "A and B",
+ mfVar.formatToString(Args.of("count", 2, "delta", 2)));
+ assertEquals("test local vars loop", "A, B, and 1 more character",
+ mfVar.formatToString(Args.of("count", 3, "delta", 2)));
+ assertEquals("test local vars loop", "A, B, and 5 more characters",
+ mfVar.formatToString(Args.of("count", 7, "delta", 2)));
+
+ String messageVar2 = ""
+ + "match {$count :plural offset=$delta}\n"
+ + " when 1 {Exactly 1}\n"
+ + " when 2 {Exactly 2}\n"
+ + " when * {Count = {$count :number offset=$delta} and delta={$delta}.}\n";
+ MessageFormatter mfVar2 = MessageFormatter.builder()
+ .setPattern(messageVar2)
+ .build();
+ assertEquals("test local vars loop", "Exactly 1",
+ mfVar2.formatToString(Args.of("count", 1, "delta", 0)));
+ assertEquals("test local vars loop", "Exactly 1",
+ mfVar2.formatToString(Args.of("count", 1, "delta", 1)));
+ assertEquals("test local vars loop", "Exactly 1",
+ mfVar2.formatToString(Args.of("count", 1, "delta", 2)));
+
+ assertEquals("test local vars loop", "Exactly 2",
+ mfVar2.formatToString(Args.of("count", 2, "delta", 0)));
+ assertEquals("test local vars loop", "Exactly 2",
+ mfVar2.formatToString(Args.of("count", 2, "delta", 1)));
+ assertEquals("test local vars loop", "Exactly 2",
+ mfVar2.formatToString(Args.of("count", 2, "delta", 2)));
+
+ assertEquals("test local vars loop", "Count = 3 and delta=0.",
+ mfVar2.formatToString(Args.of("count", 3, "delta", 0)));
+ assertEquals("test local vars loop", "Count = 2 and delta=1.",
+ mfVar2.formatToString(Args.of("count", 3, "delta", 1)));
+ assertEquals("test local vars loop", "Count = 1 and delta=2.",
+ mfVar2.formatToString(Args.of("count", 3, "delta", 2)));
+
+ assertEquals("test local vars loop", "Count = 23 and delta=0.",
+ mfVar2.formatToString(Args.of("count", 23, "delta", 0)));
+ assertEquals("test local vars loop", "Count = 22 and delta=1.",
+ mfVar2.formatToString(Args.of("count", 23, "delta", 1)));
+ assertEquals("test local vars loop", "Count = 21 and delta=2.",
+ mfVar2.formatToString(Args.of("count", 23, "delta", 2)));
+ }
+
+ @Test
+ public void testVariableOptionsInSelectorWithLocalVar() {
+ String messageFix = ""
+ + "let $offCount = {$count :number offset=2}"
+ + "match {$offCount :plural}\n"
+ + " when 1 {A}\n"
+ + " when 2 {A and B}\n"
+ + " when one {A, B, and {$offCount} more character}\n"
+ + " when * {A, B, and {$offCount} more characters}\n";
+ MessageFormatter mfFix = MessageFormatter.builder()
+ .setPattern(messageFix)
+ .build();
+ assertEquals("test local vars loop", "A", mfFix.formatToString(Args.of("count", 1)));
+ assertEquals("test local vars loop", "A and B", mfFix.formatToString(Args.of("count", 2)));
+ assertEquals("test local vars loop", "A, B, and 1 more character", mfFix.formatToString(Args.of("count", 3)));
+ assertEquals("test local vars loop", "A, B, and 5 more characters", mfFix.formatToString(Args.of("count", 7)));
+
+ String messageVar = ""
+ + "let $offCount = {$count :number offset=$delta}"
+ + "match {$offCount :plural}\n"
+ + " when 1 {A}\n"
+ + " when 2 {A and B}\n"
+ + " when one {A, B, and {$offCount} more character}\n"
+ + " when * {A, B, and {$offCount} more characters}\n";
+ MessageFormatter mfVar = MessageFormatter.builder()
+ .setPattern(messageVar)
+ .build();
+ assertEquals("test local vars loop", "A",
+ mfVar.formatToString(Args.of("count", 1, "delta", 2)));
+ assertEquals("test local vars loop", "A and B",
+ mfVar.formatToString(Args.of("count", 2, "delta", 2)));
+ assertEquals("test local vars loop", "A, B, and 1 more character",
+ mfVar.formatToString(Args.of("count", 3, "delta", 2)));
+ assertEquals("test local vars loop", "A, B, and 5 more characters",
+ mfVar.formatToString(Args.of("count", 7, "delta", 2)));
+
+ String messageVar2 = ""
+ + "let $offCount = {$count :number offset=$delta}"
+ + "match {$offCount :plural}\n"
+ + " when 1 {Exactly 1}\n"
+ + " when 2 {Exactly 2}\n"
+ + " when * {Count = {$count}, OffCount = {$offCount}, and delta={$delta}.}\n";
+ MessageFormatter mfVar2 = MessageFormatter.builder()
+ .setPattern(messageVar2)
+ .build();
+ assertEquals("test local vars loop", "Exactly 1",
+ mfVar2.formatToString(Args.of("count", 1, "delta", 0)));
+ assertEquals("test local vars loop", "Exactly 1",
+ mfVar2.formatToString(Args.of("count", 1, "delta", 1)));
+ assertEquals("test local vars loop", "Exactly 1",
+ mfVar2.formatToString(Args.of("count", 1, "delta", 2)));
+
+ assertEquals("test local vars loop", "Exactly 2",
+ mfVar2.formatToString(Args.of("count", 2, "delta", 0)));
+ assertEquals("test local vars loop", "Exactly 2",
+ mfVar2.formatToString(Args.of("count", 2, "delta", 1)));
+ assertEquals("test local vars loop", "Exactly 2",
+ mfVar2.formatToString(Args.of("count", 2, "delta", 2)));
+
+ assertEquals("test local vars loop", "Count = 3, OffCount = 3, and delta=0.",
+ mfVar2.formatToString(Args.of("count", 3, "delta", 0)));
+ assertEquals("test local vars loop", "Count = 3, OffCount = 2, and delta=1.",
+ mfVar2.formatToString(Args.of("count", 3, "delta", 1)));
+ assertEquals("test local vars loop", "Count = 3, OffCount = 1, and delta=2.",
+ mfVar2.formatToString(Args.of("count", 3, "delta", 2)));
+
+ assertEquals("test local vars loop", "Count = 23, OffCount = 23, and delta=0.",
+ mfVar2.formatToString(Args.of("count", 23, "delta", 0)));
+ assertEquals("test local vars loop", "Count = 23, OffCount = 22, and delta=1.",
+ mfVar2.formatToString(Args.of("count", 23, "delta", 1)));
+ assertEquals("test local vars loop", "Count = 23, OffCount = 21, and delta=2.",
+ mfVar2.formatToString(Args.of("count", 23, "delta", 2)));
+ }
+}
diff --git a/icu4j/main/tests/core/src/com/ibm/icu/dev/test/message2/Mf2FeaturesTest.java b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/message2/Mf2FeaturesTest.java
new file mode 100644
index 000000000000..b12bda11214b
--- /dev/null
+++ b/icu4j/main/tests/core/src/com/ibm/icu/dev/test/message2/Mf2FeaturesTest.java
@@ -0,0 +1,465 @@
+// © 2022 and later: Unicode, Inc. and others.
+// License & terms of use: http://www.unicode.org/copyright.html
+
+package com.ibm.icu.dev.test.message2;
+
+import java.util.Date;
+import java.util.Locale;
+import java.util.Map;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+
+import com.ibm.icu.dev.test.TestFmwk;
+import com.ibm.icu.math.BigDecimal;
+import com.ibm.icu.util.Currency;
+import com.ibm.icu.util.CurrencyAmount;
+
+/**
+ * Trying to show off most of the features in one place.
+ *
+ * It covers the examples in the
+ * spec document,
+ * except for the custom formatters ones, which are too verbose and were moved to separate test classes.
+ *