diff --git a/src/java.base/share/classes/java/text/DateFormat.java b/src/java.base/share/classes/java/text/DateFormat.java index 335fe1105a6..d7a4b5a39b7 100644 --- a/src/java.base/share/classes/java/text/DateFormat.java +++ b/src/java.base/share/classes/java/text/DateFormat.java @@ -482,6 +482,31 @@ public abstract class DateFormat extends Format { */ public static final int DEFAULT = MEDIUM; + /** + * A DateFormat style. + * {@code Style} is an enum which corresponds to the DateFormat style + * constants. Use {@code getValue()} to retrieve the associated int style + * value. + */ + enum Style { + + FULL(DateFormat.FULL), + LONG(DateFormat.LONG), + MEDIUM(DateFormat.MEDIUM), + SHORT(DateFormat.SHORT), + DEFAULT(DateFormat.MEDIUM); + + private final int value; + + Style(int value){ + this.value = value; + } + + int getValue() { + return value; + } + } + /** * Gets the time formatter with the default formatting style * for the default {@link java.util.Locale.Category#FORMAT FORMAT} locale. diff --git a/src/java.base/share/classes/java/text/MessageFormat.java b/src/java.base/share/classes/java/text/MessageFormat.java index c309801bf0c..8f939f8af26 100644 --- a/src/java.base/share/classes/java/text/MessageFormat.java +++ b/src/java.base/share/classes/java/text/MessageFormat.java @@ -38,10 +38,10 @@ package java.text; -import java.io.InvalidObjectException; import java.io.IOException; +import java.io.InvalidObjectException; import java.io.ObjectInputStream; -import java.text.DecimalFormat; +import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; @@ -52,7 +52,7 @@ import java.util.Objects; /** * {@code MessageFormat} provides a means to produce concatenated - * messages in a language-neutral way. Use this to construct messages + * messages in a language-neutral way. Use this class to construct messages * displayed for end users. * *

@@ -82,8 +82,16 @@ import java.util.Objects; * { ArgumentIndex , FormatType } * { ArgumentIndex , FormatType , FormatStyle } * - * FormatType: one of - * number date time choice + * FormatType: + * number + * dtf_date + * dtf_time + * dtf_datetime + * pre-defined DateTimeFormatter(s) + * date + * time + * choice + * list * * FormatStyle: * short @@ -93,9 +101,230 @@ import java.util.Objects; * integer * currency * percent + * compact_short + * compact_long + * or + * unit * SubformatPattern * * + *

+ * The ArgumentIndex value is a non-negative integer written + * using the digits {@code '0'} through {@code '9'}, and represents an index into the + * {@code arguments} array passed to the {@code format} methods + * or the result array returned by the {@code parse} methods. + *

+ * The FormatType and FormatStyle values are used to create + * a {@code Format} instance for the format element. The following + * table shows how the values map to {@code Format} instances. These values + * are case-insensitive when passed to {@link #applyPattern(String)}. Combinations + * not shown in the table are illegal. A SubformatPattern must + * be a valid pattern string for the {@code Format} subclass used. + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
Shows how FormatType and FormatStyle values map to Format instances
FormatType + * FormatStyle + * Subformat Created + *
(none) + * (none) + * {@code null} + *
{@code number} + * (none) + * {@link NumberFormat#getInstance(Locale) NumberFormat.getInstance}{@code (getLocale())} + *
{@code integer} + * {@link NumberFormat#getIntegerInstance(Locale) NumberFormat.getIntegerInstance}{@code (getLocale())} + *
{@code currency} + * {@link NumberFormat#getCurrencyInstance(Locale) NumberFormat.getCurrencyInstance}{@code (getLocale())} + *
{@code percent} + * {@link NumberFormat#getPercentInstance(Locale) NumberFormat.getPercentInstance}{@code (getLocale())} + *
{@code compact_short} + * {@link NumberFormat#getCompactNumberInstance(Locale, NumberFormat.Style) + * NumberFormat.getCompactNumberInstance}{@code (getLocale(),} {@link NumberFormat.Style#SHORT}) + *
{@code compact_long} + * {@link NumberFormat#getCompactNumberInstance(Locale, NumberFormat.Style) + * NumberFormat.getCompactNumberInstance}{@code (getLocale(),} {@link NumberFormat.Style#LONG}) + *
SubformatPattern + * {@code new} {@link DecimalFormat#DecimalFormat(String,DecimalFormatSymbols) + * DecimalFormat}{@code (subformatPattern,} {@link DecimalFormatSymbols#getInstance(Locale) + * DecimalFormatSymbols.getInstance}{@code (getLocale()))} + *
{@code dtf_date} + * (none) + * {@link DateTimeFormatter#ofLocalizedDate(java.time.format.FormatStyle) + * DateTimeFormatter.ofLocalizedDate(}{@link java.time.format.FormatStyle#MEDIUM}{@code ).withLocale(getLocale())} + *
{@code short} + * {@link DateTimeFormatter#ofLocalizedDate(java.time.format.FormatStyle) + * DateTimeFormatter.ofLocalizedDate(}{@link java.time.format.FormatStyle#SHORT}{@code ).withLocale(getLocale())} + *
{@code medium} + * {@link DateTimeFormatter#ofLocalizedDate(java.time.format.FormatStyle) + * DateTimeFormatter.ofLocalizedDate(}{@link java.time.format.FormatStyle#MEDIUM}{@code ).withLocale(getLocale())} + *
{@code long} + * {@link DateTimeFormatter#ofLocalizedDate(java.time.format.FormatStyle) + * DateTimeFormatter.ofLocalizedDate(}{@link java.time.format.FormatStyle#LONG}{@code ).withLocale(getLocale())} + *
{@code full} + * {@link DateTimeFormatter#ofLocalizedDate(java.time.format.FormatStyle) + * DateTimeFormatter.ofLocalizedDate(}{@link java.time.format.FormatStyle#FULL}{@code ).withLocale(getLocale())} + *
SubformatPattern + * {@link DateTimeFormatter#ofPattern(String, Locale) + * DateTimeFormatter.ofPattern}{@code (subformatPattern, getLocale())} + *
{@code dtf_time} + * (none) + * {@link DateTimeFormatter#ofLocalizedTime(java.time.format.FormatStyle) + * DateTimeFormatter.ofLocalizedTime(}{@link java.time.format.FormatStyle#MEDIUM}{@code ).withLocale(getLocale())} + *
{@code short} + * {@link DateTimeFormatter#ofLocalizedTime(java.time.format.FormatStyle) + * DateTimeFormatter.ofLocalizedTime(}{@link java.time.format.FormatStyle#SHORT}{@code ).withLocale(getLocale())} + *
{@code medium} + * {@link DateTimeFormatter#ofLocalizedTime(java.time.format.FormatStyle) + * DateTimeFormatter.ofLocalizedTime(}{@link java.time.format.FormatStyle#MEDIUM}{@code ).withLocale(getLocale())} + *
{@code long} + * {@link DateTimeFormatter#ofLocalizedTime(java.time.format.FormatStyle) + * DateTimeFormatter.ofLocalizedTime(}{@link java.time.format.FormatStyle#LONG}{@code ).withLocale(getLocale())} + *
{@code full} + * {@link DateTimeFormatter#ofLocalizedTime(java.time.format.FormatStyle) + * DateTimeFormatter.ofLocalizedTime(}{@link java.time.format.FormatStyle#FULL}{@code ).withLocale(getLocale())} + *
SubformatPattern + * {@link DateTimeFormatter#ofPattern(String, Locale) DateTimeFormatter.ofPattern}{@code (subformatPattern, getLocale())} + *
{@code dtf_datetime} + * (none) + * {@link DateTimeFormatter#ofLocalizedDateTime(java.time.format.FormatStyle) + * DateTimeFormatter.ofLocalizedDateTime(}{@link java.time.format.FormatStyle#MEDIUM}{@code ).withLocale(getLocale())} + *
{@code short} + * {@link DateTimeFormatter#ofLocalizedDateTime(java.time.format.FormatStyle) + * DateTimeFormatter.ofLocalizedDateTime(}{@link java.time.format.FormatStyle#SHORT}{@code ).withLocale(getLocale())} + *
{@code medium} + * {@link DateTimeFormatter#ofLocalizedDateTime(java.time.format.FormatStyle) + * DateTimeFormatter.ofLocalizedDateTime(}{@link java.time.format.FormatStyle#MEDIUM}{@code ).withLocale(getLocale())} + *
{@code long} + * {@link DateTimeFormatter#ofLocalizedDateTime(java.time.format.FormatStyle) + * DateTimeFormatter.ofLocalizedDateTime(}{@link java.time.format.FormatStyle#LONG}{@code ).withLocale(getLocale())} + *
{@code full} + * {@link DateTimeFormatter#ofLocalizedDateTime(java.time.format.FormatStyle) + * DateTimeFormatter.ofLocalizedDateTime(}{@link java.time.format.FormatStyle#FULL}{@code ).withLocale(getLocale())} + *
SubformatPattern + * {@link DateTimeFormatter#ofPattern(String, Locale) + * DateTimeFormatter.ofPattern}{@code (subformatPattern, getLocale())} + *
{@code pre-defined DateTimeFormatter(s)} + * (none) + * The {@code pre-defined DateTimeFormatter(s)} are used as a {@code FormatType} : + * {@link DateTimeFormatter#BASIC_ISO_DATE BASIC_ISO_DATE}, + * {@link DateTimeFormatter#ISO_LOCAL_DATE ISO_LOCAL_DATE}, + * {@link DateTimeFormatter#ISO_OFFSET_DATE ISO_OFFSET_DATE}, + * {@link DateTimeFormatter#ISO_DATE ISO_DATE}, + * {@link DateTimeFormatter#ISO_LOCAL_TIME ISO_LOCAL_TIME}, + * {@link DateTimeFormatter#ISO_OFFSET_TIME ISO_OFFSET_TIME}, + * {@link DateTimeFormatter#ISO_TIME ISO_TIME}, + * {@link DateTimeFormatter#ISO_LOCAL_DATE_TIME ISO_LOCAL_DATE_TIME}, + * {@link DateTimeFormatter#ISO_OFFSET_DATE_TIME ISO_OFFSET_DATE_TIME}, + * {@link DateTimeFormatter#ISO_ZONED_DATE_TIME ISO_ZONED_DATE_TIME}, + * {@link DateTimeFormatter#ISO_DATE_TIME ISO_DATE_TIME}, + * {@link DateTimeFormatter#ISO_ORDINAL_DATE ISO_ORDINAL_DATE}, + * {@link DateTimeFormatter#ISO_WEEK_DATE ISO_WEEK_DATE}, + * {@link DateTimeFormatter#ISO_INSTANT ISO_INSTANT}, + * {@link DateTimeFormatter#RFC_1123_DATE_TIME RFC_1123_DATE_TIME} + *
{@code date} + * (none) + * {@link DateFormat#getDateInstance(int,Locale) + * DateFormat.getDateInstance}{@code (}{@link DateFormat#DEFAULT}{@code , getLocale())} + *
{@code short} + * {@link DateFormat#getDateInstance(int,Locale) + * DateFormat.getDateInstance}{@code (}{@link DateFormat#SHORT}{@code , getLocale())} + *
{@code medium} + * {@link DateFormat#getDateInstance(int,Locale) + * DateFormat.getDateInstance}{@code (}{@link DateFormat#MEDIUM}{@code , getLocale())} + *
{@code long} + * {@link DateFormat#getDateInstance(int,Locale) + * DateFormat.getDateInstance}{@code (}{@link DateFormat#LONG}{@code , getLocale())} + *
{@code full} + * {@link DateFormat#getDateInstance(int,Locale) + * DateFormat.getDateInstance}{@code (}{@link DateFormat#FULL}{@code , getLocale())} + *
SubformatPattern + * {@code new} {@link SimpleDateFormat#SimpleDateFormat(String,Locale) + * SimpleDateFormat}{@code (subformatPattern, getLocale())} + *
{@code time} + * (none) + * {@link DateFormat#getTimeInstance(int,Locale) + * DateFormat.getTimeInstance}{@code (}{@link DateFormat#DEFAULT}{@code , getLocale())} + *
{@code short} + * {@link DateFormat#getTimeInstance(int,Locale) + * DateFormat.getTimeInstance}{@code (}{@link DateFormat#SHORT}{@code , getLocale())} + *
{@code medium} + * {@link DateFormat#getTimeInstance(int,Locale) + * DateFormat.getTimeInstance}{@code (}{@link DateFormat#MEDIUM}{@code , getLocale())} + *
{@code long} + * {@link DateFormat#getTimeInstance(int,Locale) + * DateFormat.getTimeInstance}{@code (}{@link DateFormat#LONG}{@code , getLocale())} + *
{@code full} + * {@link DateFormat#getTimeInstance(int,Locale) + * DateFormat.getTimeInstance}{@code (}{@link DateFormat#FULL}{@code , getLocale())} + *
SubformatPattern + * {@code new} {@link SimpleDateFormat#SimpleDateFormat(String,Locale) + * SimpleDateFormat}{@code (subformatPattern, getLocale())} + *
{@code choice} + * SubformatPattern + * {@code new} {@link ChoiceFormat#ChoiceFormat(String) ChoiceFormat}{@code (subformatPattern)} + *
{@code list} + * (none) + * {@link ListFormat#getInstance(Locale, ListFormat.Type, ListFormat.Style) + * ListFormat.getInstance}{@code (getLocale()}, {@link ListFormat.Type#STANDARD}, {@link ListFormat.Style#FULL}) + *
{@code or} + * {@link ListFormat#getInstance(Locale, ListFormat.Type, ListFormat.Style) + * ListFormat.getInstance}{@code (getLocale()}, {@link ListFormat.Type#OR}, {@link ListFormat.Style#FULL}) + *
{@code unit} + * {@link ListFormat#getInstance(Locale, ListFormat.Type, ListFormat.Style) + * ListFormat.getInstance}{@code (getLocale()}, {@link ListFormat.Type#UNIT}, {@link ListFormat.Style#FULL}} + *
+ * + *

Quoting Rules in Patterns

+ * *

Within a String, a pair of single quotes can be used to * quote any arbitrary characters except single quotes. For example, * pattern string "'{0}'" represents string @@ -135,148 +364,38 @@ import java.util.Objects; * Note that localizers may need to use single quotes in translated * strings where the original version doesn't have them. * - *

- * The ArgumentIndex value is a non-negative integer written - * using the digits {@code '0'} through {@code '9'}, and represents an index into the - * {@code arguments} array passed to the {@code format} methods - * or the result array returned by the {@code parse} methods. - *

- * The FormatType and FormatStyle values are used to create - * a {@code Format} instance for the format element. The following - * table shows how the values map to {@code Format} instances. Combinations not - * shown in the table are illegal. A SubformatPattern must - * be a valid pattern string for the {@code Format} subclass used. * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
Shows how FormatType and FormatStyle values map to Format instances
FormatType - * FormatStyle - * Subformat Created - *
(none) - * (none) - * {@code null} - *
{@code number} - * (none) - * {@link NumberFormat#getInstance(Locale) NumberFormat.getInstance}{@code (getLocale())} - *
{@code integer} - * {@link NumberFormat#getIntegerInstance(Locale) NumberFormat.getIntegerInstance}{@code (getLocale())} - *
{@code currency} - * {@link NumberFormat#getCurrencyInstance(Locale) NumberFormat.getCurrencyInstance}{@code (getLocale())} - *
{@code percent} - * {@link NumberFormat#getPercentInstance(Locale) NumberFormat.getPercentInstance}{@code (getLocale())} - *
SubformatPattern - * {@code new} {@link DecimalFormat#DecimalFormat(String,DecimalFormatSymbols) DecimalFormat}{@code (subformatPattern,} {@link DecimalFormatSymbols#getInstance(Locale) DecimalFormatSymbols.getInstance}{@code (getLocale()))} - *
{@code date} - * (none) - * {@link DateFormat#getDateInstance(int,Locale) DateFormat.getDateInstance}{@code (}{@link DateFormat#DEFAULT}{@code , getLocale())} - *
{@code short} - * {@link DateFormat#getDateInstance(int,Locale) DateFormat.getDateInstance}{@code (}{@link DateFormat#SHORT}{@code , getLocale())} - *
{@code medium} - * {@link DateFormat#getDateInstance(int,Locale) DateFormat.getDateInstance}{@code (}{@link DateFormat#DEFAULT}{@code , getLocale())} - *
{@code long} - * {@link DateFormat#getDateInstance(int,Locale) DateFormat.getDateInstance}{@code (}{@link DateFormat#LONG}{@code , getLocale())} - *
{@code full} - * {@link DateFormat#getDateInstance(int,Locale) DateFormat.getDateInstance}{@code (}{@link DateFormat#FULL}{@code , getLocale())} - *
SubformatPattern - * {@code new} {@link SimpleDateFormat#SimpleDateFormat(String,Locale) SimpleDateFormat}{@code (subformatPattern, getLocale())} - *
{@code time} - * (none) - * {@link DateFormat#getTimeInstance(int,Locale) DateFormat.getTimeInstance}{@code (}{@link DateFormat#DEFAULT}{@code , getLocale())} - *
{@code short} - * {@link DateFormat#getTimeInstance(int,Locale) DateFormat.getTimeInstance}{@code (}{@link DateFormat#SHORT}{@code , getLocale())} - *
{@code medium} - * {@link DateFormat#getTimeInstance(int,Locale) DateFormat.getTimeInstance}{@code (}{@link DateFormat#DEFAULT}{@code , getLocale())} - *
{@code long} - * {@link DateFormat#getTimeInstance(int,Locale) DateFormat.getTimeInstance}{@code (}{@link DateFormat#LONG}{@code , getLocale())} - *
{@code full} - * {@link DateFormat#getTimeInstance(int,Locale) DateFormat.getTimeInstance}{@code (}{@link DateFormat#FULL}{@code , getLocale())} - *
SubformatPattern - * {@code new} {@link SimpleDateFormat#SimpleDateFormat(String,Locale) SimpleDateFormat}{@code (subformatPattern, getLocale())} - *
{@code choice} - * SubformatPattern - * {@code new} {@link ChoiceFormat#ChoiceFormat(String) ChoiceFormat}{@code (subformatPattern)} - *
- * - *

Usage Information

+ *

Usage Information

* *

- * Here are some examples of usage. - * In real internationalized programs, the message format pattern and other - * static strings will, of course, be obtained from resource bundles. - * Other parameters will be dynamically determined at runtime. + * The following example demonstrates a general usage of {@code MessageFormat}. + * In internationalized programs, the message format pattern and other + * static strings will likely be obtained from resource bundles. + * *

- * The first example uses the static method {@code MessageFormat.format}, - * which internally creates a {@code MessageFormat} for one-time use: * {@snippet lang=java : * int planet = 7; * String event = "a disturbance in the Force"; - * * String result = MessageFormat.format( * "At {1,time} on {1,date}, there was {2} on planet {0,number,integer}.", - * planet, new Date(), event); + * planet, new GregorianCalendar(2053, Calendar.JULY, 3, 12, 30).getTime(), event); * } - * The output is: + * + * {@code result} returns the following: *

- * At 12:30 PM on Jul 3, 2053, there was a disturbance in the Force on planet 7.
- * 
- * - *

- * The following example creates a {@code MessageFormat} instance that - * can be used repeatedly: - * {@snippet lang=java : - * int fileCount = 1273; - * String diskName = "MyDisk"; - * Object[] testArgs = {Long.valueOf(fileCount), diskName}; - * - * MessageFormat form = new MessageFormat( - * "The disk \"{1}\" contains {0} file(s)."); - * - * System.out.println(form.format(testArgs)); - * } - * The output with different values for {@code fileCount}: - *

- * The disk "MyDisk" contains 0 file(s).
- * The disk "MyDisk" contains 1 file(s).
- * The disk "MyDisk" contains 1,273 file(s).
+ * At 12:30:00 PM on Jul 3, 2053, there was a disturbance in the Force on planet 7.
  * 
* *

* For more sophisticated patterns, {@link ChoiceFormat} can be used with * {@code MessageFormat} to produce accurate forms for singular and plural: * {@snippet lang=java : - * MessageFormat msgFmt = new MessageFormat("The disk \"{0}\" contains {1}."); - * double[] fileLimits = {0,1,2}; - * String[] filePart = {"no files","one file","{1,number} files"}; - * ChoiceFormat fileChoices = new ChoiceFormat(fileLimits, filePart); - * msgFmt.setFormatByArgumentIndex(1, fileChoices); - * Object[] args = {"MyDisk", 1273}; - * System.out.println(msgFmt.format(args)); + * MessageFormat msgFmt = new MessageFormat("The disk \"{0}\" contains {1,choice,0#no files|1#one file|1< {1,number,integer} files}."); + * Object[] args = {"MyDisk", fileCount}; + * String result = msgFmt.format(args); * } - * The output with different values for {@code fileCount}: + * + * {@code result} with different values for {@code fileCount}, returns the following: *

  * The disk "MyDisk" contains no files.
  * The disk "MyDisk" contains one file.
@@ -284,15 +403,6 @@ import java.util.Objects;
  * 
* *

- * You can create the {@code ChoiceFormat} programmatically, as in the - * above example, or by using a pattern. See {@link ChoiceFormat} - * for more information. - * {@snippet lang=java : - * msgFmt.applyPattern( - * "There {0,choice,0#are no files|1#is one file|1 * Notes: As seen in the previous snippet, * the string produced by a {@code ChoiceFormat} in {@code MessageFormat} is * treated as special; occurrences of '{' are used to indicate subformats, and @@ -304,6 +414,35 @@ import java.util.Objects; * If you create both a {@code MessageFormat} and {@code ChoiceFormat} * programmatically (instead of using the string patterns), then be careful not to * produce a format that recurses on itself, which will cause an infinite loop. + * + *

Formatting Date and Time

+ * + * MessageFormat provides patterns that support the date/time formatters in the + * {@link java.time.format} and {@link java.text} packages. Consider the following three examples, + * with a date of 11/16/2023: + * + *

1) a date {@code FormatType} with a full {@code FormatStyle}, + * {@snippet lang=java : + * Object[] arg = {new GregorianCalendar(2023, Calendar.NOVEMBER, 16).getTime()}; + * var fmt = new MessageFormat("The date was {0,date,full}"); + * fmt.format(arg); // returns "The date was Thursday, November 16, 2023" + * } + * + *

2) a dtf_date {@code FormatType} with a full {@code FormatStyle}, + * {@snippet lang=java : + * Object[] arg = {LocalDate.of(2023, 11, 16)}; + * var fmt = new MessageFormat("The date was {0,dtf_date,full}"); + * fmt.format(arg); // returns "The date was Thursday, November 16, 2023" + * } + * + *

3) an ISO_LOCAL_DATE {@code FormatType}, + * {@snippet lang=java : + * Object[] arg = {LocalDate.of(2023, 11, 16)}; + * var fmt = new MessageFormat("The date was {0,ISO_LOCAL_DATE}"); + * fmt.format(arg); // returns "The date was 2023-11-16" + * } + * + *

Parsing

*

* When a single argument is parsed more than once in the string, the last match * will be the final result of the parsing. For example, @@ -343,6 +482,7 @@ import java.util.Objects; * @see ChoiceFormat * @see DateFormat * @see SimpleDateFormat + * @see DateTimeFormatter * * @author Mark Davis * @since 1.1 @@ -513,7 +653,7 @@ public class MessageFormat extends Format { if (braceStack == 0) { part = SEG_RAW; // Set the subformat - makeFormat(i, formatNumber, segments); + setFormatFromPattern(i, formatNumber, segments); formatNumber++; // throw away other segments segments[SEG_INDEX] = null; @@ -549,16 +689,18 @@ public class MessageFormat extends Format { /** - * Returns a pattern representing the current state of the message format. + * {@return a String pattern adhering to the {@link ##patterns patterns section} that + * represents the current state of this {@code MessageFormat}} + * * The string is constructed from internal information and therefore * does not necessarily equal the previously applied pattern. * * @implSpec The implementation in {@link MessageFormat} returns a * string that, when passed to a {@code MessageFormat()} constructor * or {@link #applyPattern applyPattern()}, produces an instance that - * is semantically equivalent to this instance. - * - * @return a pattern representing the current state of the message format + * is semantically equivalent to this instance. If a subformat cannot be + * converted to a String pattern, the {@code FormatType} and {@code FormatStyle} + * will be omitted from the {@code FormatElement}. */ public String toPattern() { // later, make this more extensible @@ -567,74 +709,79 @@ public class MessageFormat extends Format { for (int i = 0; i <= maxOffset; ++i) { copyAndFixQuotes(pattern, lastOffset, offsets[i], result); lastOffset = offsets[i]; - result.append('{').append(argumentNumbers[i]); - Format fmt = formats[i]; - String subformatPattern = null; - if (fmt == null) { - // do nothing, string format - } else if (fmt instanceof NumberFormat) { - if (fmt.equals(NumberFormat.getInstance(locale))) { - result.append(",number"); - } else if (fmt.equals(NumberFormat.getCurrencyInstance(locale))) { - result.append(",number,currency"); - } else if (fmt.equals(NumberFormat.getPercentInstance(locale))) { - result.append(",number,percent"); - } else if (fmt.equals(NumberFormat.getIntegerInstance(locale))) { - result.append(",number,integer"); - } else { - if (fmt instanceof DecimalFormat dfmt) { - result.append(",number"); - subformatPattern = dfmt.toPattern(); - } else if (fmt instanceof ChoiceFormat cfmt) { - result.append(",choice"); - subformatPattern = cfmt.toPattern(); - } else { - // UNKNOWN - } - } - } else if (fmt instanceof DateFormat) { - int index; - for (index = MODIFIER_DEFAULT; index < DATE_TIME_MODIFIERS.length; index++) { - DateFormat df = DateFormat.getDateInstance(DATE_TIME_MODIFIERS[index], - locale); - if (fmt.equals(df)) { - result.append(",date"); - break; - } - df = DateFormat.getTimeInstance(DATE_TIME_MODIFIERS[index], - locale); - if (fmt.equals(df)) { - result.append(",time"); - break; - } - } - if (index >= DATE_TIME_MODIFIERS.length) { - if (fmt instanceof SimpleDateFormat sdfmt) { - result.append(",date"); - subformatPattern = sdfmt.toPattern(); - } else { - // UNKNOWN - } - } else if (index != MODIFIER_DEFAULT) { - result.append(',').append(DATE_TIME_MODIFIER_KEYWORDS[index]); - } - } else { - //result.append(", unknown"); - } - if (subformatPattern != null) { - result.append(','); - - // The subformat pattern comes already quoted, but only for those characters that are - // special to the subformat. Therefore, we may need to quote additional characters. - // The ones we care about at the MessageFormat level are '{' and '}'. - copyAndQuoteBraces(subformatPattern, result); - } - result.append('}'); + result.append('{') + .append(argumentNumbers[i]) + .append(patternFromFormat(formats[i])) + .append('}'); } copyAndFixQuotes(pattern, lastOffset, pattern.length(), result); return result.toString(); } + /** + * This method converts a Format into a {@code FormatType} and {@code + * FormatStyle}, if applicable. For each Format, this method will + * first check against the pre-defined styles established in the + * {@link ##patterns patterns section}. Any "default"/"medium" styles + * are omitted according to the specification. + * If a Format does not match to a pre-defined style, it will provide the + * {@code SubformatPattern}, if the Format class can provide one. The + * following subformats do not provide a {@code SubformatPattern}: + * CompactNumberFormat, ListFormat, and DateTimeFormatter (ClassicFormat). + * + *

In addition, since DateTimeFormatter and ClassicFormat do not implement {@code equals()}, + * there is not a means to compare {@code fmt} to a ClassicFormat for equality, + * and thus we don't have enough info to represent it as a pattern since there is no way to check + * if {@code fmt} is equal to some, (for example, "long" style) pre-defined ClassicFormat. + * Even if ClassicFormat implemented equals(), it is a wrapper class for + * DateTimeFormatter, which would require DTF to implement equals() as well to effectively + * compare the two ClassicFormats. + */ + private String patternFromFormat(Format fmt) { + if (fmt instanceof NumberFormat nFmt) { + // Check nFmt factory instances + String nStyle = NumberFormat.matchToStyle(nFmt, locale); + if (nStyle != null) { + return ",number" + (nStyle.isEmpty() ? nStyle : "," + nStyle); + } + // Check SubformatPattern + if (fmt instanceof DecimalFormat dFmt) { + // Quote eligible mFmt pattern characters: '{' and '}' + // Here, and in other subformatPattern instances + return ",number," + copyAndQuoteBraces(dFmt.toPattern()); + } else if (fmt instanceof ChoiceFormat cFmt) { + return ",choice," + copyAndQuoteBraces(cFmt.toPattern()); + } + } else if (fmt instanceof DateFormat) { + // Check dFmt factory instances + for (DateFormat.Style style : DateFormat.Style.values()) { + if (fmt.equals(DateFormat.getDateInstance(style.getValue(), locale))) { + return ",date" + ((style.getValue() != DateFormat.DEFAULT) + ? "," + style.name().toLowerCase(Locale.ROOT) : ""); + } + if (fmt.equals(DateFormat.getTimeInstance(style.getValue(), locale))) { + return ",time" + ((style.getValue() != DateFormat.DEFAULT) + ? "," + style.name().toLowerCase(Locale.ROOT) : ""); + } + } + // Check SubformatPattern + if (fmt instanceof SimpleDateFormat sdFmt) { + return ",date," + copyAndQuoteBraces(sdFmt.toPattern()); + } + } else if (fmt instanceof ListFormat) { + // Check lFmt factory instances + for (ListFormat.Type type : ListFormat.Type.values()) { + if (fmt.equals(ListFormat.getInstance(locale, type, ListFormat.Style.FULL))) { + return ",list" + ((type != ListFormat.Type.STANDARD) + ? "," + type.name().toLowerCase(Locale.ROOT) : ""); + } + } + } + // By here, this is an instanceof Format that is unknown to MessageFormat. + // Since it is unknown, nothing can be done. + return ""; + } + /** * Sets the formats to use for the values passed into * {@code format} methods or returned from {@code parse} @@ -694,9 +841,8 @@ public class MessageFormat extends Format { if (runsToCopy > maxOffset + 1) { runsToCopy = maxOffset + 1; } - for (int i = 0; i < runsToCopy; i++) { - formats[i] = newFormats[i]; - } + if (runsToCopy >= 0) + System.arraycopy(newFormats, 0, formats, 0, runsToCopy); } /** @@ -1063,7 +1209,7 @@ public class MessageFormat extends Format { return null; // leave index as is to signal error } else { String strValue= source.substring(sourceOffset,next); - if (!strValue.equals("{"+argumentNumbers[i]+"}")) + if (!strValue.equals("{" + argumentNumbers[i] + "}")) resultArray[argumentNumbers[i]] = source.substring(sourceOffset,next); sourceOffset = next; @@ -1450,65 +1596,21 @@ public class MessageFormat extends Format { } // Indices for segments - private static final int SEG_RAW = 0; - private static final int SEG_INDEX = 1; - private static final int SEG_TYPE = 2; - private static final int SEG_MODIFIER = 3; // modifier or subformat + private static final int SEG_RAW = 0; // String in MessageFormatPattern + private static final int SEG_INDEX = 1; // ArgumentIndex + private static final int SEG_TYPE = 2; // FormatType + private static final int SEG_MODIFIER = 3; // FormatStyle - // Indices for type keywords - private static final int TYPE_NULL = 0; - private static final int TYPE_NUMBER = 1; - private static final int TYPE_DATE = 2; - private static final int TYPE_TIME = 3; - private static final int TYPE_CHOICE = 4; + /** + * This method sets a Format in the {@code formats} array for the + * corresponding {@code argumentNumber} based on the pattern supplied. + * If the pattern supplied does not contain a {@code FormatType}, null + * is stored in the {@code formats} array. + */ + private void setFormatFromPattern(int position, int offsetNumber, + StringBuilder[] textSegments) { - private static final String[] TYPE_KEYWORDS = { - "", - "number", - "date", - "time", - "choice" - }; - - // Indices for number modifiers - private static final int MODIFIER_DEFAULT = 0; // common in number and date-time - private static final int MODIFIER_CURRENCY = 1; - private static final int MODIFIER_PERCENT = 2; - private static final int MODIFIER_INTEGER = 3; - - private static final String[] NUMBER_MODIFIER_KEYWORDS = { - "", - "currency", - "percent", - "integer" - }; - - // Indices for date-time modifiers - private static final int MODIFIER_SHORT = 1; - private static final int MODIFIER_MEDIUM = 2; - private static final int MODIFIER_LONG = 3; - private static final int MODIFIER_FULL = 4; - - private static final String[] DATE_TIME_MODIFIER_KEYWORDS = { - "", - "short", - "medium", - "long", - "full" - }; - - // Date-time style values corresponding to the date-time modifiers. - private static final int[] DATE_TIME_MODIFIERS = { - DateFormat.DEFAULT, - DateFormat.SHORT, - DateFormat.MEDIUM, - DateFormat.LONG, - DateFormat.FULL, - }; - - private void makeFormat(int position, int offsetNumber, - StringBuilder[] textSegments) - { + // Convert any null values in textSegments to empty string String[] segments = new String[textSegments.length]; for (int i = 0; i < textSegments.length; i++) { StringBuilder oneseg = textSegments[i]; @@ -1541,104 +1643,205 @@ public class MessageFormat extends Format { offsets = newOffsets; argumentNumbers = newArgumentNumbers; } + int oldMaxOffset = maxOffset; maxOffset = offsetNumber; offsets[offsetNumber] = segments[SEG_RAW].length(); argumentNumbers[offsetNumber] = argumentNumber; - // now get the format - Format newFormat = null; + // Only search for corresponding type/style if type is not empty if (!segments[SEG_TYPE].isEmpty()) { - int type = findKeyword(segments[SEG_TYPE], TYPE_KEYWORDS); - switch (type) { - case TYPE_NULL: - // Type "" is allowed. e.g., "{0,}", "{0,,}", and "{0,,#}" - // are treated as "{0}". - break; - - case TYPE_NUMBER: - switch (findKeyword(segments[SEG_MODIFIER], NUMBER_MODIFIER_KEYWORDS)) { - case MODIFIER_DEFAULT: - newFormat = NumberFormat.getInstance(locale); - break; - case MODIFIER_CURRENCY: - newFormat = NumberFormat.getCurrencyInstance(locale); - break; - case MODIFIER_PERCENT: - newFormat = NumberFormat.getPercentInstance(locale); - break; - case MODIFIER_INTEGER: - newFormat = NumberFormat.getIntegerInstance(locale); - break; - default: // DecimalFormat pattern - try { - newFormat = new DecimalFormat(segments[SEG_MODIFIER], - DecimalFormatSymbols.getInstance(locale)); - } catch (IllegalArgumentException e) { - maxOffset = oldMaxOffset; - throw e; - } - break; - } - break; - - case TYPE_DATE: - case TYPE_TIME: - int mod = findKeyword(segments[SEG_MODIFIER], DATE_TIME_MODIFIER_KEYWORDS); - if (mod >= 0 && mod < DATE_TIME_MODIFIER_KEYWORDS.length) { - if (type == TYPE_DATE) { - newFormat = DateFormat.getDateInstance(DATE_TIME_MODIFIERS[mod], - locale); - } else { - newFormat = DateFormat.getTimeInstance(DATE_TIME_MODIFIERS[mod], - locale); - } - } else { - // SimpleDateFormat pattern - try { - newFormat = new SimpleDateFormat(segments[SEG_MODIFIER], locale); - } catch (IllegalArgumentException e) { - maxOffset = oldMaxOffset; - throw e; - } - } - break; - - case TYPE_CHOICE: - try { - // ChoiceFormat pattern - newFormat = new ChoiceFormat(segments[SEG_MODIFIER]); - } catch (Exception e) { - maxOffset = oldMaxOffset; - throw new IllegalArgumentException("Choice Pattern incorrect: " - + segments[SEG_MODIFIER], e); - } - break; - - default: + try { + formats[offsetNumber] = formatFromPattern(segments[SEG_TYPE], segments[SEG_MODIFIER]); + } catch (Exception e) { + // Catch to reset maxOffset maxOffset = oldMaxOffset; - throw new IllegalArgumentException("unknown format type: " + - segments[SEG_TYPE]); + throw e; } + } else { + // Type "" is allowed. e.g., "{0,}", "{0,,}", and "{0,,#}" + // are treated as "{0}". + formats[offsetNumber] = null; } - formats[offsetNumber] = newFormat; } - private static int findKeyword(String s, String[] list) { - for (int i = 0; i < list.length; ++i) { - if (s.equals(list[i])) - return i; + /** + * This method converts a {@code FormatType} and {@code FormatStyle} to a + * {@code Format} value. The String parameters are converted + * to their corresponding enum values FormatType and FormatStyle which are used + * to return a {@code Format}. See the patterns section in the class + * description for further detail on a MessageFormat pattern. + * + * @param type the {@code FormatType} in {@code FormatElement} + * @param style the {@code FormatStyle} in {@code FormatElement} + * @return a Format that corresponds to the corresponding {@code formatType} + * and {@code formatStyle} + * @throws IllegalArgumentException if a Format cannot be produced from the + * type and style provided + */ + private Format formatFromPattern(String type, String style) { + // Get the type, if it's valid + FormatType fType; + try { + fType = FormatType.valueOf(type.trim().toUpperCase(Locale.ROOT)); + } catch (IllegalArgumentException iae) { + // Invalid type throws exception + throw new IllegalArgumentException("unknown format type: " + type); } + // Get the style if recognized, otherwise treat style as a SubformatPattern + FormatStyle fStyle; + try { + fStyle = FormatStyle.fromString(style); + } catch (IllegalArgumentException iae) { + fStyle = FormatStyle.SUBFORMATPATTERN; + } + return switch (fType) { + case NUMBER -> switch (fStyle) { + case DEFAULT -> NumberFormat.getInstance(locale); + case CURRENCY -> + NumberFormat.getCurrencyInstance(locale); + case PERCENT -> + NumberFormat.getPercentInstance(locale); + case INTEGER -> + NumberFormat.getIntegerInstance(locale); + case COMPACT_SHORT -> + NumberFormat.getCompactNumberInstance(locale, NumberFormat.Style.SHORT); + case COMPACT_LONG -> + NumberFormat.getCompactNumberInstance(locale, NumberFormat.Style.LONG); + default -> formatFromSubformatPattern(fType, style); + }; + case DATE -> switch (fStyle) { + case DEFAULT -> + DateFormat.getDateInstance(DateFormat.DEFAULT, locale); + case SHORT -> + DateFormat.getDateInstance(DateFormat.SHORT, locale); + case MEDIUM -> + DateFormat.getDateInstance(DateFormat.MEDIUM, locale); + case LONG -> + DateFormat.getDateInstance(DateFormat.LONG, locale); + case FULL -> + DateFormat.getDateInstance(DateFormat.FULL, locale); + default -> formatFromSubformatPattern(fType, style); + }; + case TIME -> switch (fStyle) { + case DEFAULT -> + DateFormat.getTimeInstance(DateFormat.DEFAULT, locale); + case SHORT -> + DateFormat.getTimeInstance(DateFormat.SHORT, locale); + case MEDIUM -> + DateFormat.getTimeInstance(DateFormat.MEDIUM, locale); + case LONG -> + DateFormat.getTimeInstance(DateFormat.LONG, locale); + case FULL -> + DateFormat.getTimeInstance(DateFormat.FULL, locale); + default -> formatFromSubformatPattern(fType, style); + }; + case DTF_DATE -> switch (fStyle) { + case DEFAULT, MEDIUM -> + DateTimeFormatter.ofLocalizedDate(java.time.format.FormatStyle.MEDIUM).withLocale(locale).toFormat(); + case SHORT -> + DateTimeFormatter.ofLocalizedDate(java.time.format.FormatStyle.SHORT).withLocale(locale).toFormat(); + case LONG -> + DateTimeFormatter.ofLocalizedDate(java.time.format.FormatStyle.LONG).withLocale(locale).toFormat(); + case FULL -> + DateTimeFormatter.ofLocalizedDate(java.time.format.FormatStyle.FULL).withLocale(locale).toFormat(); + default -> formatFromSubformatPattern(fType, style); + }; + case DTF_TIME -> switch (fStyle) { + case DEFAULT, MEDIUM -> + DateTimeFormatter.ofLocalizedTime(java.time.format.FormatStyle.MEDIUM).withLocale(locale).toFormat(); + case SHORT -> + DateTimeFormatter.ofLocalizedTime(java.time.format.FormatStyle.SHORT).withLocale(locale).toFormat(); + case LONG -> + DateTimeFormatter.ofLocalizedTime(java.time.format.FormatStyle.LONG).withLocale(locale).toFormat(); + case FULL -> + DateTimeFormatter.ofLocalizedTime(java.time.format.FormatStyle.FULL).withLocale(locale).toFormat(); + default -> formatFromSubformatPattern(fType, style); + }; + case DTF_DATETIME -> switch (fStyle) { + case DEFAULT, MEDIUM -> + DateTimeFormatter.ofLocalizedDateTime(java.time.format.FormatStyle.MEDIUM).withLocale(locale).toFormat(); + case SHORT -> + DateTimeFormatter.ofLocalizedDateTime(java.time.format.FormatStyle.SHORT).withLocale(locale).toFormat(); + case LONG -> + DateTimeFormatter.ofLocalizedDateTime(java.time.format.FormatStyle.LONG).withLocale(locale).toFormat(); + case FULL -> + DateTimeFormatter.ofLocalizedDateTime(java.time.format.FormatStyle.FULL).withLocale(locale).toFormat(); + default -> formatFromSubformatPattern(fType, style); + }; + case CHOICE -> formatFromSubformatPattern(fType, style); + case LIST -> switch (fStyle) { + case DEFAULT -> + ListFormat.getInstance(locale, ListFormat.Type.STANDARD, ListFormat.Style.FULL); + case OR -> + ListFormat.getInstance(locale, ListFormat.Type.OR, ListFormat.Style.FULL); + case UNIT -> + ListFormat.getInstance(locale, ListFormat.Type.UNIT, ListFormat.Style.FULL); + // ListFormat does not provide a String pattern method/constructor + default -> formatFromSubformatPattern(fType, style); + }; + // The DateTimeFormatter constants are only given as a type + // Regardless of style, return the corresponding DTF constant + case BASIC_ISO_DATE -> DateTimeFormatter.BASIC_ISO_DATE.toFormat(); + case ISO_LOCAL_DATE -> DateTimeFormatter.ISO_LOCAL_DATE.toFormat(); + case ISO_OFFSET_DATE -> DateTimeFormatter.ISO_OFFSET_DATE.toFormat(); + case ISO_DATE -> DateTimeFormatter.ISO_DATE.toFormat(); + case ISO_LOCAL_TIME -> DateTimeFormatter.ISO_LOCAL_TIME.toFormat(); + case ISO_OFFSET_TIME -> DateTimeFormatter.ISO_OFFSET_TIME.toFormat(); + case ISO_TIME -> DateTimeFormatter.ISO_TIME.toFormat(); + case ISO_LOCAL_DATE_TIME -> DateTimeFormatter.ISO_LOCAL_DATE_TIME.toFormat(); + case ISO_OFFSET_DATE_TIME -> DateTimeFormatter.ISO_OFFSET_DATE_TIME.toFormat(); + case ISO_ZONED_DATE_TIME -> DateTimeFormatter.ISO_ZONED_DATE_TIME.toFormat(); + case ISO_DATE_TIME -> DateTimeFormatter.ISO_DATE_TIME.toFormat(); + case ISO_ORDINAL_DATE -> DateTimeFormatter.ISO_ORDINAL_DATE.toFormat(); + case ISO_WEEK_DATE -> DateTimeFormatter.ISO_WEEK_DATE.toFormat(); + case ISO_INSTANT -> DateTimeFormatter.ISO_INSTANT.toFormat(); + case RFC_1123_DATE_TIME -> DateTimeFormatter.RFC_1123_DATE_TIME.toFormat(); + }; + } - // Try trimmed lowercase. - String ls = s.trim().toLowerCase(Locale.ROOT); - if (ls != s) { - for (int i = 0; i < list.length; ++i) { - if (ls.equals(list[i])) - return i; + /** + * This method will attempt to return a subformat produced with the provided + * SubformatPattern applied. If the subformat does not support SubformatPatterns + * or the SubformatPattern is illegal to the subformat, an IllegalArgumentException + * is thrown. To adhere to the specification, this method ensures if an underlying + * exception is thrown, it is rethrown as an IllegalArgumentException unless + * the underlying exception is itself an IAE, or an NPE. + * + * @param fType the enum type of the subformat + * @param pattern the SubformatPattern to be applied + * @return a Format that corresponds to the corresponding {@code fType} + * and {@code pattern} + * @throws IllegalArgumentException if a Format cannot be produced from the + * type and SubformatPattern provided + */ + private Format formatFromSubformatPattern(FormatType fType, String pattern) { + // Modified for neater exception value if needed + String type = fType.name().charAt(0) + fType.name().substring(1).toLowerCase(Locale.ROOT); + try { + return switch (fType) { + case NUMBER -> new DecimalFormat(pattern, DecimalFormatSymbols.getInstance(locale)); + case DATE, TIME -> new SimpleDateFormat(pattern, locale); + case DTF_DATE, DTF_TIME, DTF_DATETIME -> + DateTimeFormatter.ofPattern(pattern).toFormat(); + case CHOICE -> new ChoiceFormat(pattern); + // These classe(s) do not support String patterns + default -> throw new IllegalArgumentException(String.format( + "Unexpected modifier for %s: %s", type, pattern)); + }; + } catch (Exception e) { + // getClass check over separate catch block to not catch the IAE subclasses + // For example, ChoiceFormat can throw a NumberFormatException + if (e.getClass() == IllegalArgumentException.class + || e.getClass() == NullPointerException.class) { + // If IAE no need to wrap with another IAE + // If NPE, it should be thrown as is (as specified) + throw e; + } else { + throw new IllegalArgumentException(String.format( + "%s pattern incorrect: %s", type, pattern), e); } } - return -1; } private static void copyAndFixQuotes(String source, int start, int end, @@ -1668,14 +1871,17 @@ public class MessageFormat extends Format { } } - // Copy the text, but add quotes around any quotables that aren't already quoted - private static void copyAndQuoteBraces(String source, StringBuilder target) { + // The subformat pattern comes already quoted, but only for those characters that are + // special to the subformat. Therefore, we may need to quote additional characters. + // The ones we care about at the MessageFormat level are '{' and '}'. + private static String copyAndQuoteBraces(String source) { // Analyze existing string for already quoted and newly quotable characters record Qchar(char ch, boolean quoted) { }; ArrayList qchars = new ArrayList<>(); boolean quoted = false; boolean anyChangeNeeded = false; + StringBuilder quotedSource = new StringBuilder(); for (int i = 0; i < source.length(); i++) { char ch = source.charAt(i); if (ch == '\'') { @@ -1694,8 +1900,7 @@ public class MessageFormat extends Format { // Was any change needed? if (!anyChangeNeeded) { - target.append(source); - return; + return source; } // Build new string, automatically consolidating adjacent runs of quoted chars @@ -1703,15 +1908,85 @@ public class MessageFormat extends Format { for (Qchar qchar : qchars) { char ch = qchar.ch; if (ch == '\'') { - target.append(ch); // doubling works whether quoted or not + quotedSource.append(ch); // doubling works whether quoted or not } else if (qchar.quoted() != quoted) { - target.append('\''); + quotedSource.append('\''); quoted = qchar.quoted(); } - target.append(ch); + quotedSource.append(ch); } if (quoted) { - target.append('\''); + quotedSource.append('\''); + } + + return quotedSource.toString(); + } + + // Corresponding to the FormatType pattern + private enum FormatType { + NUMBER, + DATE, + TIME, + DTF_DATE, + DTF_TIME, + DTF_DATETIME, + CHOICE, + LIST, + + // Pre-defined DateTimeFormatter types + BASIC_ISO_DATE, + ISO_LOCAL_DATE, + ISO_OFFSET_DATE , + ISO_DATE, + ISO_LOCAL_TIME, + ISO_OFFSET_TIME, + ISO_TIME, + ISO_LOCAL_DATE_TIME, + ISO_OFFSET_DATE_TIME, + ISO_ZONED_DATE_TIME, + ISO_DATE_TIME, + ISO_ORDINAL_DATE, + ISO_WEEK_DATE, + ISO_INSTANT, + RFC_1123_DATE_TIME; + } + + // Corresponding to the FormatStyle pattern + private enum FormatStyle { + DEFAULT(""), + SHORT("short"), + MEDIUM("medium"), + LONG("long"), + FULL("full"), + INTEGER("integer"), + CURRENCY("currency"), + PERCENT("percent"), + COMPACT_SHORT("compact_short"), + COMPACT_LONG("compact_long"), + OR("or"), + UNIT("unit"), + SUBFORMATPATTERN(null); + + private final String text; + + // Differs from FormatType in that the text String is + // not guaranteed to match the Enum name, thus a text field is used + FormatStyle(String text) { + this.text = text; + } + + // This method returns a FormatStyle (excluding SUBFORMATPATTERN) + // that matches the passed String. If no FormatStyle is found, + // an IllegalArgumentException is thrown + private static FormatStyle fromString(String text) { + for (FormatStyle style : values()) { + // Also check trimmed case-insensitive for historical reasons + if (style != FormatStyle.SUBFORMATPATTERN && + text.trim().compareToIgnoreCase(style.text) == 0) { + return style; + } + } + throw new IllegalArgumentException(); } } diff --git a/src/java.base/share/classes/java/text/NumberFormat.java b/src/java.base/share/classes/java/text/NumberFormat.java index 2f448950bea..716cfb710c4 100644 --- a/src/java.base/share/classes/java/text/NumberFormat.java +++ b/src/java.base/share/classes/java/text/NumberFormat.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 1996, 2023, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 1996, 2024, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -677,6 +677,33 @@ public abstract class NumberFormat extends Format { return getInstance(locale, formatStyle, COMPACTSTYLE); } + /** + * This method compares the passed NumberFormat to a number of pre-defined + * style NumberFormat instances, (created with the passed locale). Returns a + * matching FormatStyle string if found, otherwise null. + * This method is used by MessageFormat to provide string pattens for NumberFormat + * Subformats. Any future pre-defined NumberFormat styles should be added to this method. + */ + static String matchToStyle(NumberFormat fmt, Locale locale) { + if (fmt.equals(NumberFormat.getInstance(locale))) { + return ""; + } else if (fmt.equals(NumberFormat.getCurrencyInstance(locale))) { + return "currency"; + } else if (fmt.equals(NumberFormat.getPercentInstance(locale))) { + return "percent"; + } else if (fmt.equals(NumberFormat.getIntegerInstance(locale))) { + return "integer"; + } else if (fmt.equals(NumberFormat.getCompactNumberInstance(locale, + NumberFormat.Style.SHORT))) { + return "compact_short"; + } else if (fmt.equals(NumberFormat.getCompactNumberInstance(locale, + NumberFormat.Style.LONG))) { + return "compact_long"; + } else { + return null; + } + } + /** * Returns an array of all locales for which the * {@code get*Instance} methods of this class can return diff --git a/test/jdk/java/text/Format/MessageFormat/CompactSubFormats.java b/test/jdk/java/text/Format/MessageFormat/CompactSubFormats.java new file mode 100644 index 00000000000..f4b12a14fcc --- /dev/null +++ b/test/jdk/java/text/Format/MessageFormat/CompactSubFormats.java @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8318761 + * @summary Test MessageFormatPattern ability to recognize and produce + * appropriate FormatType and FormatStyle for CompactNumberFormat. + * @run junit CompactSubFormats + */ + +import java.text.CompactNumberFormat; +import java.text.DecimalFormat; +import java.text.DecimalFormatSymbols; +import java.text.MessageFormat; +import java.text.NumberFormat; +import java.util.Locale; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class CompactSubFormats { + + // Ensure the built-in FormatType and FormatStyles for cnFmt are as expected + @Test + public void applyPatternTest() { + var mFmt = new MessageFormat( + "{0,number,compact_short}{1,number,compact_long}"); + var compactShort = NumberFormat.getCompactNumberInstance( + mFmt.getLocale(), NumberFormat.Style.SHORT); + var compactLong = NumberFormat.getCompactNumberInstance( + mFmt.getLocale(), NumberFormat.Style.LONG); + assertEquals(mFmt.getFormatsByArgumentIndex()[0], compactShort); + assertEquals(mFmt.getFormatsByArgumentIndex()[1], compactLong); + } + + // Ensure that only 'compact_short' and 'compact_long' are recognized as + // compact number modifiers. All other compact_XX should be interpreted as + // a subformatPattern for a DecimalFormat + @Test + public void recognizedCompactStylesTest() { + // An exception won't be thrown since 'compact_regular' will be interpreted as a + // subformatPattern. + assertEquals(new DecimalFormat("compact_regular"), + new MessageFormat("{0,number,compact_regular}").getFormatsByArgumentIndex()[0]); + } + + // SHORT and LONG CompactNumberFormats should produce correct patterns + @Test + public void toPatternTest() { + var mFmt = new MessageFormat("{0}{1}"); + mFmt.setFormatByArgumentIndex(0, NumberFormat.getCompactNumberInstance( + mFmt.getLocale(), NumberFormat.Style.SHORT)); + mFmt.setFormatByArgumentIndex(1, NumberFormat.getCompactNumberInstance( + mFmt.getLocale(), NumberFormat.Style.LONG)); + assertEquals("{0,number,compact_short}{1,number,compact_long}", mFmt.toPattern()); + } + + // A custom cnFmt cannot be recognized, thus does not produce any built-in pattern + @Test + public void badToPatternTest() { + var mFmt = new MessageFormat("{0}"); + // Non-recognizable compactNumberFormat + mFmt.setFormatByArgumentIndex(0, new CompactNumberFormat("", + DecimalFormatSymbols.getInstance(Locale.US), new String[]{""})); + // Default behavior of unrecognizable Formats is a FormatElement + // in the form of { ArgumentIndex } + assertEquals("{0}", mFmt.toPattern()); + } +} diff --git a/test/jdk/java/text/Format/MessageFormat/ListSubFormats.java b/test/jdk/java/text/Format/MessageFormat/ListSubFormats.java new file mode 100644 index 00000000000..598239ef5ef --- /dev/null +++ b/test/jdk/java/text/Format/MessageFormat/ListSubFormats.java @@ -0,0 +1,98 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8318761 + * @summary Test MessageFormatPattern ability to recognize and produce the + * appropriate FormatType and FormatStyle for ListFormat. ListFormat's + * STANDARD, OR, and UNIT types are supported as built-in patterns for + * MessageFormat. All types use the FULL style. + * @run junit ListSubFormats + */ + +import java.text.ListFormat; +import java.text.MessageFormat; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class ListSubFormats { + + // Recognize the 'list' FormatType as well as '', 'or', and + // 'unit' associated FormatStyles + @Test + public void applyPatternTest() { + var mFmt = new MessageFormat("{0,list}{1,list,or}{2,list,unit}"); + var listStandard = ListFormat.getInstance(mFmt.getLocale(), + ListFormat.Type.STANDARD, ListFormat.Style.FULL); + var listOr = ListFormat.getInstance(mFmt.getLocale(), + ListFormat.Type.OR, ListFormat.Style.FULL); + var listUnit = ListFormat.getInstance(mFmt.getLocale(), + ListFormat.Type.UNIT, ListFormat.Style.FULL); + assertEquals(mFmt.getFormatsByArgumentIndex()[0], listStandard); + assertEquals(mFmt.getFormatsByArgumentIndex()[1], listOr); + assertEquals(mFmt.getFormatsByArgumentIndex()[2], listUnit); + } + + // Ensure incorrect FormatElement pattern throws IAE + // java.text.ListFormat does not support String subformatPatterns + @Test + public void badApplyPatternTest() { + // Wrong FormatStyle + IllegalArgumentException exc = assertThrows(IllegalArgumentException.class, () -> + new MessageFormat("{0,list,standard}")); + assertEquals("Unexpected modifier for List: standard", exc.getMessage()); + + // Wrong FormatType + exc = assertThrows(IllegalArgumentException.class, () -> + new MessageFormat("{0,listt,or}")); + assertEquals("unknown format type: listt", exc.getMessage()); + + } + + // STANDARD, OR, UNIT ListFormats (with FULL style) should + // produce correct patterns. + @Test + public void toPatternTest() { + var mFmt = new MessageFormat("{0}{1}{2}"); + mFmt.setFormatByArgumentIndex(0, + ListFormat.getInstance(mFmt.getLocale(), ListFormat.Type.STANDARD, ListFormat.Style.FULL)); + mFmt.setFormatByArgumentIndex(1, + ListFormat.getInstance(mFmt.getLocale(), ListFormat.Type.OR, ListFormat.Style.FULL)); + mFmt.setFormatByArgumentIndex(2, + ListFormat.getInstance(mFmt.getLocale(), ListFormat.Type.UNIT, ListFormat.Style.FULL)); + assertEquals("{0,list}{1,list,or}{2,list,unit}", mFmt.toPattern()); + } + + // A custom ListFormat cannot be recognized, thus does not produce any built-in pattern + @Test + public void badToPatternTest() { + var mFmt = new MessageFormat("{0}"); + mFmt.setFormatByArgumentIndex(0, + ListFormat.getInstance(mFmt.getLocale(), ListFormat.Type.UNIT, ListFormat.Style.NARROW)); + assertEquals("{0}", mFmt.toPattern()); + } +} diff --git a/test/jdk/java/text/Format/MessageFormat/MessageFormatExceptions.java b/test/jdk/java/text/Format/MessageFormat/MessageFormatExceptions.java index f28c3170e71..276f49a07d7 100644 --- a/test/jdk/java/text/Format/MessageFormat/MessageFormatExceptions.java +++ b/test/jdk/java/text/Format/MessageFormat/MessageFormatExceptions.java @@ -24,7 +24,7 @@ /* * @test * @summary Validate some exceptions in MessageFormat - * @bug 6481179 8039165 + * @bug 6481179 8039165 8318761 * @run junit MessageFormatExceptions */ @@ -39,6 +39,15 @@ import static org.junit.jupiter.api.Assertions.assertThrows; public class MessageFormatExceptions { + // Any exception for a Subformat should be re-thrown as propagated as an IAE + // to the MessageFormat + @Test + public void rethrowAsIAE() { + // Same Subformat pattern for ChoiceFormat throws NumberFormatException + assertThrows(IllegalArgumentException.class, + () -> new MessageFormat("{0,choice,0foo#foo}")); + } + // MessageFormat should throw NPE when constructed with a null pattern @Test public void nullPatternTest() { @@ -57,6 +66,9 @@ public class MessageFormatExceptions { // Fails when constructor invokes applyPattern() assertThrows(NullPointerException.class, () -> new MessageFormat("{0, date}", null)); + // Same as above, but with Subformat pattern + assertThrows(NullPointerException.class, + () -> new MessageFormat("{0, date,dd}", null)); // Fail when constructor invokes applyPattern() assertThrows(NullPointerException.class, () -> new MessageFormat("{0, number}", null)); diff --git a/test/jdk/java/text/Format/MessageFormat/TemporalSubFormats.java b/test/jdk/java/text/Format/MessageFormat/TemporalSubFormats.java new file mode 100644 index 00000000000..c8572551a8d --- /dev/null +++ b/test/jdk/java/text/Format/MessageFormat/TemporalSubFormats.java @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2024, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8318761 + * @summary Test MessageFormatPattern ability to recognize the appropriate + * FormatType and FormatStyle for DateTimeFormatter(ClassicFormat). + * This includes the types dtf_time, dtf_date, dtf_datetime, + * and the DateTimeFormatter predefined formatters. + * @run junit TemporalSubFormats + */ + +import java.text.Format; +import java.text.MessageFormat; +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.OffsetDateTime; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.time.format.FormatStyle; +import java.util.stream.Stream; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class TemporalSubFormats { + + // Check that applying the built-in DateTimeFormatter types returns the + // correct Format and formats properly. Patterns are case-insensitive + @ParameterizedTest + @MethodSource("preDefinedTypes") + public void preDefinedPatternsTest(String pattern, Format fmt) { + var mFmt = new MessageFormat("quux{0,"+pattern+"}quux"); + Object[] temporals = new Object[]{LocalDate.now(), LocalTime.now(), + ZonedDateTime.now(), LocalDateTime.now(), OffsetDateTime.now(), Instant.now()}; + for (Object val : temporals) { + // Wrap in Object array for MessageFormat + Object[] wrappedVal = new Object[]{val}; + + try { + String mFmtted = mFmt.format(wrappedVal); + // If current format can support the time object. Check equality of result + assertEquals(mFmtted, "quux"+fmt.format(val)+"quux"); + } catch (IllegalArgumentException ignored) { + // Otherwise, ensure both throw IAE on unsupported field + assertThrows(IllegalArgumentException.class, () -> fmt.format(val)); + } + } + } + + // Provides String patterns and the associated (standalone) FormatType + // Values should be case-insensitive + private static Stream preDefinedTypes() { + return Stream.of( + Arguments.of("BASIC_ISO_DATE", DateTimeFormatter.BASIC_ISO_DATE.toFormat()), + Arguments.of("ISO_LOCAL_DATE", DateTimeFormatter.ISO_LOCAL_DATE.toFormat()), + Arguments.of("ISO_OFFSET_DATE", DateTimeFormatter.ISO_OFFSET_DATE.toFormat()), + Arguments.of("ISO_DATE", DateTimeFormatter.ISO_DATE.toFormat()), + Arguments.of("iso_local_time", DateTimeFormatter.ISO_LOCAL_TIME.toFormat()), + Arguments.of("ISO_OFFSET_TIME", DateTimeFormatter.ISO_OFFSET_TIME.toFormat()), + Arguments.of("iso_time", DateTimeFormatter.ISO_TIME.toFormat()), + Arguments.of("ISO_LOCAL_DATE_TIME", DateTimeFormatter.ISO_LOCAL_DATE_TIME.toFormat()), + Arguments.of("ISO_OFFSET_DATE_TIME", DateTimeFormatter.ISO_OFFSET_DATE_TIME.toFormat()), + Arguments.of("ISO_ZONED_DATE_TIME", DateTimeFormatter.ISO_ZONED_DATE_TIME.toFormat()), + Arguments.of("ISO_DATE_TIME", DateTimeFormatter.ISO_DATE_TIME.toFormat()), + Arguments.of("ISO_ORDINAL_DATE", DateTimeFormatter.ISO_ORDINAL_DATE.toFormat()), + Arguments.of("iso_week_date", DateTimeFormatter.ISO_WEEK_DATE.toFormat()), + Arguments.of("ISO_INSTANT", DateTimeFormatter.ISO_INSTANT.toFormat()), + Arguments.of("RFC_1123_DATE_TIME", DateTimeFormatter.RFC_1123_DATE_TIME.toFormat()) + ); + } + + // Check that the appropriate FormatType/Style combo returns correct Format + // Unlike the other pattern tests, the formatted output is used to check + // equality, as DateTimeFormatter does not implement equals() + @ParameterizedTest + @MethodSource("styles") + public void applyPatternTest(String style, FormatStyle fStyle) { + var time = ZonedDateTime.now(); + var date = LocalDate.now(); + + // Test dtf_date + var dFmt = new MessageFormat("{0,dtf_date"+style+"}"); + assertEquals(DateTimeFormatter.ofLocalizedDate(fStyle).withLocale( + dFmt.getLocale()).toFormat().format(date), + dFmt.getFormatsByArgumentIndex()[0].format(date)); + + // Test dtf_time + var tFmt = new MessageFormat("{0,dtf_time"+style+"}"); + assertEquals(DateTimeFormatter.ofLocalizedTime(fStyle).withLocale( + tFmt.getLocale()).toFormat().format(time), + tFmt.getFormatsByArgumentIndex()[0].format(time)); + + // Test dtf_datetime + var dtFmt = new MessageFormat("{0,dtf_datetime"+style+"}"); + assertEquals(DateTimeFormatter.ofLocalizedDateTime(fStyle).withLocale( + dtFmt.getLocale()).toFormat().format(time), + dtFmt.getFormatsByArgumentIndex()[0].format(time)); + } + + // Provides String patterns and the associated FormatStyle + private static Stream styles() { + return Stream.of( + Arguments.of("", FormatStyle.MEDIUM), + Arguments.of(",short", FormatStyle.SHORT), + Arguments.of(",medium", FormatStyle.MEDIUM), + Arguments.of(",long", FormatStyle.LONG), + Arguments.of(",full", FormatStyle.FULL) + ); + } + + // Test that a proper Format from a SubformatPattern can be reproduced + @Test + public void subformatPatternTest() { + // SubformatPattern invokes the same method for both dtf_date, + // dtf_time, and dtf_datetime + var pattern = "d MMM uuuu"; + var date = LocalDate.now(); + + // Test dtf_date + var dFmt = new MessageFormat("{0,dtf_date,"+pattern+"}"); + assertEquals(DateTimeFormatter.ofPattern(pattern,dFmt.getLocale()).toFormat().format(date), + dFmt.getFormatsByArgumentIndex()[0].format(date)); + + // Test dtf_time + var tFmt = new MessageFormat("{0,dtf_time,"+pattern+"}"); + assertEquals(DateTimeFormatter.ofPattern(pattern,tFmt.getLocale()).toFormat().format(date), + tFmt.getFormatsByArgumentIndex()[0].format(date)); + + // Test dtf_datetime + var dtFmt = new MessageFormat("{0,dtf_datetime,"+pattern+"}"); + assertEquals(DateTimeFormatter.ofPattern(pattern,dtFmt.getLocale()).toFormat().format(date), + dtFmt.getFormatsByArgumentIndex()[0].format(date)); + } + + // Ensure that only the supported built-in FormatStyles or a + // valid SubformatPattern are recognized + @Test + public void badApplyPatternTest() { + // Not a supported FormatStyle: throws the underlying IAE from DTF + // as it is interpreted as a subformatPattern + IllegalArgumentException exc = assertThrows(IllegalArgumentException.class, () -> + new MessageFormat("{0,dtf_date,longer}")); + assertEquals("Unknown pattern letter: l", exc.getMessage()); + + // Not a legal SubformatPattern: throws the underlying IAE from DTF + exc = assertThrows(IllegalArgumentException.class, () -> + new MessageFormat("{0,dtf_date,VVV}")); + assertEquals("Pattern letter count must be 2: V", exc.getMessage()); + + // Pre-defined ISO style does not exist and should be ignored + assertDoesNotThrow(() -> new MessageFormat("{0,BASIC_ISO_DATE,foo}"), + "Style on a pre-defined DTF should be ignored, instead of throwing an exception"); + } + + // DateTimeFormatters cannot be recognized when toPattern() is invoked + // Default behavior of unrecognizable Formats is a FormatElement + // in the form of { ArgumentIndex } + @Test + public void nonRecognizableToPatternTest() { + // Check SubformatPattern + var validPattern = "yy"; + var mFmt = new MessageFormat("{0}"); + mFmt.setFormatByArgumentIndex(0, DateTimeFormatter.ofPattern(validPattern).toFormat()); + assertEquals("{0}", mFmt.toPattern()); + + // Check pre-defined styles + var dFmt = new MessageFormat("{0,dtf_date,long}"); + assertEquals("{0}", dFmt.toPattern()); + var tFmt = new MessageFormat("{0,dtf_time,long}"); + assertEquals("{0}", tFmt.toPattern()); + var dtFmt = new MessageFormat("{0,dtf_datetime,long}"); + assertEquals("{0}", dtFmt.toPattern()); + } +}