diff --git a/make/jdk/src/classes/build/tools/cldrconverter/Bundle.java b/make/jdk/src/classes/build/tools/cldrconverter/Bundle.java index 9f262fa0106..952e28fd43b 100644 --- a/make/jdk/src/classes/build/tools/cldrconverter/Bundle.java +++ b/make/jdk/src/classes/build/tools/cldrconverter/Bundle.java @@ -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 @@ -25,6 +25,9 @@ package build.tools.cldrconverter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; import java.util.EnumSet; @@ -34,6 +37,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.stream.Collectors; import java.util.stream.IntStream; class Bundle { @@ -47,21 +51,21 @@ class Bundle { FORMATDATA); } - private final static Map bundles = new HashMap<>(); + private static final Map bundles = new HashMap<>(); - private final static String[] NUMBER_PATTERN_KEYS = { + private static final String[] NUMBER_PATTERN_KEYS = { "NumberPatterns/decimal", "NumberPatterns/currency", "NumberPatterns/percent", "NumberPatterns/accounting" }; - private final static String[] COMPACT_NUMBER_PATTERN_KEYS = { - "short.CompactNumberPatterns", - "long.CompactNumberPatterns" + private static final String[] COMPACT_NUMBER_PATTERN_KEYS = { + "short.CompactNumberPatterns", + "long.CompactNumberPatterns" }; - private final static String[] NUMBER_ELEMENT_KEYS = { + private static final String[] NUMBER_ELEMENT_KEYS = { "NumberElements/decimal", "NumberElements/group", "NumberElements/list", @@ -77,41 +81,45 @@ class Bundle { "NumberElements/currencyGroup", }; - private final static String[] TIME_PATTERN_KEYS = { + private static final String[] TIME_PATTERN_KEYS = { "DateTimePatterns/full-time", "DateTimePatterns/long-time", "DateTimePatterns/medium-time", "DateTimePatterns/short-time", }; - private final static String[] DATE_PATTERN_KEYS = { + private static final String[] DATE_PATTERN_KEYS = { "DateTimePatterns/full-date", "DateTimePatterns/long-date", "DateTimePatterns/medium-date", "DateTimePatterns/short-date", }; - private final static String[] DATETIME_PATTERN_KEYS = { + private static final String[] DATETIME_PATTERN_KEYS = { "DateTimePatterns/full-dateTime", "DateTimePatterns/long-dateTime", "DateTimePatterns/medium-dateTime", "DateTimePatterns/short-dateTime", }; - private final static String[] ERA_KEYS = { + private static final String[] ERA_KEYS = { "long.Eras", "Eras", "narrow.Eras" }; + // DateFormatItem prefix + static final String DATEFORMATITEM_KEY_PREFIX = "DateFormatItem."; + static final String DATEFORMATITEM_INPUT_REGIONS_PREFIX = "DateFormatItemInputRegions."; + // Keys for individual time zone names - private final static String TZ_GEN_LONG_KEY = "timezone.displayname.generic.long"; - private final static String TZ_GEN_SHORT_KEY = "timezone.displayname.generic.short"; - private final static String TZ_STD_LONG_KEY = "timezone.displayname.standard.long"; - private final static String TZ_STD_SHORT_KEY = "timezone.displayname.standard.short"; - private final static String TZ_DST_LONG_KEY = "timezone.displayname.daylight.long"; - private final static String TZ_DST_SHORT_KEY = "timezone.displayname.daylight.short"; - private final static String[] ZONE_NAME_KEYS = { + private static final String TZ_GEN_LONG_KEY = "timezone.displayname.generic.long"; + private static final String TZ_GEN_SHORT_KEY = "timezone.displayname.generic.short"; + private static final String TZ_STD_LONG_KEY = "timezone.displayname.standard.long"; + private static final String TZ_STD_SHORT_KEY = "timezone.displayname.standard.short"; + private static final String TZ_DST_LONG_KEY = "timezone.displayname.daylight.long"; + private static final String TZ_DST_SHORT_KEY = "timezone.displayname.daylight.short"; + private static final String[] ZONE_NAME_KEYS = { TZ_STD_LONG_KEY, TZ_STD_SHORT_KEY, TZ_DST_LONG_KEY, @@ -262,7 +270,7 @@ class Bundle { CLDRConverter.handleAliases(myMap); // another hack: parentsMap is not used for date-time resources. - if ("root".equals(id)) { + if (isRoot()) { parentsMap = null; } @@ -287,6 +295,14 @@ class Bundle { handleDateTimeFormatPatterns(TIME_PATTERN_KEYS, myMap, parentsMap, calendarType, "TimePatterns"); handleDateTimeFormatPatterns(DATE_PATTERN_KEYS, myMap, parentsMap, calendarType, "DatePatterns"); handleDateTimeFormatPatterns(DATETIME_PATTERN_KEYS, myMap, parentsMap, calendarType, "DateTimePatterns"); + + // Skeleton + handleSkeletonPatterns(myMap, calendarType); + } + + // Skeleton input regions + if (isRoot()) { + skeletonInputRegions(myMap); } // First, weed out any empty timezone or metazone names from myMap. @@ -647,8 +663,9 @@ class Bundle { private void convertDateTimePatternLetter(CalendarType calendarType, char cldrLetter, int count, StringBuilder sb) { switch (cldrLetter) { case 'u': - // Change cldr letter 'u' to 'y', as 'u' is interpreted as - // "Extended year (numeric)" in CLDR/LDML, + case 'U': + // Change cldr letter 'u'/'U' to 'y', as 'u' is interpreted as + // "Extended year (numeric)", and 'U' as "Cyclic year" in CLDR/LDML, // which is not supported in SimpleDateFormat and // j.t.f.DateTimeFormatter, so it is replaced with 'y' // as the best approximation @@ -742,6 +759,19 @@ class Bundle { return false; } + private void handleSkeletonPatterns(Map myMap, CalendarType calendarType) { + String calendarPrefix = calendarType.keyElementName(); + myMap.putAll(myMap.entrySet().stream() + .filter(e -> e.getKey().startsWith(Bundle.DATEFORMATITEM_KEY_PREFIX)) + .collect(Collectors.toMap( + e -> calendarPrefix + e.getKey(), + e -> translateDateFormatLetters(calendarType, + (String)e.getValue(), + this::convertDateTimePatternLetter) + )) + ); + } + @FunctionalInterface private interface ConvertDateTimeLetters { void convert(CalendarType calendarType, char cldrLetter, int count, StringBuilder sb); @@ -790,4 +820,14 @@ class Bundle { } return numArray; } + + private static void skeletonInputRegions(Map myMap) { + myMap.putAll(myMap.entrySet().stream() + .filter(e -> e.getKey().startsWith(Bundle.DATEFORMATITEM_INPUT_REGIONS_PREFIX)) + .collect(Collectors.toMap( + e -> e.getKey(), + e -> ((String)e.getValue()).trim() + )) + ); + } } diff --git a/make/jdk/src/classes/build/tools/cldrconverter/CLDRConverter.java b/make/jdk/src/classes/build/tools/cldrconverter/CLDRConverter.java index 267f93e733b..abf1be64306 100644 --- a/make/jdk/src/classes/build/tools/cldrconverter/CLDRConverter.java +++ b/make/jdk/src/classes/build/tools/cldrconverter/CLDRConverter.java @@ -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 @@ -847,20 +847,19 @@ public class CLDRConverter { "DateTimePatternChars", "PluralRules", "DayPeriodRules", + "DateFormatItem", }; private static Map extractFormatData(Map map, String id) { Map formatData = new LinkedHashMap<>(); for (CalendarType calendarType : CalendarType.values()) { - if (calendarType == CalendarType.GENERIC) { - continue; - } String prefix = calendarType.keyElementName(); - for (String element : FORMAT_DATA_ELEMENTS) { - String key = prefix + element; - copyIfPresent(map, "java.time." + key, formatData); - copyIfPresent(map, key, formatData); - } + Arrays.stream(FORMAT_DATA_ELEMENTS) + .flatMap(elem -> map.keySet().stream().filter(k -> k.startsWith(prefix + elem))) + .forEach(key -> { + copyIfPresent(map, "java.time." + key, formatData); + copyIfPresent(map, key, formatData); + }); } for (String key : map.keySet()) { @@ -868,9 +867,6 @@ public class CLDRConverter { if (key.startsWith(CLDRConverter.LOCALE_TYPE_PREFIX_CA)) { String type = key.substring(CLDRConverter.LOCALE_TYPE_PREFIX_CA.length()); for (CalendarType calendarType : CalendarType.values()) { - if (calendarType == CalendarType.GENERIC) { - continue; - } if (type.equals(calendarType.lname())) { Object value = map.get(key); String dataKey = key.replace(LOCALE_TYPE_PREFIX_CA, diff --git a/make/jdk/src/classes/build/tools/cldrconverter/LDMLParseHandler.java b/make/jdk/src/classes/build/tools/cldrconverter/LDMLParseHandler.java index 745796d96cf..668c187c383 100644 --- a/make/jdk/src/classes/build/tools/cldrconverter/LDMLParseHandler.java +++ b/make/jdk/src/classes/build/tools/cldrconverter/LDMLParseHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2020, 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 @@ -756,6 +756,14 @@ class LDMLParseHandler extends AbstractLDMLHandler { pushStringEntry(qName, attributes, prefix + "DateTimePatterns/" + attributes.getValue("type") + "-dateTime"); } break; + case "dateFormatItem": + { + // for FormatData + String prefix = (currentCalendarType == null) ? "" : currentCalendarType.keyElementName(); + pushStringEntry(qName, attributes, + prefix + Bundle.DATEFORMATITEM_KEY_PREFIX + attributes.getValue("id")); + } + break; case "localizedPatternChars": { // for FormatData @@ -1113,7 +1121,7 @@ class LDMLParseHandler extends AbstractLDMLHandler { if (id.equals("root") && key.startsWith("MonthNames")) { value = new DateFormatSymbols(Locale.US).getShortMonths(); } - return put(entry.getKey(), value); + return put(key, value); } } return null; diff --git a/make/jdk/src/classes/build/tools/cldrconverter/SupplementDataParseHandler.java b/make/jdk/src/classes/build/tools/cldrconverter/SupplementDataParseHandler.java index fdc8a977766..11547ccb38e 100644 --- a/make/jdk/src/classes/build/tools/cldrconverter/SupplementDataParseHandler.java +++ b/make/jdk/src/classes/build/tools/cldrconverter/SupplementDataParseHandler.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2012, 2017, 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 @@ -27,8 +27,12 @@ package build.tools.cldrconverter; import java.io.File; import java.io.IOException; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import java.util.SortedSet; +import java.util.TreeSet; +import java.util.stream.Collectors; import org.xml.sax.Attributes; import org.xml.sax.InputSource; import org.xml.sax.SAXException; @@ -62,10 +66,15 @@ class SupplementDataParseHandler extends AbstractLDMLHandler { // parentLocale.=(" ")+ private final Map parentLocalesMap; + // Input Skeleton map for "preferred" and "allowed" + // Map<"preferred"/"allowed", Map<"skeleton", SortedSet<"regions">>> + private final Map>> inputSkeletonMap; + SupplementDataParseHandler() { firstDayMap = new HashMap<>(); minDaysMap = new HashMap<>(); parentLocalesMap = new HashMap<>(); + inputSkeletonMap = new HashMap<>(); } /** @@ -76,22 +85,25 @@ class SupplementDataParseHandler extends AbstractLDMLHandler { * It returns null when there is no firstDay and minDays for the country * although this should not happen because supplementalData.xml includes * default value for the world ("001") for firstDay and minDays. + * + * This method also returns Maps for "preferred" and "allowed" skeletons, + * which are grouped by regions. E.g, "h:XX YY ZZ;" which means 'h' pattern + * is "preferred"/"allowed" in "XX", "YY", and "ZZ" regions. */ Map getData(String id) { Map values = new HashMap<>(); if ("root".equals(id)) { - parentLocalesMap.keySet().forEach(key -> { - values.put(CLDRConverter.PARENT_LOCALE_PREFIX+key, - parentLocalesMap.get(key)); - }); - firstDayMap.keySet().forEach(key -> { - values.put(CLDRConverter.CALENDAR_FIRSTDAY_PREFIX+firstDayMap.get(key), - key); - }); - minDaysMap.keySet().forEach(key -> { - values.put(CLDRConverter.CALENDAR_MINDAYS_PREFIX+minDaysMap.get(key), - key); - }); + parentLocalesMap.forEach((k, v) -> values.put(CLDRConverter.PARENT_LOCALE_PREFIX + k, v)); + firstDayMap.forEach((k, v) -> values.put(CLDRConverter.CALENDAR_FIRSTDAY_PREFIX + v, k)); + minDaysMap.forEach((k, v) -> values.put(CLDRConverter.CALENDAR_MINDAYS_PREFIX + v, k)); + inputSkeletonMap.get("preferred").forEach((k, v) -> + values.merge(Bundle.DATEFORMATITEM_INPUT_REGIONS_PREFIX + "preferred", + k + ":" + v.stream().collect(Collectors.joining(" ")) + ";", + (old, newVal) -> old + (String)newVal)); + inputSkeletonMap.get("allowed").forEach((k, v) -> + values.merge(Bundle.DATEFORMATITEM_INPUT_REGIONS_PREFIX + "allowed", + k + ":" + v.stream().collect(Collectors.joining(" ")) + ";", + (old, newVal) -> old + (String)newVal)); } return values.isEmpty() ? null : values; } @@ -158,11 +170,23 @@ class SupplementDataParseHandler extends AbstractLDMLHandler { attributes.getValue("locales").replaceAll("_", "-")); } break; + case "hours": + if (!isIgnored(attributes)) { + var preferred = attributes.getValue("preferred"); + var allowed = attributes.getValue("allowed").replaceFirst(" .*", "").replaceFirst("b", "B"); // take only the first one, "b" -> "B" + var regions = Arrays.stream(attributes.getValue("regions").split(" ")) + .map(r -> r.replaceAll("_", "-")) + .collect(Collectors.toSet()); + var pmap = inputSkeletonMap.computeIfAbsent("preferred", k -> new HashMap<>()); + var amap = inputSkeletonMap.computeIfAbsent("allowed", k -> new HashMap<>()); + pmap.computeIfAbsent(preferred, k -> new TreeSet<>()).addAll(regions); + amap.computeIfAbsent(allowed, k -> new TreeSet<>()).addAll(regions); + } + break; default: // treat anything else as a container pushContainer(qName, attributes); break; } } - } diff --git a/src/java.base/share/classes/java/time/format/DateTimeFormatter.java b/src/java.base/share/classes/java/time/format/DateTimeFormatter.java index 063b6e14a00..0a67e22c0f3 100644 --- a/src/java.base/share/classes/java/time/format/DateTimeFormatter.java +++ b/src/java.base/share/classes/java/time/format/DateTimeFormatter.java @@ -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 Patterns for + * Formatting and Parsing section. Other pattern symbols in the requested template are + * invalid. + *

+ * The mapping of the requested template to the closest of the available localized formats + * is defined by the + * + * Unicode LDML specification. 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}. + *

+ * 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. + *

+ * 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 diff --git a/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java b/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java index 40c0de53d3f..62ea488ede7 100644 --- a/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java +++ b/src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java @@ -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. + *

+ * If the locale contains the "rg" (region override) + * Unicode extensions, + * the formatting pattern is overridden with the one appropriate for the region. + *

+ * 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. + *

+ * 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: + *

    + *
  • the {@code requestedTemplate} specified to this method + *
  • the {@code Locale} of the {@code DateTimeFormatter} + *
  • the {@code Chronology} of the {@code DateTimeFormatter} unless overridden + *
+ * During formatting, the chronology is obtained from the temporal object + * being formatted, which may have been overridden by + * {@link DateTimeFormatter#withChronology(Chronology)}. + *

+ * 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. + *

+ * 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 + * Patterns for Formatting and Parsing section. + * Other pattern symbols in the requested template are invalid. + *

+ * The mapping of the requested template to the closest of the available localized formats + * is defined by the + * + * Unicode LDML specification. 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 printerParsers, boolean optional) { + private CompositePrinterParser(List 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 preferredZones, boolean isGeneric) { + private ZoneTextPrinterParser(TextStyle textStyle, Set 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 query; private final String description; - ZoneIdPrinterParser(TemporalQuery query, String description) { + private ZoneIdPrinterParser(TemporalQuery 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. *

- * 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; } diff --git a/src/java.base/share/classes/sun/text/spi/JavaTimeDateTimePatternProvider.java b/src/java.base/share/classes/sun/text/spi/JavaTimeDateTimePatternProvider.java index a3835c6ac82..58235c9dff0 100644 --- a/src/java.base/share/classes/sun/text/spi/JavaTimeDateTimePatternProvider.java +++ b/src/java.base/share/classes/sun/text/spi/JavaTimeDateTimePatternProvider.java @@ -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); + } } diff --git a/src/java.base/share/classes/sun/util/locale/provider/JavaTimeDateTimePatternImpl.java b/src/java.base/share/classes/sun/util/locale/provider/JavaTimeDateTimePatternImpl.java index 41de2dd7f65..c39cea799b3 100644 --- a/src/java.base/share/classes/sun/util/locale/provider/JavaTimeDateTimePatternImpl.java +++ b/src/java.base/share/classes/sun/util/locale/provider/JavaTimeDateTimePatternImpl.java @@ -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 diff --git a/src/java.base/share/classes/sun/util/locale/provider/LocaleResources.java b/src/java.base/share/classes/sun/util/locale/provider/LocaleResources.java index fe84b66b167..aa991359c68 100644 --- a/src/java.base/share/classes/sun/util/locale/provider/LocaleResources.java +++ b/src/java.base/share/classes/sun/util/locale/provider/LocaleResources.java @@ -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( + "(?" + + "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 + "(?