8318761: MessageFormat pattern support for CompactNumberFormat, ListFormat, and DateTimeFormatter

Reviewed-by: naoto, rriggs
This commit is contained in:
Justin Lu 2024-02-22 22:27:12 +00:00
parent d695af89f6
commit 00ffc42cef
7 changed files with 1092 additions and 360 deletions

@ -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.

File diff suppressed because it is too large Load Diff

@ -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

@ -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());
}
}

@ -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());
}
}

@ -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));

@ -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<Arguments> 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<Arguments> 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());
}
}