mirror of
https://github.com/unicode-org/icu.git
synced 2025-04-06 05:55:35 +00:00
ICU-22124 Adding a tech preview implementation of MessageFormat v2
See #2170
This commit is contained in:
parent
06259cc6c3
commit
db59034793
34 changed files with 6558 additions and 0 deletions
|
@ -1361,6 +1361,7 @@
|
|||
<packageset dir="${icu4j.core.dir}/src">
|
||||
<include name="com/ibm/icu/lang/**"/>
|
||||
<include name="com/ibm/icu/math/**"/>
|
||||
<include name="com/ibm/icu/message2/**"/>
|
||||
<include name="com/ibm/icu/number/**"/>
|
||||
<include name="com/ibm/icu/text/**"/>
|
||||
<include name="com/ibm/icu/util/**"/>
|
||||
|
@ -1396,6 +1397,7 @@
|
|||
<packageset dir="${icu4j.core.dir}/src">
|
||||
<include name="com/ibm/icu/lang/**"/>
|
||||
<include name="com/ibm/icu/math/**"/>
|
||||
<include name="com/ibm/icu/message2/**"/>
|
||||
<include name="com/ibm/icu/number/**"/>
|
||||
<include name="com/ibm/icu/text/**"/>
|
||||
<include name="com/ibm/icu/util/**"/>
|
||||
|
@ -1442,6 +1444,7 @@
|
|||
<packageset dir="${icu4j.core.dir}/src">
|
||||
<include name="com/ibm/icu/lang/**"/>
|
||||
<include name="com/ibm/icu/math/**"/>
|
||||
<include name="com/ibm/icu/message2/**"/>
|
||||
<include name="com/ibm/icu/number/**"/>
|
||||
<include name="com/ibm/icu/text/**"/>
|
||||
<include name="com/ibm/icu/util/**"/>
|
||||
|
@ -1646,6 +1649,7 @@
|
|||
<packageset dir="${icu4j.core.dir}/src">
|
||||
<include name="com/ibm/icu/lang/**"/>
|
||||
<include name="com/ibm/icu/math/**"/>
|
||||
<include name="com/ibm/icu/message2/**"/>
|
||||
<include name="com/ibm/icu/number/**"/>
|
||||
<include name="com/ibm/icu/text/**"/>
|
||||
<include name="com/ibm/icu/util/**"/>
|
||||
|
@ -1668,6 +1672,7 @@
|
|||
<packageset dir="${icu4j.core.dir}/src">
|
||||
<include name="com/ibm/icu/lang/**"/>
|
||||
<include name="com/ibm/icu/math/**"/>
|
||||
<include name="com/ibm/icu/message2/**"/>
|
||||
<include name="com/ibm/icu/number/**"/>
|
||||
<include name="com/ibm/icu/text/**"/>
|
||||
<include name="com/ibm/icu/util/**"/>
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
// © 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 java.util.Objects;
|
||||
|
||||
import com.ibm.icu.impl.locale.AsciiUtil;
|
||||
import com.ibm.icu.text.DateFormat;
|
||||
import com.ibm.icu.text.SimpleDateFormat;
|
||||
|
||||
/**
|
||||
* Creates a {@link Formatter} doing formatting of date / time, similar to
|
||||
* <code>{exp, date}</code> and <code>{exp, time}</code> in {@link com.ibm.icu.text.MessageFormat}.
|
||||
*/
|
||||
class DateTimeFormatterFactory implements FormatterFactory {
|
||||
|
||||
private static int stringToStyle(String option) {
|
||||
switch (AsciiUtil.toUpperString(option)) {
|
||||
case "FULL": return DateFormat.FULL;
|
||||
case "LONG": return DateFormat.LONG;
|
||||
case "MEDIUM": return DateFormat.MEDIUM;
|
||||
case "SHORT": return DateFormat.SHORT;
|
||||
case "": // intentional fall-through
|
||||
case "DEFAULT": return DateFormat.DEFAULT;
|
||||
default: throw new IllegalArgumentException("Invalid datetime style: " + option);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @throws IllegalArgumentException when something goes wrong
|
||||
* (for example conflicting options, invalid option values, etc.)
|
||||
*/
|
||||
@Override
|
||||
public Formatter createFormatter(Locale locale, Map<String, Object> fixedOptions) {
|
||||
DateFormat df;
|
||||
|
||||
// TODO: how to handle conflicts. What if we have both skeleton and style, or pattern?
|
||||
Object opt = fixedOptions.get("skeleton");
|
||||
if (opt != null) {
|
||||
String skeleton = Objects.toString(opt);
|
||||
df = DateFormat.getInstanceForSkeleton(skeleton, locale);
|
||||
return new DateTimeFormatter(df);
|
||||
}
|
||||
|
||||
opt = fixedOptions.get("pattern");
|
||||
if (opt != null) {
|
||||
String pattern = Objects.toString(opt);
|
||||
SimpleDateFormat sf = new SimpleDateFormat(pattern, locale);
|
||||
return new DateTimeFormatter(sf);
|
||||
}
|
||||
|
||||
int dateStyle = DateFormat.NONE;
|
||||
opt = fixedOptions.get("datestyle");
|
||||
if (opt != null) {
|
||||
dateStyle = stringToStyle(Objects.toString(opt, ""));
|
||||
}
|
||||
|
||||
int timeStyle = DateFormat.NONE;
|
||||
opt = fixedOptions.get("timestyle");
|
||||
if (opt != null) {
|
||||
timeStyle = stringToStyle(Objects.toString(opt, ""));
|
||||
}
|
||||
|
||||
if (dateStyle == DateFormat.NONE && timeStyle == DateFormat.NONE) {
|
||||
// Match the MessageFormat behavior
|
||||
dateStyle = DateFormat.SHORT;
|
||||
timeStyle = DateFormat.SHORT;
|
||||
}
|
||||
df = DateFormat.getDateTimeInstance(dateStyle, timeStyle, locale);
|
||||
|
||||
return new DateTimeFormatter(df);
|
||||
}
|
||||
|
||||
private static class DateTimeFormatter implements Formatter {
|
||||
private final DateFormat icuFormatter;
|
||||
|
||||
private DateTimeFormatter(DateFormat df) {
|
||||
this.icuFormatter = df;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public FormattedPlaceholder format(Object toFormat, Map<String, Object> variableOptions) {
|
||||
// TODO: use a special type to indicate function without input argument.
|
||||
if (toFormat == null) {
|
||||
throw new IllegalArgumentException("The date to format can't be null");
|
||||
}
|
||||
String result = icuFormatter.format(toFormat);
|
||||
return new FormattedPlaceholder(toFormat, new PlainStringFormattedValue(result));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public String formatToString(Object toFormat, Map<String, Object> variableOptions) {
|
||||
return format(toFormat, variableOptions).toString();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,120 @@
|
|||
// © 2022 and later: Unicode, Inc. and others.
|
||||
// License & terms of use: http://www.unicode.org/copyright.html
|
||||
|
||||
package com.ibm.icu.message2;
|
||||
|
||||
import java.text.AttributedCharacterIterator;
|
||||
|
||||
import com.ibm.icu.text.ConstrainedFieldPosition;
|
||||
import com.ibm.icu.text.FormattedValue;
|
||||
|
||||
/**
|
||||
* Not yet implemented: The result of a message formatting operation.
|
||||
*
|
||||
* <p>This contains information about where the various fields and placeholders
|
||||
* ended up in the final result.</p>
|
||||
* <p>This class allows the result to be exported in several data types,
|
||||
* including a {@link String}, {@link AttributedCharacterIterator}, more (TBD).</p>
|
||||
*
|
||||
* @internal ICU 72 technology preview
|
||||
* @deprecated This API is for ICU internal use only.
|
||||
*/
|
||||
@Deprecated
|
||||
public class FormattedMessage implements FormattedValue {
|
||||
|
||||
/**
|
||||
* Not yet implemented.
|
||||
*
|
||||
* @internal ICU 72 technology preview
|
||||
* @deprecated This API is for ICU internal use only.
|
||||
*/
|
||||
@Deprecated
|
||||
public FormattedMessage() {
|
||||
throw new RuntimeException("Not yet implemented.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Not yet implemented.
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @internal ICU 72 technology preview
|
||||
* @deprecated This API is for ICU internal use only.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
public int length() {
|
||||
throw new RuntimeException("Not yet implemented.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Not yet implemented.
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @internal ICU 72 technology preview
|
||||
* @deprecated This API is for ICU internal use only.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
public char charAt(int index) {
|
||||
throw new RuntimeException("Not yet implemented.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Not yet implemented.
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @internal ICU 72 technology preview
|
||||
* @deprecated This API is for ICU internal use only.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
public CharSequence subSequence(int start, int end) {
|
||||
throw new RuntimeException("Not yet implemented.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Not yet implemented.
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @internal ICU 72 technology preview
|
||||
* @deprecated This API is for ICU internal use only.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
public <A extends Appendable> A appendTo(A appendable) {
|
||||
throw new RuntimeException("Not yet implemented.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Not yet implemented.
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @internal ICU 72 technology preview
|
||||
* @deprecated This API is for ICU internal use only.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
public boolean nextPosition(ConstrainedFieldPosition cfpos) {
|
||||
throw new RuntimeException("Not yet implemented.");
|
||||
}
|
||||
|
||||
/**
|
||||
* Not yet implemented.
|
||||
*
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @internal ICU 72 technology preview
|
||||
* @deprecated This API is for ICU internal use only.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
public AttributedCharacterIterator toCharacterIterator() {
|
||||
throw new RuntimeException("Not yet implemented.");
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,79 @@
|
|||
// © 2022 and later: Unicode, Inc. and others.
|
||||
// License & terms of use: http://www.unicode.org/copyright.html
|
||||
|
||||
package com.ibm.icu.message2;
|
||||
|
||||
import com.ibm.icu.text.FormattedValue;
|
||||
|
||||
/**
|
||||
* An immutable, richer formatting result, encapsulating a {@link FormattedValue},
|
||||
* the original value to format, and we are considering adding some more info.
|
||||
* Very preliminary.
|
||||
*
|
||||
* @internal ICU 72 technology preview
|
||||
* @deprecated This API is for ICU internal use only.
|
||||
*/
|
||||
@Deprecated
|
||||
public class FormattedPlaceholder {
|
||||
private final FormattedValue formattedValue;
|
||||
private final Object inputValue;
|
||||
|
||||
/**
|
||||
* Constructor creating the {@code FormattedPlaceholder}.
|
||||
*
|
||||
* @param inputValue the original value to be formatted.
|
||||
* @param formattedValue the result of formatting the placeholder.
|
||||
*
|
||||
* @internal ICU 72 technology preview
|
||||
* @deprecated This API is for ICU internal use only.
|
||||
*/
|
||||
@Deprecated
|
||||
public FormattedPlaceholder(Object inputValue, FormattedValue formattedValue) {
|
||||
if (formattedValue == null) {
|
||||
throw new IllegalAccessError("Should not try to wrap a null formatted value");
|
||||
}
|
||||
this.inputValue = inputValue;
|
||||
this.formattedValue = formattedValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the original input value that was formatted.
|
||||
*
|
||||
* @return the original value to be formatted.
|
||||
*
|
||||
* @internal ICU 72 technology preview
|
||||
* @deprecated This API is for ICU internal use only.
|
||||
*/
|
||||
@Deprecated
|
||||
public Object getInput() {
|
||||
return inputValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the formatted value.
|
||||
*
|
||||
* @return the result of formatting the placeholder.
|
||||
*
|
||||
* @internal ICU 72 technology preview
|
||||
* @deprecated This API is for ICU internal use only.
|
||||
*/
|
||||
@Deprecated
|
||||
public FormattedValue getFormattedValue() {
|
||||
return formattedValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string representation of the object.
|
||||
* It can be null, which is unusual, and we plan to change that.
|
||||
*
|
||||
* @return a string representation of the object.
|
||||
*
|
||||
* @internal ICU 72 technology preview
|
||||
* @deprecated This API is for ICU internal use only.
|
||||
*/
|
||||
@Deprecated
|
||||
@Override
|
||||
public String toString() {
|
||||
return formattedValue.toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
// © 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 formatters
|
||||
* that can be used from {@link MessageFormatter}.
|
||||
*
|
||||
* @internal ICU 72 technology preview
|
||||
* @deprecated This API is for ICU internal use only.
|
||||
*/
|
||||
@Deprecated
|
||||
public interface Formatter {
|
||||
/**
|
||||
* A method that takes the object to format and returns
|
||||
* the i18n-aware string representation.
|
||||
*
|
||||
* @param toFormat the object to format.
|
||||
* @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
|
||||
String formatToString(Object toFormat, Map<String, Object> variableOptions);
|
||||
|
||||
/**
|
||||
* A method that takes the object to format and returns
|
||||
* the i18n-aware formatted placeholder.
|
||||
*
|
||||
* @param toFormat the object to format.
|
||||
* @param variableOptions options that are not know at build time.
|
||||
* @return the formatted placeholder.
|
||||
*
|
||||
* @internal ICU 72 technology preview
|
||||
* @deprecated This API is for ICU internal use only.
|
||||
*/
|
||||
@Deprecated
|
||||
FormattedPlaceholder format(Object toFormat, Map<String, Object> variableOptions);
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
// © 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 formatting function name
|
||||
* that can be used from {@link MessageFormatter}.
|
||||
*
|
||||
* <p>We use it to create and cache various formatters with various options.</p>
|
||||
*
|
||||
* @internal ICU 72 technology preview
|
||||
* @deprecated This API is for ICU internal use only.
|
||||
*/
|
||||
@Deprecated
|
||||
public interface FormatterFactory {
|
||||
/**
|
||||
* The method that is called to create a formatter.
|
||||
*
|
||||
* @param locale the locale to use for formatting.
|
||||
* @param fixedOptions the options to use for formatting. The keys and values are function dependent.
|
||||
* @return the formatter.
|
||||
* @throws IllegalArgumentException
|
||||
*
|
||||
* @internal ICU 72 technology preview
|
||||
* @deprecated This API is for ICU internal use only.
|
||||
*/
|
||||
@Deprecated
|
||||
Formatter createFormatter(Locale locale, Map<String, Object> fixedOptions);
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
// © 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 java.util.Objects;
|
||||
|
||||
/**
|
||||
* Creates a {@link Formatter} that simply returns the String non-i18n aware representation of an object.
|
||||
*/
|
||||
class IdentityFormatterFactory implements FormatterFactory {
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Formatter createFormatter(Locale locale, Map<String, Object> fixedOptions) {
|
||||
return new IdentityFormatterImpl();
|
||||
}
|
||||
|
||||
private static class IdentityFormatterImpl implements Formatter {
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public FormattedPlaceholder format(Object toFormat, Map<String, Object> variableOptions) {
|
||||
return new FormattedPlaceholder(toFormat, new PlainStringFormattedValue(Objects.toString(toFormat)));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public String formatToString(Object toFormat, Map<String, Object> variableOptions) {
|
||||
return format(toFormat, variableOptions).toString();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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}.
|
||||
*
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p>It is more modular, easier to backport, and provides extension points to add new
|
||||
* formatters and selectors without having to modify the specification.</p>
|
||||
*
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p>MessageFormat 2 will support more complex grammatical features, such as gender, inflections,
|
||||
* and tagging parts of the message for style changes or speech.</p>
|
||||
*
|
||||
* <p>The reasoning for this effort is shared in the
|
||||
* <a target="github" href="https://github.com/unicode-org/message-format-wg/blob/main/docs/why_mf_next.md">“Why
|
||||
* MessageFormat needs a successor”</a> document.</p>
|
||||
*
|
||||
* <p>The “MessageFormat 2” project, which develops the new data model, semantics, and syntax,
|
||||
* is hosted on <a target="github" href="https://github.com/unicode-org/message-format-wg">GitHub</a>.</p>
|
||||
*
|
||||
* <p>The current specification for the syntax and data model can be found
|
||||
* <a target="github" href="https://github.com/unicode-org/message-format-wg/blob/develop/spec/syntax.md">here</a>.</p>
|
||||
*
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p>These are the functions interpreted right now:</p>
|
||||
*
|
||||
* <table border="1">
|
||||
* <tr>
|
||||
* <td rowspan="4">{@code datetime}</td>
|
||||
* <td>Similar to the ICU {@code "date"} and {@code "time"}.</td>
|
||||
* </tr>
|
||||
*
|
||||
* <tr><td>{@code datestyle} and {@code timestyle}<br>
|
||||
* Similar to {@code argStyle : short | medium | long | full}.<br>
|
||||
* Same values are accepted, but we can use both in one placeholder,
|
||||
* for example <code>{$due :datetime datestyle=full timestyle=long}</code>.
|
||||
* </td></tr>
|
||||
*
|
||||
* <tr><td>{@code pattern}<br>
|
||||
* Similar to {@code argStyle = argStyleText}.<br>
|
||||
* This is bad i18n practice, and will probably be dropped.<br>
|
||||
* This is included just to support migration to MessageFormat 2.
|
||||
* </td></tr>
|
||||
*
|
||||
* <tr><td>{@code skeleton}<br>
|
||||
* Same as {@code argStyle = argSkeletonText}.<br>
|
||||
* These are the date/time skeletons as supported by {@link com.ibm.icu.text.SimpleDateFormat}.
|
||||
* </td></tr>
|
||||
*
|
||||
* <tr>
|
||||
* <td rowspan="4">{@code number}</td>
|
||||
* <td>Similar to the ICU "number".</td>
|
||||
* </tr>
|
||||
*
|
||||
* <tr><td>{@code skeleton}<br>
|
||||
* These are the number skeletons as supported by {@link com.ibm.icu.number.NumberFormatter}.</td></tr>
|
||||
*
|
||||
* <tr><td>{@code minimumFractionDigits}<br>
|
||||
* Only implemented to be able to pass the unit tests from the ECMA tech preview implementation,
|
||||
* which prefers options bags to skeletons.<br>
|
||||
* TBD if the final {@number} function will support skeletons, option backs, or both.</td></tr>
|
||||
*
|
||||
* <tr><td>{@code offset}<br>
|
||||
* Used to support plural with an offset.</td></tr>
|
||||
*
|
||||
* <tr><td >{@code identity}</td><td>Returns the direct string value of the argument (calling {@code toString()}).</td></tr>
|
||||
*
|
||||
* <tr>
|
||||
* <td rowspan="3">{@code plural}</td>
|
||||
* <td>Similar to the ICU {@code "plural"}.</td>
|
||||
* </tr>
|
||||
*
|
||||
* <tr><td>{@code skeleton}<br>
|
||||
* These are the number skeletons as supported by {@link com.ibm.icu.number.NumberFormatter}.<br>
|
||||
* Can also be indirect, from a local variable of type {@code number} (recommended).</td></tr>
|
||||
*
|
||||
* <tr><td>{@code offset}<br>
|
||||
* Used to support plural with an offset.<br>
|
||||
* Can also be indirect, from a local variable of type {@code number} (recommended).</td></tr>
|
||||
*
|
||||
* <tr>
|
||||
* <td>{@code selectordinal}</td>
|
||||
* <td>Similar to the ICU {@code "selectordinal"}.<br>
|
||||
* For now it accepts the same parameters as "plural", although there is no use case for them.<br>
|
||||
* TBD if this will be merged into "plural" (with some {@code kind} option) or not.</td></tr>
|
||||
*
|
||||
* <tr><td>{@code select}</td><td>Literal match, same as the ICU4 {@code "select"}.</td></tr>
|
||||
* </table>
|
||||
*
|
||||
* @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}.
|
||||
*
|
||||
* <p>If the {@code MessageFormatter} was created from an {@link Mf2DataModel}
|
||||
* the this string is generated from that model.</p>
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* <p>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.</p>
|
||||
*
|
||||
* <p>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}.</p>
|
||||
*
|
||||
* @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<String, Object> 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<String, Object> 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.<br>
|
||||
* 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.
|
||||
*
|
||||
* <p>There is no need to do this in order to use standard functions
|
||||
* (for example date / time / number formatting, plural / ordinal / literal selection).<br>
|
||||
* The exact set of standard functions, with the types they format and the options
|
||||
* they accept is still TBD.</p>
|
||||
*
|
||||
* @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.<br>
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
*
|
||||
* <p>See <a target="github" href="https://github.com/unicode-org/message-format-wg/blob/develop/spec/syntax.md">the
|
||||
* description of the syntax with examples and use cases</a> and the corresponding
|
||||
* <a target="github" href="https://github.com/unicode-org/message-format-wg/blob/develop/spec/message.ebnf">EBNF</a>.</p>
|
||||
*
|
||||
* @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<String> 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<String> 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<String> 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<String> 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<Part> 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<Part> 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<Part> 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<Part> 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<String, Value> 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<String, Value> 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<String, Value> 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<String, Value> 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<String, Value> 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<K, V> extends LinkedHashMap<K, V> {
|
||||
private static final long serialVersionUID = -7049361727790825496L;
|
||||
}
|
||||
|
||||
private final OrderedMap<String, Expression> localVariables;
|
||||
private final List<Expression> selectors;
|
||||
private final OrderedMap<SelectorKeys, Pattern> 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<String, Expression> getLocalVariables() {
|
||||
return localVariables;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal ICU 72 technology preview
|
||||
* @deprecated This API is for ICU internal use only.
|
||||
*/
|
||||
@Deprecated
|
||||
public List<Expression> getSelectors() {
|
||||
return selectors;
|
||||
}
|
||||
|
||||
/**
|
||||
* @internal ICU 72 technology preview
|
||||
* @deprecated This API is for ICU internal use only.
|
||||
*/
|
||||
@Deprecated
|
||||
public OrderedMap<SelectorKeys, Pattern> 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<String, Expression> 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<SelectorKeys, Pattern> 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<String, Expression> localVariables = new OrderedMap<>(); // declaration*
|
||||
private final List<Expression> selectors = new ArrayList<>();
|
||||
private final OrderedMap<SelectorKeys, Pattern> 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<String, Expression> 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<Expression> 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<SelectorKeys, Pattern> 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<String, Object> mf2OptToFixedOptions(Map<String, Value> options) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
for (Entry<String, Value> option : options.entrySet()) {
|
||||
Value value = option.getValue();
|
||||
if (value.isLiteral()) {
|
||||
result.put(option.getKey(), value.getLiteral());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<String, Object> mf2OptToVariableOptions(Map<String, Value> options, Map<String, Object> arguments) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
for (Entry<String, Value> 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<String, Object> arguments) {
|
||||
List<Expression> 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<Expression> selectors, Map<String, Object> arguments) {
|
||||
Pattern patternToRender = null;
|
||||
|
||||
// Collect all the selector functions in an array, to reuse
|
||||
List<Selector> 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<String, Object> 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<SelectorKeys, Pattern> variant : dm.getVariants().entrySet()) {
|
||||
int maxCount = selectors.size();
|
||||
List<String> 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<String, Object> 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:
|
||||
*
|
||||
* <code><pre>
|
||||
* let $l1 = {$l4 :number}
|
||||
* let $l2 = {$l1 :number}
|
||||
* let $l3 = {$l2 :number}
|
||||
* let $l4 = {$l3 :number}
|
||||
* </pre></code>
|
||||
*
|
||||
* 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<String, Object> 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<String, Object> 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<String, Object> fixedOptions = mf2OptToFixedOptions(ph.getOptions());
|
||||
Formatter ff = funcFactory.createFormatter(locale, fixedOptions);
|
||||
ph.formatter = ff;
|
||||
}
|
||||
}
|
||||
if (ph.formatter != null) {
|
||||
Map<String, Object> 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() + "}"));
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
*
|
||||
* <p>For example to add formatting for a {@code Person} object one would need to:</p>
|
||||
* <ul>
|
||||
* <li>write a function (class, lambda, etc.) that does the formatting proper
|
||||
* (implementing {@link Formatter})</li>
|
||||
* <li>write a factory that creates such a function
|
||||
* (implementing {@link FormatterFactory})</li>
|
||||
* <li>add a mapping from the function name as used in the syntax
|
||||
* (for example {@code "person"}) to the factory</li>
|
||||
* <li>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}).
|
||||
* </li>
|
||||
* </ul>
|
||||
*
|
||||
* @internal ICU 72 technology preview
|
||||
* @deprecated This API is for ICU internal use only.
|
||||
*/
|
||||
@Deprecated
|
||||
public class Mf2FunctionRegistry {
|
||||
private final Map<String, FormatterFactory> formattersMap;
|
||||
private final Map<String, SelectorFactory> selectorsMap;
|
||||
private final Map<Class<?>, 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}.
|
||||
*
|
||||
* <p>Note: function name here means the name used to refer to the function in the
|
||||
* MessageFormat 2 syntax, for example {@code "... {$exp :datetime} ..."}<br>
|
||||
* 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.</p>
|
||||
*
|
||||
* <p>For example one might write a {@code PersonFormatterFactory} returning a {@code PersonFormatter},
|
||||
* and map that to the MessageFormat function named {@code "person"}.<br>
|
||||
* The only name visible to the users of MessageFormat syntax will be {@code "person"}.</p>
|
||||
*
|
||||
* @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<String> 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<Class<?>, 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<Class<?>> getDefaultFormatterTypes() {
|
||||
return classToFormatter.keySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the selector factory used to create the selector for function
|
||||
* named {@code name}.
|
||||
*
|
||||
* <p>Note: the same comments about naming as the ones on {@code getFormatter} apply.</p>
|
||||
*
|
||||
* @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<String> 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<String, FormatterFactory> formattersMap = new HashMap<>();
|
||||
private final Map<String, SelectorFactory> selectorsMap = new HashMap<>();
|
||||
private final Map<Class<?>, 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);
|
||||
}
|
||||
}
|
||||
}
|
754
icu4j/main/classes/core/src/com/ibm/icu/message2/Mf2Parser.java
Normal file
754
icu4j/main/classes/core/src/com/ibm/icu/message2/Mf2Parser.java
Normal file
|
@ -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<String> 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
|
|
@ -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<Token> 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<Pattern> 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<List<Expression>> selectorResult = parseSelector(i);
|
||||
result.addSelectors(selectorResult.resultValue);
|
||||
i = selectorResult.skipLen;
|
||||
break;
|
||||
case VARIANT:
|
||||
ParseResult<Variant> 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<Variant> 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<Pattern> 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<List<Expression>> parseSelector(int startToken) {
|
||||
List<Expression> 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<Expression> 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<Expression> 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<Pattern> 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<Expression> 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<T> {
|
||||
final int skipLen;
|
||||
final T resultValue;
|
||||
|
||||
public ParseResult(int skipLen, T resultValue) {
|
||||
this.skipLen = skipLen;
|
||||
this.resultValue = resultValue;
|
||||
}
|
||||
}
|
||||
|
||||
private ParseResult<Expression> 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();
|
||||
}
|
||||
}
|
|
@ -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 <code>{exp, number}</code>
|
||||
* in {@link com.ibm.icu.text.MessageFormat}.
|
||||
*/
|
||||
class NumberFormatterFactory implements FormatterFactory {
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Formatter createFormatter(Locale locale, Map<String, Object> fixedOptions) {
|
||||
return new NumberFormatterImpl(locale, fixedOptions);
|
||||
}
|
||||
|
||||
static class NumberFormatterImpl implements Formatter {
|
||||
private final Locale locale;
|
||||
private final Map<String, Object> fixedOptions;
|
||||
private final LocalizedNumberFormatter icuFormatter;
|
||||
final boolean advanced;
|
||||
|
||||
private static LocalizedNumberFormatter formatterForOptions(Locale locale, Map<String, Object> 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<String, Object> 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<String, Object> variableOptions) {
|
||||
return format(toFormat, variableOptions).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public FormattedPlaceholder format(Object toFormat, Map<String, Object> variableOptions) {
|
||||
LocalizedNumberFormatter realFormatter;
|
||||
if (variableOptions.isEmpty()) {
|
||||
realFormatter = this.icuFormatter;
|
||||
} else {
|
||||
Map<String, Object> 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<String, Object> 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<String, Object> options, String key) {
|
||||
Object value = options.get(key);
|
||||
if (value == null) {
|
||||
return null;
|
||||
}
|
||||
if (value instanceof CharSequence) {
|
||||
return value.toString();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
|
@ -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 extends Appendable> 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;
|
||||
}
|
||||
}
|
|
@ -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 <code>{exp, plural}</code>
|
||||
* 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<String, Object> fixedOptions) {
|
||||
PluralRules rules = PluralRules.forLocale(locale, pluralType);
|
||||
return new PluralSelectorImpl(rules, fixedOptions);
|
||||
}
|
||||
|
||||
private static class PluralSelectorImpl implements Selector {
|
||||
private final PluralRules rules;
|
||||
private Map<String, Object> fixedOptions;
|
||||
|
||||
private PluralSelectorImpl(PluralRules rules, Map<String, Object> fixedOptions) {
|
||||
this.rules = rules;
|
||||
this.fixedOptions = fixedOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean matches(Object value, String key, Map<String, Object> 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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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}.
|
||||
*
|
||||
* <p>Selectors are used to choose between different message variants,
|
||||
* similar to <code>plural</code>, <code>selectordinal</code>,
|
||||
* and <code>select</code> in {@link com.ibm.icu.text.MessageFormat}.</p>
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* <p>For example an English plural {@code matches} would return {@code true}
|
||||
* for {@code matches(1, "1")}, {@code matches(1, "one")}, and {@code matches(1, "*")}.</p>
|
||||
*
|
||||
* @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<String, Object> variableOptions);
|
||||
}
|
|
@ -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}.
|
||||
*
|
||||
* <p>The we use it to create and cache various selectors with various options.</p>
|
||||
*
|
||||
* @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<String, Object> fixedOptions);
|
||||
}
|
|
@ -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 <code>{exp, select}</code>
|
||||
* in {@link com.ibm.icu.text.MessageFormat}.
|
||||
*/
|
||||
class TextSelectorFactory implements SelectorFactory {
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Selector createSelector(Locale locale, Map<String, Object> fixedOptions) {
|
||||
return new TextSelector();
|
||||
}
|
||||
|
||||
private static class TextSelector implements Selector {
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public boolean matches(Object value, String key, Map<String, Object> variableOptions) {
|
||||
if ("*".equals(key)) {
|
||||
return true;
|
||||
}
|
||||
return key.equals(value);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
|
||||
<html>
|
||||
<head>
|
||||
<!-- Copyright (C) 2022 and later: Unicode, Inc. and others.
|
||||
License & terms of use: http://www.unicode.org/copyright.html
|
||||
-->
|
||||
<title>ICU4J com.ibm.icu.message2 Package Overview</title>
|
||||
</head>
|
||||
<body bgcolor="white">
|
||||
|
||||
<p>Tech Preview implementation of the
|
||||
<a href="https://github.com/unicode-org/message-format-wg/blob/develop/spec/syntax.md">MessageFormat v2 specification</a>.</p>
|
||||
|
||||
</body>
|
||||
</html>
|
|
@ -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
|
||||
* <code>Map.of</code> introduced in JDK 11, which can't be used yet for ICU4J.
|
||||
*
|
||||
* <p>The returned Map is immutable, to prove that the {@link MessageFormatter}
|
||||
* does not change it</p>
|
||||
*/
|
||||
@SuppressWarnings("javadoc")
|
||||
public class Args {
|
||||
|
||||
public static final Map<String, Object> NONE = new HashMap<>();
|
||||
|
||||
public static Map<String, Object> of(
|
||||
String argName0, Object argValue0) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put(argName0, argValue0);
|
||||
return Collections.unmodifiableMap(result);
|
||||
}
|
||||
|
||||
public static Map<String, Object> of(
|
||||
String argName0, Object argValue0,
|
||||
String argName1, Object argValue1) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put(argName0, argValue0);
|
||||
result.put(argName1, argValue1);
|
||||
return Collections.unmodifiableMap(result);
|
||||
}
|
||||
|
||||
public static Map<String, Object> of(
|
||||
String argName0, Object argValue0,
|
||||
String argName1, Object argValue1,
|
||||
String argName2, Object argValue2) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put(argName0, argValue0);
|
||||
result.put(argName1, argValue1);
|
||||
result.put(argName2, argValue2);
|
||||
return Collections.unmodifiableMap(result);
|
||||
}
|
||||
|
||||
public static Map<String, Object> of(
|
||||
String argName0, Object argValue0,
|
||||
String argName1, Object argValue1,
|
||||
String argName2, Object argValue2,
|
||||
String argName3, Object argValue3) {
|
||||
Map<String, Object> 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<String, Object> of(
|
||||
String argName0, Object argValue0,
|
||||
String argName1, Object argValue1,
|
||||
String argName2, Object argValue2,
|
||||
String argName3, Object argValue3,
|
||||
String argName4, Object argValue4) {
|
||||
Map<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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);
|
||||
}
|
||||
}
|
|
@ -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<String, Object> 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<String, Object> variableOptions) {
|
||||
return format(toFormat, variableOptions).toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public FormattedPlaceholder format(Object toFormat, Map<String, Object> 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")));
|
||||
}
|
||||
}
|
|
@ -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<String, Object> fixedOptions) {
|
||||
return new ListFormatterImpl(locale, fixedOptions);
|
||||
}
|
||||
|
||||
static class ListFormatterImpl implements Formatter {
|
||||
private final ListFormatter lf;
|
||||
|
||||
ListFormatterImpl(Locale locale, Map<String, Object> 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<String, Object> variableOptions) {
|
||||
return format(toFormat, variableOptions).toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public FormattedPlaceholder format(Object toFormat, Map<String, Object> 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());
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
*
|
||||
* <p>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.</p>
|
||||
*/
|
||||
@RunWith(JUnit4.class)
|
||||
@SuppressWarnings("javadoc")
|
||||
public class CustomFormatterMessageRefTest extends TestFmwk {
|
||||
|
||||
static class ResourceManagerFactory implements FormatterFactory {
|
||||
|
||||
@Override
|
||||
public Formatter createFormatter(Locale locale, Map<String, Object> fixedOptions) {
|
||||
return new ResourceManagerFactoryImpl(locale, fixedOptions);
|
||||
}
|
||||
|
||||
static class ResourceManagerFactoryImpl implements Formatter {
|
||||
final Map<String, Object> options;
|
||||
|
||||
ResourceManagerFactoryImpl(Locale locale, Map<String, Object> options) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FormattedPlaceholder format(Object toFormat, Map<String, Object> 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<String, Object> 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<String, Object> 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));
|
||||
}
|
||||
}
|
|
@ -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<String, Object> 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<String, Object> 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<String, Object> 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());
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
*
|
||||
* <p>Original JSON file
|
||||
* <a href="https://github.com/messageformat/messageformat/blob/master/packages/mf2-messageformat/src/__fixtures/test-messages.json">here</a>.</p>
|
||||
*/
|
||||
@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);
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
*
|
||||
* <p>It does not include all the tests for edge cases and error handling, only the ones that show real functionality.</p>
|
||||
*/
|
||||
@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<String, Object> 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<String, LocalizedNumberFormatter> cachedFormatters =
|
||||
new HashMap<>();
|
||||
|
||||
TemperatureFormatterImpl(LocalizedNumberFormatter nf, TemperatureFormatterFactory formatterFactory) {
|
||||
this.nf = nf;
|
||||
this.formatterFactory = formatterFactory;
|
||||
this.formatterFactory.constructCount++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String formatToString(Object toFormat, Map<String, Object> variableOptions) {
|
||||
return this.format(toFormat, variableOptions).toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public FormattedPlaceholder format(Object toFormat, Map<String, Object> 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)));
|
||||
}
|
||||
}
|
|
@ -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.
|
||||
*
|
||||
* <p>It covers the examples in the
|
||||
* <a href="https://github.com/unicode-org/message-format-wg/blob/develop/spec/syntax.md">spec document</a>,
|
||||
* except for the custom formatters ones, which are too verbose and were moved to separate test classes.</p>
|
||||
* </p>
|
||||
*/
|
||||
@RunWith(JUnit4.class)
|
||||
@SuppressWarnings("javadoc")
|
||||
public class Mf2FeaturesTest extends TestFmwk {
|
||||
|
||||
// November 23, 2022 at 7:42:37.123 PM
|
||||
static final Date TEST_DATE = new Date(1669261357123L);
|
||||
|
||||
@Test
|
||||
public void testEmptyMessage() {
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern("{}")
|
||||
.arguments(Args.NONE)
|
||||
.expected("")
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPlainText() {
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern("{Hello World!}")
|
||||
.arguments(Args.NONE)
|
||||
.expected("Hello World!")
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPlaceholders() {
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern("{Hello, {$userName}!}")
|
||||
.arguments(Args.of("userName", "John"))
|
||||
.expected("Hello, John!")
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testArgumentMissing() {
|
||||
// Test to check what happens if an argument name from the placeholder is not found
|
||||
// We do what the old ICU4J MessageFormat does.
|
||||
String message = "{Hello {$name}, today is {$today :datetime skeleton=yMMMMdEEEE}.}";
|
||||
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern(message)
|
||||
.arguments(Args.of("name", "John", "today", TEST_DATE))
|
||||
.expected("Hello John, today is Wednesday, November 23, 2022.")
|
||||
.build());
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern(message)
|
||||
.arguments(Args.of("name", "John"))
|
||||
.expected("Hello John, today is {$today}.")
|
||||
.build());
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern(message)
|
||||
.arguments(Args.of("today", TEST_DATE))
|
||||
.expected("Hello {$name}, today is Wednesday, November 23, 2022.")
|
||||
.build());
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern(message)
|
||||
.arguments(Args.NONE)
|
||||
.expected("Hello {$name}, today is {$today}.")
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDefaultLocale() {
|
||||
String message = "{Date: {$date :datetime skeleton=yMMMMdEEEE}.}";
|
||||
String expectedEn = "Date: Wednesday, November 23, 2022.";
|
||||
String expectedRo = "Date: miercuri, 23 noiembrie 2022.";
|
||||
Map<String, Object> arguments = Args.of("date", TEST_DATE);
|
||||
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern(message)
|
||||
.arguments(arguments)
|
||||
.expected(expectedEn)
|
||||
.build());
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern(message)
|
||||
.arguments(arguments)
|
||||
.locale("ro")
|
||||
.expected(expectedRo)
|
||||
.build());
|
||||
|
||||
Locale originalLocale = Locale.getDefault();
|
||||
Locale.setDefault(Locale.forLanguageTag("ro"));
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern(message)
|
||||
.arguments(arguments)
|
||||
.locale("en-US")
|
||||
.expected(expectedEn)
|
||||
.build());
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern(message)
|
||||
.arguments(arguments)
|
||||
.expected(expectedRo)
|
||||
.build());
|
||||
Locale.setDefault(originalLocale);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAllKindOfDates() {
|
||||
// Default function
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern("{Testing date formatting: {$date}.}")
|
||||
.locale("ro")
|
||||
.arguments(Args.of("date", TEST_DATE))
|
||||
.expected("Testing date formatting: 23.11.2022, 19:42.")
|
||||
.build());
|
||||
// Default options
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern("{Testing date formatting: {$date :datetime}.}")
|
||||
.locale("ro")
|
||||
.arguments(Args.of("date", TEST_DATE))
|
||||
.expected("Testing date formatting: 23.11.2022, 19:42.")
|
||||
.build());
|
||||
|
||||
// Skeleton
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern("{Testing date formatting: {$date :datetime skeleton=yMMMMd}.}")
|
||||
.locale("ro-RO")
|
||||
.arguments(Args.of("date", TEST_DATE))
|
||||
.expected("Testing date formatting: 23 noiembrie 2022.")
|
||||
.build());
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern("{Testing date formatting: {$date :datetime skeleton=jm}.}")
|
||||
.locale("ro-RO")
|
||||
.arguments(Args.of("date", TEST_DATE))
|
||||
.expected("Testing date formatting: 19:42.")
|
||||
.build());
|
||||
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern("{Testing date formatting: {$date :datetime skeleton=yMMMd}.}")
|
||||
.arguments(Args.of("date", TEST_DATE))
|
||||
.expected("Testing date formatting: Nov 23, 2022.")
|
||||
.build());
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern("{Testing date formatting: {$date :datetime skeleton=yMMMdjms}.}")
|
||||
.arguments(Args.of("date", TEST_DATE))
|
||||
.expected("Testing date formatting: Nov 23, 2022, 7:42:37\u202FPM.")
|
||||
.build());
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern("{Testing date formatting: {$date :datetime skeleton=jm}.}")
|
||||
.arguments(Args.of("date", TEST_DATE))
|
||||
.expected("Testing date formatting: 7:42\u202FPM.")
|
||||
.build());
|
||||
|
||||
// Style
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern("{Testing date formatting: {$date :datetime datestyle=long}.}")
|
||||
.arguments(Args.of("date", TEST_DATE))
|
||||
.expected("Testing date formatting: November 23, 2022.")
|
||||
.build());
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern(
|
||||
"{Testing date formatting: {$date :datetime datestyle=medium}.}")
|
||||
.arguments(Args.of("date", TEST_DATE))
|
||||
.expected("Testing date formatting: Nov 23, 2022.")
|
||||
.build());
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern("{Testing date formatting: {$date :datetime datestyle=short}.}")
|
||||
.arguments(Args.of("date", TEST_DATE))
|
||||
.expected("Testing date formatting: 11/23/22.")
|
||||
.build());
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern("{Testing date formatting: {$date :datetime timestyle=long}.}")
|
||||
.arguments(Args.of("date", TEST_DATE))
|
||||
.expected("Testing date formatting: 7:42:37\u202FPM PST.")
|
||||
.build());
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern("{Testing date formatting: {$date :datetime timestyle=medium}.}")
|
||||
.arguments(Args.of("date", TEST_DATE))
|
||||
.expected("Testing date formatting: 7:42:37\u202FPM.")
|
||||
.build());
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern(
|
||||
"{Testing date formatting: {$date :datetime timestyle=short}.}")
|
||||
.arguments(Args.of("date", TEST_DATE))
|
||||
.expected("Testing date formatting: 7:42\u202FPM.")
|
||||
.build());
|
||||
|
||||
// Pattern
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern("{Testing date formatting: {$date :datetime pattern=(d 'of' MMMM, y 'at' HH:mm)}.}")
|
||||
.arguments(Args.of("date", TEST_DATE))
|
||||
.expected("Testing date formatting: 23 of November, 2022 at 19:42.")
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAllKindOfNumbers() {
|
||||
double value = 1234567890.97531;
|
||||
|
||||
// From literal values
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern("{From literal: {(123456789) :number}!}")
|
||||
.locale("ro")
|
||||
.arguments(Args.of("val", value))
|
||||
.expected("From literal: 123.456.789!")
|
||||
.build());
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern("{From literal: {(123456789.531) :number}!}")
|
||||
.locale("ro")
|
||||
.arguments(Args.of("val", value))
|
||||
.expected("From literal: 123.456.789,531!")
|
||||
.build());
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern("{From literal: {(123456789.531) :number}!}")
|
||||
.locale("my")
|
||||
.arguments(Args.of("val", value))
|
||||
.expected("From literal: \u1041\u1042\u1043,\u1044\u1045\u1046,\u1047\u1048\u1049.\u1045\u1043\u1041!")
|
||||
.build());
|
||||
|
||||
// Testing that the detection works for various types (without specifying :number)
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern("{Default double: {$val}!}")
|
||||
.locale("en-IN")
|
||||
.arguments(Args.of("val", value))
|
||||
.expected("Default double: 1,23,45,67,890.97531!")
|
||||
.build());
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern("{Default double: {$val}!}")
|
||||
.locale("ro")
|
||||
.arguments(Args.of("val", value))
|
||||
.expected("Default double: 1.234.567.890,97531!")
|
||||
.build());
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern("{Default float: {$val}!}")
|
||||
.locale("ro")
|
||||
.arguments(Args.of("val", 3.1415926535))
|
||||
.expected("Default float: 3,141593!")
|
||||
.build());
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern("{Default long: {$val}!}")
|
||||
.locale("ro")
|
||||
.arguments(Args.of("val", 1234567890123456789L))
|
||||
.expected("Default long: 1.234.567.890.123.456.789!")
|
||||
.build());
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern("{Default number: {$val}!}")
|
||||
.locale("ro")
|
||||
.arguments(Args.of("val", new BigDecimal("1234567890123456789.987654321")))
|
||||
.expected("Default number: 1.234.567.890.123.456.789,987654!")
|
||||
.build());
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern("{Price: {$val}}")
|
||||
.locale("de")
|
||||
.arguments(Args.of("val", new CurrencyAmount(1234.56, Currency.getInstance("EUR"))))
|
||||
.expected("Price: 1.234,56\u00A0\u20AC")
|
||||
.build());
|
||||
|
||||
// Various skeletons
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern("{Skeletons, minFraction: {$val :number skeleton=(.00000000*)}!}")
|
||||
.locale("ro")
|
||||
.arguments(Args.of("val", value))
|
||||
.expected("Skeletons, minFraction: 1.234.567.890,97531000!")
|
||||
.build());
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern("{Skeletons, maxFraction: {$val :number skeleton=(.###)}!}")
|
||||
.locale("ro")
|
||||
.arguments(Args.of("val", value))
|
||||
.expected("Skeletons, maxFraction: 1.234.567.890,975!")
|
||||
.build());
|
||||
// Currency
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern("{Skeletons, currency: {$val :number skeleton=(currency/EUR)}!}")
|
||||
.locale("de")
|
||||
.arguments(Args.of("val", value))
|
||||
.expected("Skeletons, currency: 1.234.567.890,98\u00A0\u20AC!")
|
||||
.build());
|
||||
// Currency as a parameter
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern("{Skeletons, currency: {$val :number skeleton=$skel}!}")
|
||||
.locale("de")
|
||||
.arguments(Args.of("val", value, "skel", "currency/EUR"))
|
||||
.expected("Skeletons, currency: 1.234.567.890,98\u00A0\u20AC!")
|
||||
.build());
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern("{Skeletons, currency: {$val :number skeleton=$skel}!}")
|
||||
.locale("de")
|
||||
.arguments(Args.of("val", value, "skel", "currency/JPY"))
|
||||
.expected("Skeletons, currency: 1.234.567.891\u00A0\u00A5!")
|
||||
.build());
|
||||
|
||||
// Various measures
|
||||
double celsius = 27;
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern(""
|
||||
+ "let $intl = {$valC :number skeleton=(unit/celsius)}\n"
|
||||
+ "let $us = {$valF :number skeleton=(unit/fahrenheit)}\n"
|
||||
+ "{Temperature: {$intl} ({$us})}")
|
||||
.locale("ro")
|
||||
.arguments(Args.of("valC", celsius, "valF", celsius * 9 / 5 + 32))
|
||||
.expected("Temperature: 27 \u00B0C (80,6 \u00B0F)")
|
||||
.build());
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern("{Height: {$len :number skeleton=(unit/meter)}}")
|
||||
.locale("ro")
|
||||
.arguments(Args.of("len", 1.75))
|
||||
.expected("Height: 1,75 m")
|
||||
.build());
|
||||
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern("{Skeletons, currency: {$val :number skeleton=(currency/EUR)}!}")
|
||||
.locale("de")
|
||||
.arguments(Args.of("val", value))
|
||||
.expected("Skeletons, currency: 1.234.567.890,98\u00A0\u20AC!")
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSpecialPluralWithDecimals() {
|
||||
String message;
|
||||
message = "let $amount = {$count :number}\n"
|
||||
+ "match {$amount :plural}\n"
|
||||
+ " when 1 {I have {$amount} dollar.}\n"
|
||||
+ " when * {I have {$amount} dollars.}\n";
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern(message)
|
||||
.locale("en-US")
|
||||
.arguments(Args.of("count", 1))
|
||||
.expected("I have 1 dollar.")
|
||||
.build());
|
||||
message = "let $amount = {$count :number skeleton=(.00*)}\n"
|
||||
+ "match {$amount :plural skeleton=(.00*)}\n"
|
||||
+ " when 1 {I have {$amount} dollar.}\n"
|
||||
+ " when * {I have {$amount} dollars.}\n";
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern(message)
|
||||
.locale("en-US")
|
||||
.arguments(Args.of("count", 1))
|
||||
.expected("I have 1.00 dollars.")
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDefaultFunctionAndOptions() {
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern("{Testing date formatting: {$date}.}")
|
||||
.locale("ro")
|
||||
.arguments(Args.of("date", TEST_DATE))
|
||||
.expected("Testing date formatting: 23.11.2022, 19:42.")
|
||||
.build());
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern("{Testing date formatting: {$date :datetime}.}")
|
||||
.locale("ro")
|
||||
.arguments(Args.of("date", TEST_DATE))
|
||||
.expected("Testing date formatting: 23.11.2022, 19:42.")
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSimpleSelection() {
|
||||
String message = "match {$count :plural}\n"
|
||||
+ " when 1 {You have one notification.}\n"
|
||||
+ " when * {You have {$count} notifications.}\n";
|
||||
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern(message)
|
||||
.arguments(Args.of("count", 1))
|
||||
.expected("You have one notification.")
|
||||
.build());
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern(message)
|
||||
.arguments(Args.of("count", 42))
|
||||
.expected("You have 42 notifications.")
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testComplexSelection() {
|
||||
String message = ""
|
||||
+ "match {$photoCount :plural} {$userGender :select}\n"
|
||||
+ " when 1 masculine {{$userName} added a new photo to his album.}\n"
|
||||
+ " when 1 feminine {{$userName} added a new photo to her album.}\n"
|
||||
+ " when 1 * {{$userName} added a new photo to their album.}\n"
|
||||
+ " when * masculine {{$userName} added {$photoCount} photos to his album.}\n"
|
||||
+ " when * feminine {{$userName} added {$photoCount} photos to her album.}\n"
|
||||
+ " when * * {{$userName} added {$photoCount} photos to their album.}";
|
||||
|
||||
TestUtils.runTestCase(new TestCase.Builder().pattern(message)
|
||||
.arguments(Args.of("photoCount", 1, "userGender", "masculine", "userName", "John"))
|
||||
.expected("John added a new photo to his album.")
|
||||
.build());
|
||||
TestUtils.runTestCase(new TestCase.Builder().pattern(message)
|
||||
.arguments(Args.of("photoCount", 1, "userGender", "feminine", "userName", "Anna"))
|
||||
.expected("Anna added a new photo to her album.")
|
||||
.build());
|
||||
TestUtils.runTestCase(new TestCase.Builder().pattern(message)
|
||||
.arguments(Args.of("photoCount", 1, "userGender", "unknown", "userName", "Anonymous"))
|
||||
.expected("Anonymous added a new photo to their album.")
|
||||
.build());
|
||||
|
||||
TestUtils.runTestCase(new TestCase.Builder().pattern(message)
|
||||
.arguments(Args.of("photoCount", 13, "userGender", "masculine", "userName", "John"))
|
||||
.expected("John added 13 photos to his album.")
|
||||
.build());
|
||||
TestUtils.runTestCase(new TestCase.Builder().pattern(message)
|
||||
.arguments(Args.of("photoCount", 13, "userGender", "feminine", "userName", "Anna"))
|
||||
.expected("Anna added 13 photos to her album.")
|
||||
.build());
|
||||
TestUtils.runTestCase(new TestCase.Builder().pattern(message)
|
||||
.arguments(Args.of("photoCount", 13, "userGender", "unknown", "userName", "Anonymous"))
|
||||
.expected("Anonymous added 13 photos to their album.")
|
||||
.build());
|
||||
}
|
||||
|
||||
// Local Variables
|
||||
|
||||
@Test
|
||||
public void testSimpleLocaleVariable() {
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern(""
|
||||
+ "let $expDate = {$expDate :datetime skeleton=yMMMdE}\n"
|
||||
+ "{Your tickets expire on {$expDate}.}")
|
||||
.arguments(Args.of("count", 1, "expDate", TEST_DATE))
|
||||
.expected("Your tickets expire on Wed, Nov 23, 2022.")
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLocaleVariableWithSelect() {
|
||||
String message = ""
|
||||
+ "let $expDate = {$expDate :datetime skeleton=yMMMdE}\n"
|
||||
+ "match {$count :plural}\n"
|
||||
+ " when 1 {Your ticket expires on {$expDate}.}\n"
|
||||
+ " when * {Your {$count} tickets expire on {$expDate}.}\n";
|
||||
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern(message)
|
||||
.arguments(Args.of("count", 1, "expDate", TEST_DATE))
|
||||
.expected("Your ticket expires on Wed, Nov 23, 2022.")
|
||||
.build());
|
||||
TestUtils.runTestCase(new TestCase.Builder()
|
||||
.pattern(message)
|
||||
.arguments(Args.of("count", 3, "expDate", TEST_DATE))
|
||||
.expected("Your 3 tickets expire on Wed, Nov 23, 2022.")
|
||||
.build());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
// © 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.message2.MessageFormatter;
|
||||
import com.ibm.icu.text.MessageFormat;
|
||||
import com.ibm.icu.util.Calendar;
|
||||
import com.ibm.icu.util.GregorianCalendar;
|
||||
|
||||
/**
|
||||
* Ported the unit tests from {@link com.ibm.icu.text.MessageFormat} to show that they work.
|
||||
*
|
||||
* <p>It does not include all the tests for edge cases and error handling, only the ones that show real functionality.</p>
|
||||
*/
|
||||
@RunWith(JUnit4.class)
|
||||
@SuppressWarnings("javadoc")
|
||||
public class Mf2IcuTest extends TestFmwk {
|
||||
|
||||
@Test
|
||||
public void testSample() {
|
||||
MessageFormatter form = MessageFormatter.builder()
|
||||
.setPattern("{There are {$count} files on {$where}}")
|
||||
.build();
|
||||
assertEquals("format", "There are abc files on def",
|
||||
form.formatToString(Args.of("count", "abc", "where", "def")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testStaticFormat() {
|
||||
Map<String, Object> arguments = Args.of("planet", new Integer(7), "when", new Date(871068000000L), "what",
|
||||
"a disturbance in the Force");
|
||||
|
||||
assertEquals("format", "At 12:20:00\u202FPM on Aug 8, 1997, there was a disturbance in the Force on planet 7.",
|
||||
MessageFormatter.builder()
|
||||
.setPattern("{At {$when :datetime timestyle=default} on {$when :datetime datestyle=default}, "
|
||||
+ "there was {$what} on planet {$planet :number kind=integer}.}")
|
||||
.build()
|
||||
.formatToString(arguments));
|
||||
}
|
||||
|
||||
static final int FieldPosition_DONT_CARE = -1;
|
||||
|
||||
@Test
|
||||
public void testSimpleFormat() {
|
||||
Map<String, Object> testArgs1 = Args.of("fileCount", new Integer(0), "diskName", "MyDisk");
|
||||
Map<String, Object> testArgs2 = Args.of("fileCount", new Integer(1), "diskName", "MyDisk");
|
||||
Map<String, Object> testArgs3 = Args.of("fileCount", new Integer(12), "diskName", "MyDisk");
|
||||
|
||||
MessageFormatter form = MessageFormatter.builder()
|
||||
.setPattern("{The disk \"{$diskName}\" contains {$fileCount} file(s).}")
|
||||
.build();
|
||||
|
||||
assertEquals("format", "The disk \"MyDisk\" contains 0 file(s).", form.formatToString(testArgs1));
|
||||
|
||||
form.formatToString(testArgs2);
|
||||
assertEquals("format", "The disk \"MyDisk\" contains 1 file(s).", form.formatToString(testArgs2));
|
||||
|
||||
form.formatToString(testArgs3);
|
||||
assertEquals("format", "The disk \"MyDisk\" contains 12 file(s).", form.formatToString(testArgs3));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSelectFormatToPattern() {
|
||||
String pattern = ""
|
||||
+ "match {$userGender :select}\n"
|
||||
+ " when female {{$userName} est all\u00E9e \u00E0 Paris.}"
|
||||
+ " when * {{$userName} est all\u00E9 \u00E0 Paris.}"
|
||||
;
|
||||
|
||||
MessageFormatter mf = MessageFormatter.builder()
|
||||
.setPattern(pattern)
|
||||
.build();
|
||||
assertEquals("old icu test",
|
||||
"Charlotte est allée à Paris.",
|
||||
mf.formatToString(Args.of("userName", "Charlotte", "userGender", "female")));
|
||||
assertEquals("old icu test",
|
||||
"Guillaume est allé à Paris.",
|
||||
mf.formatToString(Args.of("userName", "Guillaume", "userGender", "male")));
|
||||
assertEquals("old icu test",
|
||||
"Dominique est allé à Paris.",
|
||||
mf.formatToString(Args.of("userName", "Dominique", "userGender", "unnown")));
|
||||
}
|
||||
|
||||
private static void doTheRealDateTimeSkeletonTesting(Date date, String messagePattern, Locale locale,
|
||||
String expected) {
|
||||
|
||||
MessageFormatter msgf = MessageFormatter.builder()
|
||||
.setPattern(messagePattern).setLocale(locale)
|
||||
.build();
|
||||
assertEquals(messagePattern, expected, msgf.formatToString(Args.of("when", date)));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMessageFormatDateTimeSkeleton() {
|
||||
Date date = new GregorianCalendar(2021, Calendar.NOVEMBER, 23, 16, 42, 55).getTime();
|
||||
|
||||
doTheRealDateTimeSkeletonTesting(date, "{{$when :datetime skeleton=MMMMd}}",
|
||||
Locale.ENGLISH, "November 23");
|
||||
doTheRealDateTimeSkeletonTesting(date, "{{$when :datetime skeleton=yMMMMdjm}}",
|
||||
Locale.ENGLISH, "November 23, 2021 at 4:42\u202FPM");
|
||||
doTheRealDateTimeSkeletonTesting(date, "{{$when :datetime skeleton=( yMMMMd )}}",
|
||||
Locale.ENGLISH, "November 23, 2021");
|
||||
doTheRealDateTimeSkeletonTesting(date, "{{$when :datetime skeleton=yMMMMd}}",
|
||||
Locale.FRENCH, "23 novembre 2021");
|
||||
doTheRealDateTimeSkeletonTesting(date, "{Expiration: {$when :datetime skeleton=yMMM}!}",
|
||||
Locale.ENGLISH, "Expiration: Nov 2021!");
|
||||
doTheRealDateTimeSkeletonTesting(date, "{{$when :datetime pattern=('::'yMMMMd)}}",
|
||||
Locale.ENGLISH, "::2021November23"); // pattern
|
||||
}
|
||||
|
||||
@Test
|
||||
public void checkMf1Behavior() {
|
||||
Date testDate = new Date(1671782400000L); // 2022-12-23
|
||||
Map<String, Object> goodArg = Args.of("user", "John", "today", testDate);
|
||||
Map<String, Object> badArg = Args.of("userX", "John", "todayX", testDate);
|
||||
|
||||
MessageFormat mf1 = new MessageFormat("Hello {user}, today is {today,date,long}.");
|
||||
assertEquals("old icu test", "Hello {user}, today is {today}.", mf1.format(badArg));
|
||||
assertEquals("old icu test", "Hello John, today is December 23, 2022.", mf1.format(goodArg));
|
||||
|
||||
MessageFormatter mf2 = MessageFormatter.builder()
|
||||
.setPattern("{Hello {$user}, today is {$today :datetime datestyle=long}.}")
|
||||
.build();
|
||||
assertEquals("old icu test", "Hello {$user}, today is {$today}.", mf2.formatToString(badArg));
|
||||
assertEquals("old icu test", "Hello John, today is December 23, 2022.", mf2.formatToString(goodArg));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
// © 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.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Locale.Category;
|
||||
import java.util.Map;
|
||||
import java.util.StringJoiner;
|
||||
|
||||
/** Utility class encapsulating what we need for a simple test. */
|
||||
class TestCase {
|
||||
final String message;
|
||||
final Locale locale;
|
||||
final Map<String, Object> arguments;
|
||||
final String expected;
|
||||
final boolean ignore;
|
||||
final String ignoreReason;
|
||||
final List<String> errors;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
StringJoiner result = new StringJoiner(",\n ", "TestCase {\n ", "\n}");
|
||||
result.add("message: " + message + "'");
|
||||
result.add("locale: '" + locale.toLanguageTag() + "'");
|
||||
result.add("arguments: " + arguments);
|
||||
result.add("expected: '" + expected + "'");
|
||||
result.add("ignore: " + ignore);
|
||||
result.add("ignoreReason: '" + ignoreReason + "'");
|
||||
result.add("errors: " + errors);
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
private TestCase(TestCase.Builder builder) {
|
||||
this.ignore = builder.ignore;
|
||||
this.message = builder.pattern == null ? "" : builder.pattern;
|
||||
this.locale = (builder.localeId == null)
|
||||
? Locale.getDefault(Category.FORMAT)
|
||||
: Locale.forLanguageTag(builder.localeId);
|
||||
this.arguments = builder.arguments == null ? Args.NONE : builder.arguments;
|
||||
this.expected = builder.expected == null ? "" : builder.expected;
|
||||
this.errors = builder.errors == null ? new ArrayList<String>() : builder.errors;
|
||||
this.ignoreReason = builder.ignoreReason == null ? "" : builder.ignoreReason;
|
||||
}
|
||||
|
||||
static class Builder {
|
||||
private String pattern;
|
||||
private String localeId;
|
||||
private Map<String, Object> arguments;
|
||||
private String expected;
|
||||
private boolean ignore = false;
|
||||
private String ignoreReason;
|
||||
private List<String> errors;
|
||||
|
||||
public TestCase build() {
|
||||
return new TestCase(this);
|
||||
}
|
||||
|
||||
public TestCase.Builder pattern(String pattern) {
|
||||
this.pattern = pattern;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TestCase.Builder patternJs(String patternJs) {
|
||||
// Ignore the JavaScript stuff
|
||||
return this;
|
||||
}
|
||||
|
||||
public TestCase.Builder arguments(Map<String, Object> arguments) {
|
||||
this.arguments = arguments;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TestCase.Builder expected(String expected) {
|
||||
this.expected = expected;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TestCase.Builder errors(String ... errors) {
|
||||
this.errors = new ArrayList<>();
|
||||
this.errors.addAll(Arrays.asList(errors));
|
||||
return this;
|
||||
}
|
||||
|
||||
public TestCase.Builder locale(String localeId) {
|
||||
this.localeId = localeId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public TestCase.Builder ignore() {
|
||||
this.ignore = true;
|
||||
this.ignoreReason = "";
|
||||
return this;
|
||||
}
|
||||
|
||||
public TestCase.Builder ignore(String reason) {
|
||||
this.ignore = true;
|
||||
this.ignoreReason = reason;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
// © 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 static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.fail;
|
||||
|
||||
import org.junit.Ignore;
|
||||
|
||||
import com.ibm.icu.message2.MessageFormatter;
|
||||
import com.ibm.icu.message2.Mf2FunctionRegistry;
|
||||
|
||||
@Ignore("Utility class, has no test methods.")
|
||||
/** Utility class, has no test methods. */
|
||||
public class TestUtils {
|
||||
|
||||
static void runTestCase(TestCase testCase) {
|
||||
runTestCase(null, testCase);
|
||||
}
|
||||
|
||||
static void runTestCase(Mf2FunctionRegistry customFunctionsRegistry, TestCase testCase) {
|
||||
if (testCase.ignore) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We can call the "complete" constructor with null values, but we want to test that
|
||||
// all constructors work properly.
|
||||
MessageFormatter.Builder mfBuilder = MessageFormatter.builder()
|
||||
.setPattern(testCase.message)
|
||||
.setLocale(testCase.locale);
|
||||
if (customFunctionsRegistry != null) {
|
||||
mfBuilder.setFunctionRegistry(customFunctionsRegistry);
|
||||
}
|
||||
try { // TODO: expected error
|
||||
MessageFormatter mf = mfBuilder.build();
|
||||
String result = mf.formatToString(testCase.arguments);
|
||||
if (!testCase.errors.isEmpty()) {
|
||||
fail(reportCase(testCase) + "\nExpected error, but it didn't happen.\n"
|
||||
+ "Result: '" + result + "'");
|
||||
} else {
|
||||
assertEquals(reportCase(testCase), testCase.expected, result);
|
||||
}
|
||||
} catch (IllegalArgumentException | NullPointerException e) {
|
||||
if (testCase.errors.isEmpty()) {
|
||||
fail(reportCase(testCase) + "\nNo error was expected here, but it happened:\n"
|
||||
+ e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String reportCase(TestCase testCase) {
|
||||
return testCase.toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
|
||||
<html>
|
||||
<head>
|
||||
<!-- Copyright (C) 2022 and later: Unicode, Inc. and others.
|
||||
License & terms of use: http://www.unicode.org/copyright.html
|
||||
-->
|
||||
</head>
|
||||
<body bgcolor="white">
|
||||
Tests for MessageFormat2.
|
||||
</body>
|
||||
</html>
|
|
@ -39,6 +39,7 @@ import com.ibm.icu.impl.TimeZoneAdapter;
|
|||
import com.ibm.icu.impl.URLHandler;
|
||||
import com.ibm.icu.math.BigDecimal;
|
||||
import com.ibm.icu.math.MathContext;
|
||||
import com.ibm.icu.message2.Mf2DataModel;
|
||||
import com.ibm.icu.util.AnnualTimeZoneRule;
|
||||
import com.ibm.icu.util.Calendar;
|
||||
import com.ibm.icu.util.Currency;
|
||||
|
@ -762,6 +763,28 @@ public class SerializableTestUtility {
|
|||
}
|
||||
}
|
||||
|
||||
private static class Mf2DataModelOrderedMapHandler implements Handler {
|
||||
@Override
|
||||
public Object[] getTestObjects() {
|
||||
Mf2DataModel.OrderedMap<String, Object> mapWithContent = new Mf2DataModel.OrderedMap<>();
|
||||
mapWithContent.put("number", Double.valueOf(3.1416));
|
||||
mapWithContent.put("date", new Date());
|
||||
mapWithContent.put("string", "testing");
|
||||
return new Mf2DataModel.OrderedMap[] {
|
||||
new Mf2DataModel.OrderedMap(),
|
||||
mapWithContent
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSameBehavior(Object a, Object b) {
|
||||
// OrderedMap extends LinkedHashMap, without adding any functionality, nothing to test.
|
||||
Mf2DataModel.OrderedMap ra = (Mf2DataModel.OrderedMap)a;
|
||||
Mf2DataModel.OrderedMap rb = (Mf2DataModel.OrderedMap)b;
|
||||
return ra.equals(rb);
|
||||
}
|
||||
}
|
||||
|
||||
private static HashMap map = new HashMap();
|
||||
|
||||
static {
|
||||
|
@ -858,6 +881,8 @@ public class SerializableTestUtility {
|
|||
map.put("com.ibm.icu.util.ICUUncheckedIOException", new ICUUncheckedIOExceptionHandler());
|
||||
map.put("com.ibm.icu.util.ICUCloneNotSupportedException", new ICUCloneNotSupportedExceptionHandler());
|
||||
map.put("com.ibm.icu.util.ICUInputTooLongException", new ICUInputTooLongExceptionHandler());
|
||||
|
||||
map.put("com.ibm.icu.message2.Mf2DataModel$OrderedMap", new Mf2DataModelOrderedMapHandler());
|
||||
}
|
||||
|
||||
/*
|
||||
|
|
Loading…
Add table
Reference in a new issue