8176706: Additional Date-Time Formats

Reviewed-by: joehw, rriggs
This commit is contained in:
Naoto Sato 2022-02-16 16:54:53 +00:00
parent 0f3d3ac32c
commit 9b74c3f2e7
12 changed files with 809 additions and 96 deletions

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2012, 2021, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2012, 2022, 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
@ -81,7 +81,6 @@ import java.time.Period;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.chrono.ChronoLocalDateTime;
import java.time.chrono.ChronoZonedDateTime;
import java.time.chrono.Chronology;
import java.time.chrono.IsoChronology;
import java.time.format.DateTimeFormatterBuilder.CompositePrinterParser;
@ -718,6 +717,57 @@ public final class DateTimeFormatter {
.toFormatter(ResolverStyle.SMART, IsoChronology.INSTANCE);
}
//-----------------------------------------------------------------------
/**
* Creates a locale specific formatter derived from the requested template for
* the ISO chronology. The requested template is a series of typical pattern
* symbols in canonical order from the largest date or time unit to the smallest,
* which can be expressed with the following regular expression:
* {@snippet :
* "G{0,5}" + // Era
* "y*" + // Year
* "Q{0,5}" + // Quarter
* "M{0,5}" + // Month
* "w*" + // Week of Week Based Year
* "E{0,5}" + // Day of Week
* "d{0,2}" + // Day of Month
* "B{0,5}" + // Period/AmPm of Day
* "[hHjC]{0,2}" + // Hour of Day/AmPm (refer to LDML for 'j' and 'C')
* "m{0,2}" + // Minute of Hour
* "s{0,2}" + // Second of Minute
* "[vz]{0,4}" // Zone
* }
* All pattern symbols are optional, and each pattern symbol represents a field,
* for example, 'M' represents the Month field. The number of the pattern symbol letters follows the
* same presentation, such as "number" or "text" as in the <a href="#patterns">Patterns for
* Formatting and Parsing</a> section. Other pattern symbols in the requested template are
* invalid.
* <p>
* The mapping of the requested template to the closest of the available localized formats
* is defined by the
* <a href="https://www.unicode.org/reports/tr35/tr35-dates.html#availableFormats_appendItems">
* Unicode LDML specification</a>. For example, the formatter created from the requested template
* {@code yMMM} will format the date '2020-06-16' to 'Jun 2020' in the {@link Locale#US US locale}.
* <p>
* The locale is determined from the formatter. The formatter returned directly by
* this method uses the {@link Locale#getDefault() default FORMAT locale}.
* The locale can be controlled using {@link DateTimeFormatter#withLocale(Locale) withLocale(Locale)}
* on the result of this method.
* <p>
* The returned formatter has no override zone.
* It uses {@link ResolverStyle#SMART SMART} resolver style.
*
* @param requestedTemplate the requested template, not null
* @return the formatter based on the {@code requestedTemplate} pattern, not null
* @throws IllegalArgumentException if {@code requestedTemplate} is invalid
* @see #ofPattern(String)
* @since 19
*/
public static DateTimeFormatter ofLocalizedPattern(String requestedTemplate) {
return new DateTimeFormatterBuilder().appendLocalized(requestedTemplate)
.toFormatter(ResolverStyle.SMART, IsoChronology.INSTANCE);
}
//-----------------------------------------------------------------------
/**
* The ISO date formatter that formats or parses a date without an

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2012, 2021, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2012, 2022, 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
@ -227,6 +227,42 @@ public final class DateTimeFormatterBuilder {
CalendarDataUtility.findRegionOverride(locale));
}
/**
* Returns the formatting pattern for the requested template for a locale and chronology.
* The locale and chronology are used to lookup the locale specific format
* for the requested template.
* <p>
* If the locale contains the "rg" (region override)
* <a href="../../util/Locale.html#def_locale_extension">Unicode extensions</a>,
* the formatting pattern is overridden with the one appropriate for the region.
* <p>
* Refer to {@link #appendLocalized(String)} for the detail of {@code requestedTemplate}
* argument.
*
* @param requestedTemplate the requested template, not null
* @param chrono the Chronology, non-null
* @param locale the locale, non-null
* @return the locale and Chronology specific formatting pattern
* @throws IllegalArgumentException if {@code requestedTemplate} does not match
* the regular expression syntax described in {@link #appendLocalized(String)}.
* @throws DateTimeException if a match for the localized pattern for
* {@code requestedTemplate} is not available
* @see #appendLocalized(String)
* @since 19
*/
public static String getLocalizedDateTimePattern(String requestedTemplate,
Chronology chrono, Locale locale) {
Objects.requireNonNull(requestedTemplate, "requestedTemplate");
Objects.requireNonNull(chrono, "chrono");
Objects.requireNonNull(locale, "locale");
Locale override = CalendarDataUtility.findRegionOverride(locale);
LocaleProviderAdapter adapter = LocaleProviderAdapter.getAdapter(JavaTimeDateTimePatternProvider.class, override);
JavaTimeDateTimePatternProvider provider = adapter.getJavaTimeDateTimePatternProvider();
return provider.getJavaTimeDateTimePattern(requestedTemplate,
chrono.getCalendarType(),
override);
}
/**
* Converts the given FormatStyle to the java.text.DateFormat style.
*
@ -1423,6 +1459,84 @@ public final class DateTimeFormatterBuilder {
return this;
}
//-----------------------------------------------------------------------
// RegEx pattern for skeleton validity checking
private static final Pattern VALID_TEMPLATE_PATTERN = Pattern.compile(
"G{0,5}" + // Era
"y*" + // Year
"Q{0,5}" + // Quarter
"M{0,5}" + // Month
"w*" + // Week of Week Based Year
"E{0,5}" + // Day of Week
"d{0,2}" + // Day of Month
"B{0,5}" + // Period/AmPm of Day
"[hHjC]{0,2}" + // Hour of Day/AmPm
"m{0,2}" + // Minute of Hour
"s{0,2}" + // Second of Minute
"[vz]{0,4}"); // Zone
/**
* Appends a localized pattern to the formatter using the requested template.
* <p>
* This appends a localized section to the builder, suitable for outputting
* a date, time or date-time combination. The format of the localized
* section is lazily looked up based on three items:
* <ul>
* <li>the {@code requestedTemplate} specified to this method
* <li>the {@code Locale} of the {@code DateTimeFormatter}
* <li>the {@code Chronology} of the {@code DateTimeFormatter} unless overridden
* </ul>
* During formatting, the chronology is obtained from the temporal object
* being formatted, which may have been overridden by
* {@link DateTimeFormatter#withChronology(Chronology)}.
* <p>
* During parsing, if a chronology has already been parsed, then it is used.
* Otherwise the default from {@code DateTimeFormatter.withChronology(Chronology)}
* is used, with {@code IsoChronology} as the fallback.
* <p>
* The requested template is a series of typical pattern
* symbols in canonical order from the largest date or time unit to the smallest,
* which can be expressed with the following regular expression:
* {@snippet :
* "G{0,5}" + // Era
* "y*" + // Year
* "Q{0,5}" + // Quarter
* "M{0,5}" + // Month
* "w*" + // Week of Week Based Year
* "E{0,5}" + // Day of Week
* "d{0,2}" + // Day of Month
* "B{0,5}" + // Period/AmPm of Day
* "[hHjC]{0,2}" + // Hour of Day/AmPm (refer to LDML for 'j' and 'C')
* "m{0,2}" + // Minute of Hour
* "s{0,2}" + // Second of Minute
* "[vz]{0,4}" // Zone
* }
* All pattern symbols are optional, and each pattern symbol represents a field,
* for example, 'M' represents the Month field. The number of the pattern symbol letters follows the
* same presentation, such as "number" or "text" as in the
* <a href="./DateTimeFormatter.html#patterns">Patterns for Formatting and Parsing</a> section.
* Other pattern symbols in the requested template are invalid.
* <p>
* The mapping of the requested template to the closest of the available localized formats
* is defined by the
* <a href="https://www.unicode.org/reports/tr35/tr35-dates.html#availableFormats_appendItems">
* Unicode LDML specification</a>. For example, the formatter created from the requested template
* {@code yMMM} will format the date '2020-06-16' to 'Jun 2020' in the {@link Locale#US US locale}.
*
* @param requestedTemplate the requested template to use, not null
* @return this, for chaining, not null
* @throws IllegalArgumentException if {@code requestedTemplate} is invalid
* @see #appendPattern(String)
* @since 19
*/
public DateTimeFormatterBuilder appendLocalized(String requestedTemplate) {
Objects.requireNonNull(requestedTemplate, "requestedTemplate");
if (!VALID_TEMPLATE_PATTERN.matcher(requestedTemplate).matches()) {
throw new IllegalArgumentException("Requested template is invalid: " + requestedTemplate);
}
appendInternal(new LocalizedPrinterParser(requestedTemplate));
return this;
}
//-----------------------------------------------------------------------
/**
* Appends a character literal to the formatter.
@ -2378,11 +2492,11 @@ public final class DateTimeFormatterBuilder {
private final DateTimePrinterParser[] printerParsers;
private final boolean optional;
CompositePrinterParser(List<DateTimePrinterParser> printerParsers, boolean optional) {
private CompositePrinterParser(List<DateTimePrinterParser> printerParsers, boolean optional) {
this(printerParsers.toArray(new DateTimePrinterParser[0]), optional);
}
CompositePrinterParser(DateTimePrinterParser[] printerParsers, boolean optional) {
private CompositePrinterParser(DateTimePrinterParser[] printerParsers, boolean optional) {
this.printerParsers = printerParsers;
this.optional = optional;
}
@ -2476,7 +2590,7 @@ public final class DateTimeFormatterBuilder {
* @param padWidth the width to pad to, 1 or greater
* @param padChar the pad character
*/
PadPrinterParserDecorator(DateTimePrinterParser printerParser, int padWidth, char padChar) {
private PadPrinterParserDecorator(DateTimePrinterParser printerParser, int padWidth, char padChar) {
// input checked by DateTimeFormatterBuilder
this.printerParser = printerParser;
this.padWidth = padWidth;
@ -2584,7 +2698,7 @@ public final class DateTimeFormatterBuilder {
private final TemporalField field;
private final long value;
DefaultValueParser(TemporalField field, long value) {
private DefaultValueParser(TemporalField field, long value) {
this.field = field;
this.value = value;
}
@ -2608,7 +2722,7 @@ public final class DateTimeFormatterBuilder {
static final class CharLiteralPrinterParser implements DateTimePrinterParser {
private final char literal;
CharLiteralPrinterParser(char literal) {
private CharLiteralPrinterParser(char literal) {
this.literal = literal;
}
@ -2651,7 +2765,7 @@ public final class DateTimeFormatterBuilder {
static final class StringLiteralPrinterParser implements DateTimePrinterParser {
private final String literal;
StringLiteralPrinterParser(String literal) {
private StringLiteralPrinterParser(String literal) {
this.literal = literal; // validated by caller
}
@ -2717,7 +2831,7 @@ public final class DateTimeFormatterBuilder {
* @param maxWidth the maximum field width, from minWidth to 19
* @param signStyle the positive/negative sign style, not null
*/
NumberPrinterParser(TemporalField field, int minWidth, int maxWidth, SignStyle signStyle) {
private NumberPrinterParser(TemporalField field, int minWidth, int maxWidth, SignStyle signStyle) {
// validated by caller
this.field = field;
this.minWidth = minWidth;
@ -3008,7 +3122,7 @@ public final class DateTimeFormatterBuilder {
* @param baseValue the base value
* @param baseDate the base date
*/
ReducedPrinterParser(TemporalField field, int minWidth, int maxWidth,
private ReducedPrinterParser(TemporalField field, int minWidth, int maxWidth,
int baseValue, ChronoLocalDate baseDate) {
this(field, minWidth, maxWidth, baseValue, baseDate, 0);
if (minWidth < 1 || minWidth > 10) {
@ -3161,7 +3275,7 @@ public final class DateTimeFormatterBuilder {
* @param maxWidth the maximum width to output, from 0 to 9
* @param decimalPoint whether to output the localized decimal point symbol
*/
NanosPrinterParser(int minWidth, int maxWidth, boolean decimalPoint) {
private NanosPrinterParser(int minWidth, int maxWidth, boolean decimalPoint) {
this(minWidth, maxWidth, decimalPoint, 0);
if (minWidth < 0 || minWidth > 9) {
throw new IllegalArgumentException("Minimum width must be from 0 to 9 inclusive but was " + minWidth);
@ -3183,7 +3297,7 @@ public final class DateTimeFormatterBuilder {
* @param decimalPoint whether to output the localized decimal point symbol
* @param subsequentWidth the subsequentWidth for this instance
*/
NanosPrinterParser(int minWidth, int maxWidth, boolean decimalPoint, int subsequentWidth) {
private NanosPrinterParser(int minWidth, int maxWidth, boolean decimalPoint, int subsequentWidth) {
super(NANO_OF_SECOND, minWidth, maxWidth, SignStyle.NOT_NEGATIVE, subsequentWidth);
this.decimalPoint = decimalPoint;
}
@ -3366,7 +3480,7 @@ public final class DateTimeFormatterBuilder {
* @param maxWidth the maximum width to output, from 0 to 9
* @param decimalPoint whether to output the localized decimal point symbol
*/
FractionPrinterParser(TemporalField field, int minWidth, int maxWidth, boolean decimalPoint) {
private FractionPrinterParser(TemporalField field, int minWidth, int maxWidth, boolean decimalPoint) {
this(field, minWidth, maxWidth, decimalPoint, 0);
Objects.requireNonNull(field, "field");
if (field.range().isFixed() == false) {
@ -3393,7 +3507,7 @@ public final class DateTimeFormatterBuilder {
* @param decimalPoint whether to output the localized decimal point symbol
* @param subsequentWidth the subsequentWidth for this instance
*/
FractionPrinterParser(TemporalField field, int minWidth, int maxWidth, boolean decimalPoint, int subsequentWidth) {
private FractionPrinterParser(TemporalField field, int minWidth, int maxWidth, boolean decimalPoint, int subsequentWidth) {
super(field, minWidth, maxWidth, SignStyle.NOT_NEGATIVE, subsequentWidth);
this.decimalPoint = decimalPoint;
ValueRange range = field.range();
@ -3583,7 +3697,7 @@ public final class DateTimeFormatterBuilder {
* @param textStyle the text style, not null
* @param provider the text provider, not null
*/
TextPrinterParser(TemporalField field, TextStyle textStyle, DateTimeTextProvider provider) {
private TextPrinterParser(TemporalField field, TextStyle textStyle, DateTimeTextProvider provider) {
// validated by caller
this.field = field;
this.textStyle = textStyle;
@ -3681,7 +3795,7 @@ public final class DateTimeFormatterBuilder {
private static final long SECONDS_0000_TO_1970 = ((146097L * 5L) - (30L * 365L + 7L)) * 86400L;
private final int fractionalDigits;
InstantPrinterParser(int fractionalDigits) {
private InstantPrinterParser(int fractionalDigits) {
this.fractionalDigits = fractionalDigits;
}
@ -3830,7 +3944,7 @@ public final class DateTimeFormatterBuilder {
* @param pattern the pattern
* @param noOffsetText the text to use for UTC, not null
*/
OffsetIdPrinterParser(String pattern, String noOffsetText) {
private OffsetIdPrinterParser(String pattern, String noOffsetText) {
Objects.requireNonNull(pattern, "pattern");
Objects.requireNonNull(noOffsetText, "noOffsetText");
this.type = checkPattern(pattern);
@ -4312,7 +4426,7 @@ public final class DateTimeFormatterBuilder {
/** Display in generic time-zone format. True in case of pattern letter 'v' */
private final boolean isGeneric;
ZoneTextPrinterParser(TextStyle textStyle, Set<ZoneId> preferredZones, boolean isGeneric) {
private ZoneTextPrinterParser(TextStyle textStyle, Set<ZoneId> preferredZones, boolean isGeneric) {
super(TemporalQueries.zone(), "ZoneText(" + textStyle + ")");
this.textStyle = Objects.requireNonNull(textStyle, "textStyle");
this.isGeneric = isGeneric;
@ -4484,7 +4598,7 @@ public final class DateTimeFormatterBuilder {
private final TemporalQuery<ZoneId> query;
private final String description;
ZoneIdPrinterParser(TemporalQuery<ZoneId> query, String description) {
private ZoneIdPrinterParser(TemporalQuery<ZoneId> query, String description) {
this.query = query;
this.description = description;
}
@ -4903,7 +5017,7 @@ public final class DateTimeFormatterBuilder {
/** The text style to output, null means the ID. */
private final TextStyle textStyle;
ChronoPrinterParser(TextStyle textStyle) {
private ChronoPrinterParser(TextStyle textStyle) {
// validated by caller
this.textStyle = textStyle;
}
@ -4978,6 +5092,7 @@ public final class DateTimeFormatterBuilder {
private final FormatStyle dateStyle;
private final FormatStyle timeStyle;
private final String requestedTemplate;
/**
* Constructor.
@ -4985,10 +5100,23 @@ public final class DateTimeFormatterBuilder {
* @param dateStyle the date style to use, may be null
* @param timeStyle the time style to use, may be null
*/
LocalizedPrinterParser(FormatStyle dateStyle, FormatStyle timeStyle) {
// validated by caller
private LocalizedPrinterParser(FormatStyle dateStyle, FormatStyle timeStyle) {
// params validated by caller
this.dateStyle = dateStyle;
this.timeStyle = timeStyle;
this.requestedTemplate = null;
}
/**
* Constructor.
*
* @param requestedTemplate the requested template to use, not null
*/
private LocalizedPrinterParser(String requestedTemplate) {
// param validated by caller
this.dateStyle = null;
this.timeStyle = null;
this.requestedTemplate = requestedTemplate;
}
@Override
@ -5006,7 +5134,8 @@ public final class DateTimeFormatterBuilder {
/**
* Gets the formatter to use.
* <p>
* The formatter will be the most appropriate to use for the date and time style in the locale.
* The formatter will be the most appropriate to use for the date and time style, or
* the requested template for the locale.
* For example, some locales will use the month name while others will use the number.
*
* @param locale the locale to use, not null
@ -5015,23 +5144,22 @@ public final class DateTimeFormatterBuilder {
* @throws IllegalArgumentException if the formatter cannot be found
*/
private DateTimeFormatter formatter(Locale locale, Chronology chrono) {
String key = chrono.getId() + '|' + locale.toString() + '|' + dateStyle + timeStyle;
DateTimeFormatter formatter = FORMATTER_CACHE.get(key);
if (formatter == null) {
String pattern = getLocalizedDateTimePattern(dateStyle, timeStyle, chrono, locale);
formatter = new DateTimeFormatterBuilder().appendPattern(pattern).toFormatter(locale);
DateTimeFormatter old = FORMATTER_CACHE.putIfAbsent(key, formatter);
if (old != null) {
formatter = old;
}
}
return formatter;
String key = chrono.getId() + '|' + locale.toString() + '|' +
(requestedTemplate != null ? requestedTemplate : Objects.toString(dateStyle) + timeStyle);
return FORMATTER_CACHE.computeIfAbsent(key, k ->
new DateTimeFormatterBuilder()
.appendPattern(requestedTemplate != null ?
getLocalizedDateTimePattern(requestedTemplate, chrono, locale) :
getLocalizedDateTimePattern(dateStyle, timeStyle, chrono, locale))
.toFormatter(locale));
}
@Override
public String toString() {
return "Localized(" + (dateStyle != null ? dateStyle : "") + "," +
(timeStyle != null ? timeStyle : "") + ")";
return "Localized(" + (requestedTemplate != null ? requestedTemplate :
(dateStyle != null ? dateStyle : "") + "," +
(timeStyle != null ? timeStyle : "")) + ")";
}
}
@ -5056,7 +5184,7 @@ public final class DateTimeFormatterBuilder {
* @param minWidth the minimum field width, from 1 to 19
* @param maxWidth the maximum field width, from minWidth to 19
*/
WeekBasedFieldPrinterParser(char chr, int count, int minWidth, int maxWidth) {
private WeekBasedFieldPrinterParser(char chr, int count, int minWidth, int maxWidth) {
this(chr, count, minWidth, maxWidth, 0);
}
@ -5070,7 +5198,7 @@ public final class DateTimeFormatterBuilder {
* @param subsequentWidth the width of subsequent non-negative numbers, 0 or greater,
* -1 if fixed width due to active adjacent parsing
*/
WeekBasedFieldPrinterParser(char chr, int count, int minWidth, int maxWidth,
private WeekBasedFieldPrinterParser(char chr, int count, int minWidth, int maxWidth,
int subsequentWidth) {
super(null, minWidth, maxWidth, SignStyle.NOT_NEGATIVE, subsequentWidth);
this.chr = chr;
@ -5201,7 +5329,7 @@ public final class DateTimeFormatterBuilder {
*
* @param textStyle the text style, not null
*/
DayPeriodPrinterParser(TextStyle textStyle) {
private DayPeriodPrinterParser(TextStyle textStyle) {
// validated by caller
this.textStyle = textStyle;
}

View file

@ -27,6 +27,7 @@
package sun.text.spi;
import java.time.DateTimeException;
import java.util.Locale;
import java.util.spi.LocaleServiceProvider;
@ -41,10 +42,10 @@ public abstract class JavaTimeDateTimePatternProvider extends LocaleServiceProvi
}
/**
* Gets the formatting pattern for a timeStyle
* Returns the formatting pattern for a timeStyle
* dateStyle, calendarType and locale.
* Concrete implementation of this method will retrieve
* a java.time specific dateTime Pattern from selected Locale Provider.
* a java.time specific dateTime Pattern from the selected Locale Provider.
*
* @param timeStyle an {@code int} value, representing FormatStyle constant, -1
* for date-only pattern
@ -58,4 +59,26 @@ public abstract class JavaTimeDateTimePatternProvider extends LocaleServiceProvi
* @since 9
*/
public abstract String getJavaTimeDateTimePattern(int timeStyle, int dateStyle, String calType, Locale locale);
/**
* Returns the formatting pattern for the requested template, calendarType, and locale.
* Concrete implementation of this method will retrieve
* a java.time specific pattern from selected Locale Provider.
*
* @param requestedTemplate the requested template, not null
* @param calType a {@code String}, non-null representing CalendarType such as "japanese",
* "iso8601"
* @param locale {@code locale}, non-null
* @throws IllegalArgumentException if {@code requestedTemplate} does not match
* the regular expression syntax described in
* {@link java.time.format.DateTimeFormatterBuilder#appendLocalized(String)}.
* @throws DateTimeException if a match for the formatting pattern for
* {@code requestedTemplate} is not available
* @return formatting pattern {@code String}
* @since 19
*/
public String getJavaTimeDateTimePattern(String requestedTemplate, String calType, Locale locale) {
// default implementation throws exception
throw new DateTimeException("Formatting pattern is not available for the requested template: " + requestedTemplate);
}
}

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2016, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2016, 2022, 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
@ -24,7 +24,10 @@
*/
package sun.util.locale.provider;
import java.time.DateTimeException;
import java.util.Locale;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import sun.text.spi.JavaTimeDateTimePatternProvider;
@ -63,10 +66,21 @@ public class JavaTimeDateTimePatternImpl extends JavaTimeDateTimePatternProvider
@Override
public String getJavaTimeDateTimePattern(int timeStyle, int dateStyle, String calType, Locale locale) {
LocaleResources lr = LocaleProviderAdapter.getResourceBundleBased().getLocaleResources(locale);
String pattern = lr.getJavaTimeDateTimePattern(
timeStyle, dateStyle, calType);
return pattern;
return lr.getJavaTimeDateTimePattern(timeStyle, dateStyle, calType);
}
@Override
public String getJavaTimeDateTimePattern(String requestedTemplate, String calType, Locale locale) {
LocaleProviderAdapter lpa = LocaleProviderAdapter.getResourceBundleBased();
return ((ResourceBundleBasedAdapter)lpa).getCandidateLocales("", locale).stream()
.map(lpa::getLocaleResources)
.map(lr -> lr.getLocalizedPattern(requestedTemplate, calType))
.filter(Objects::nonNull)
.findFirst()
.or(() -> calType.equals("generic") ? Optional.empty():
Optional.of(getJavaTimeDateTimePattern(requestedTemplate, "generic", locale)))
.orElseThrow(() -> new DateTimeException("Requested template \"" + requestedTemplate +
"\" cannot be resolved in the locale \"" + locale + "\""));
}
@Override

View file

@ -1,5 +1,5 @@
/*
* Copyright (c) 2012, 2021, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2012, 2022, 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
@ -46,15 +46,21 @@ import java.text.MessageFormat;
import java.text.NumberFormat;
import java.util.Arrays;
import java.util.Calendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.TimeZone;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import sun.security.action.GetPropertyAction;
import sun.util.resources.LocaleData;
import sun.util.resources.OpenListResourceBundle;
@ -91,6 +97,10 @@ public class LocaleResources {
private static final String COMPACT_NUMBER_PATTERNS_CACHEKEY = "CNP";
private static final String DATE_TIME_PATTERN = "DTP.";
private static final String RULES_CACHEKEY = "RULE";
private static final String SKELETON_PATTERN = "SP.";
// ResourceBundle key names for skeletons
private static final String SKELETON_INPUT_REGIONS_KEY = "DateFormatItemInputRegions";
// TimeZoneNamesBundle exemplar city prefix
private static final String TZNB_EXCITY_PREFIX = "timezone.excity.";
@ -98,6 +108,31 @@ public class LocaleResources {
// null singleton cache value
private static final Object NULLOBJECT = new Object();
// RegEx pattern for skeleton validity checking
private static final Pattern VALID_SKELETON_PATTERN = Pattern.compile(
"(?<date>" +
"G{0,5}" + // Era
"y*" + // Year
"Q{0,5}" + // Quarter
"M{0,5}" + // Month
"w*" + // Week of Week Based Year
"E{0,5}" + // Day of Week
"d{0,2})" + // Day of Month
"(?<time>" +
"B{0,5}" + // Period/AmPm of Day
"[hHjC]{0,2}" + // Hour of Day/AmPm
"m{0,2}" + // Minute of Hour
"s{0,2}" + // Second of Minute
"[vz]{0,4})"); // Zone
// Input Skeleton map for "preferred" and "allowed"
// Map<"preferred"/"allowed", Map<"region", "skeleton">>
private static Map<String, Map<String, String>> inputSkeletons;
// Skeletons for "j" and "C" input skeleton symbols for this locale
private String jPattern;
private String CPattern;
LocaleResources(ResourceBundleBasedAdapter adapter, Locale locale) {
this.locale = locale;
this.localeData = adapter.getLocaleData();
@ -531,6 +566,202 @@ public class LocaleResources {
return rb;
}
/**
* Returns the actual format pattern string based on the requested template
* and calendar type for this locale.
*
* @param requestedTemplate requested template
* @param calType calendar type
* @throws IllegalArgumentException if the requested template is invalid
* @return format pattern string for this locale, null if not found
*/
public String getLocalizedPattern(String requestedTemplate, String calType) {
String pattern;
String cacheKey = SKELETON_PATTERN + calType + "." + requestedTemplate;
removeEmptyReferences();
ResourceReference data = cache.get(cacheKey);
if (data == null || ((pattern = (String) data.get()) == null)) {
pattern = getLocalizedPatternImpl(requestedTemplate, calType);
cache.put(cacheKey,
new ResourceReference(cacheKey, pattern != null ? pattern : "", referenceQueue));
} else if ("".equals(pattern)) {
// non-existent pattern
pattern = null;
}
return pattern;
}
private String getLocalizedPatternImpl(String requestedTemplate, String calType) {
initSkeletonIfNeeded();
// input skeleton substitution
var skeleton = substituteInputSkeletons(requestedTemplate);
// validity check
var matcher = VALID_SKELETON_PATTERN.matcher(skeleton);
if (!matcher.matches()) {
throw new IllegalArgumentException("Requested template \"%s\" is invalid".formatted(requestedTemplate) +
(requestedTemplate.equals(skeleton) ? "." : ", which translated into \"%s\"".formatted(skeleton) +
" after the 'j' or 'C' substitution."));
}
// try to match entire requested template first
String matched = matchSkeleton(skeleton, calType);
if (matched == null) {
// 2.6.2.2 Missing Skeleton Fields
var dateMatched = matchSkeleton(matcher.group("date"), calType);
var timeMatched = matchSkeleton(matcher.group("time"), calType);
if (dateMatched != null && timeMatched != null) {
// combine both matches
var style = switch (requestedTemplate.replaceAll("[^M]+", "").length()) {
case 4 -> requestedTemplate.indexOf('E') >= 0 ? 0 : 1;
case 3 -> 2;
default -> 3;
};
var dateTimePattern = getDateTimePattern(null, "DateTimePatterns", style, calType);
matched = MessageFormat.format(dateTimePattern.replaceAll("'", "''"), timeMatched, dateMatched);
}
}
trace("requested: %s, locale: %s, calType: %s, matched: %s\n", requestedTemplate, locale, calType, matched);
return matched;
}
private String matchSkeleton(String skeleton, String calType) {
// Expand it with possible inferred skeleton stream based on its priority
var inferred = possibleInferred(skeleton);
// Search the closest format pattern string from the resource bundle
ResourceBundle r = localeData.getDateFormatData(locale);
return inferred
.map(s -> ("gregory".equals(calType) ? "" : calType + ".") + "DateFormatItem." + s)
.map(key -> r.containsKey(key) ? r.getString(key) : null)
.filter(Objects::nonNull)
.findFirst()
.orElse(null);
}
private void initSkeletonIfNeeded() {
// "preferred"/"allowed" input skeleton maps
if (inputSkeletons == null) {
inputSkeletons = new HashMap<>();
Pattern p = Pattern.compile("([^:]+):([^;]+);");
ResourceBundle r = localeData.getDateFormatData(Locale.ROOT);
Stream.of("preferred", "allowed").forEach(type -> {
var inputRegionsKey = SKELETON_INPUT_REGIONS_KEY + "." + type;
Map<String, String> typeMap = new HashMap<>();
if (r.containsKey(inputRegionsKey)) {
p.matcher(r.getString(inputRegionsKey)).results()
.forEach(mr ->
Arrays.stream(mr.group(2).split(" "))
.forEach(region -> typeMap.put(region, mr.group(1))));
}
inputSkeletons.put(type, typeMap);
});
}
// j/C patterns for this locale
if (jPattern == null) {
jPattern = resolveInputSkeleton("preferred");
CPattern = resolveInputSkeleton("allowed");
// hack: "allowed" contains reversed order for hour/period, e.g, "hB" which should be "Bh" as a skeleton
if (CPattern.length() == 2) {
var ba = new byte[2];
ba[0] = (byte)CPattern.charAt(1);
ba[1] = (byte)CPattern.charAt(0);
CPattern = new String(ba);
}
}
}
/**
* Resolve locale specific input skeletons. Fall back method is different from usual
* resource bundle's, as it has to be "lang-region" -> "region" -> "lang-001" -> "001"
* @param type type of the input skeleton
* @return resolved skeletons for this locale, defaults to "h" if none found.
*/
private String resolveInputSkeleton(String type) {
var regionToSkeletonMap = inputSkeletons.get(type);
return regionToSkeletonMap.getOrDefault(locale.getLanguage() + "-" + locale.getCountry(),
regionToSkeletonMap.getOrDefault(locale.getCountry(),
regionToSkeletonMap.getOrDefault(locale.getLanguage() + "-001",
regionToSkeletonMap.getOrDefault("001", "h"))));
}
/**
* Replace 'j' and 'C' input skeletons with locale specific patterns. Note that 'j'
* is guaranteed to be replaced with one char [hkHK], while 'C' may be replaced with
* multiple chars. Repeat each as much as 'C' count.
* @param requestedTemplate requested skeleton
* @return skeleton with j/C substituted with concrete patterns
*/
private String substituteInputSkeletons(String requestedTemplate) {
var cCount = requestedTemplate.chars().filter(c -> c == 'C').count();
return requestedTemplate.replaceAll("j", jPattern)
.replaceFirst("C+", CPattern.replaceAll("([hkHK])", "$1".repeat((int)cCount)));
}
/**
* Returns a stream of possible skeletons, inferring standalone/format (M/L and/or E/c) patterns
* and their styles. (cf. 2.6.2.1 Matching Skeletons)
*
* @param skeleton original skeleton
* @return inferred Stream of skeletons in its priority order
*/
private Stream<String> possibleInferred(String skeleton) {
return priorityList(skeleton, "M", "L").stream()
.flatMap(s -> priorityList(s, "E", "c").stream())
.distinct();
}
/**
* Inferring the possible format styles in priority order, based on the original
* skeleton length.
*
* @param skeleton skeleton
* @param pChar pattern character string
* @param subChar substitute character string
* @return list of skeletons
*/
private List<String> priorityList(String skeleton, String pChar, String subChar) {
int first = skeleton.indexOf(pChar);
int last = skeleton.lastIndexOf(pChar);
if (first >= 0) {
var prefix = skeleton.substring(0, first);
var suffix = skeleton.substring(last + 1);
// Priority are based on this chart. First column is the original count of `pChar`,
// then it is followed by inferred skeletons base on priority.
//
// 1->2->3->4 (number form (1-digit) -> number form (2-digit) -> Abbr. form -> Full form)
// 2->1->3->4
// 3->4->2->1
// 4->3->2->1
var o1 = prefix + pChar + suffix;
var o2 = prefix + pChar.repeat(2) + suffix;
var o3 = prefix + pChar.repeat(3) + suffix;
var o4 = prefix + pChar.repeat(4) + suffix;
var s1 = prefix + subChar + suffix;
var s2 = prefix + subChar.repeat(2) + suffix;
var s3 = prefix + subChar.repeat(3) + suffix;
var s4 = prefix + subChar.repeat(4) + suffix;
return switch (last - first) {
case 1 -> List.of(skeleton, o1, o2, o3, o4, s1, s2, s3, s4);
case 2 -> List.of(skeleton, o2, o1, o3, o4, s2, s1, s3, s4);
case 3 -> List.of(skeleton, o3, o4, o2, o1, s3, s4, s2, s1);
default -> List.of(skeleton, o4, o3, o2, o1, s4, s3, s2, s1);
};
} else {
return List.of(skeleton);
}
}
private String getDateTimePattern(String prefix, String key, int styleIndex, String calendarType) {
StringBuilder sb = new StringBuilder();
if (prefix != null) {