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. + * + *
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}} + * |
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. * - *
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)} - * |
- * 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 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"
+ * }
+ *
+ *
* 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) { };
ArrayListFormatting 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:
+ *
+ * Parsing
*