8218948: SimpleDateFormat :: format - Zone Names are not reflected correctly during run time
Reviewed-by: lancea, rgoel
--- a/src/java.base/share/classes/sun/util/cldr/CLDRTimeZoneNameProviderImpl.java Thu Mar 07 12:15:48 2019 -0800
+++ b/src/java.base/share/classes/sun/util/cldr/CLDRTimeZoneNameProviderImpl.java Thu Mar 07 12:56:48 2019 -0800
@@ -73,31 +73,28 @@
@Override
protected String[] getDisplayNameArray(String id, Locale locale) {
- String tzid = TimeZoneNameUtility.canonicalTZID(id).orElse(id);
- String[] namesSuper = super.getDisplayNameArray(tzid, locale);
+ // Use English for the ROOT locale
+ locale = locale.equals(Locale.ROOT) ? Locale.ENGLISH : locale;
+ String[] namesSuper = super.getDisplayNameArray(id, locale);
- if (Objects.nonNull(namesSuper)) {
+ if (namesSuper == null) {
+ // try canonical id instead
+ namesSuper = super.getDisplayNameArray(
+ TimeZoneNameUtility.canonicalTZID(id).orElse(id),
+ locale);
+ }
+
+ if (namesSuper != null) {
// CLDR's resource bundle has an translated entry for this id.
// Fix up names if needed, either missing or no-inheritance
namesSuper[INDEX_TZID] = id;
- // Check if standard long name exists. If not, try to retrieve the name
- // from language only locale resources. E.g., "Europe/London"
- // for en-GB only contains DST names
- if (!exists(namesSuper, INDEX_STD_LONG) && !locale.getCountry().isEmpty()) {
- String[] names =
- getDisplayNameArray(id, Locale.forLanguageTag(locale.getLanguage()));
- if (exists(names, INDEX_STD_LONG)) {
- namesSuper[INDEX_STD_LONG] = names[INDEX_STD_LONG];
- }
- }
-
for(int i = INDEX_STD_LONG; i < namesSuper.length; i++) { // index 0 is the 'id' itself
switch (namesSuper[i]) {
case "":
// Fill in empty elements
deriveFallbackName(namesSuper, i, locale,
- namesSuper[INDEX_DST_LONG].isEmpty());
+ !exists(namesSuper, INDEX_DST_LONG));
break;
case NO_INHERITANCE_MARKER:
// CLDR's "no inheritance marker"
@@ -141,16 +138,19 @@
// Derive fallback time zone name according to LDML's logic
private void deriveFallbackNames(String[] names, Locale locale) {
+ boolean noDST = !exists(names, INDEX_DST_LONG);
for (int i = INDEX_STD_LONG; i <= INDEX_GEN_SHORT; i++) {
- deriveFallbackName(names, i, locale, false);
+ deriveFallbackName(names, i, locale, noDST);
}
}
private void deriveFallbackName(String[] names, int index, Locale locale, boolean noDST) {
+ String id = names[INDEX_TZID];
+
if (exists(names, index)) {
if (names[index].equals(NO_INHERITANCE_MARKER)) {
// CLDR's "no inheritance marker"
- names[index] = toGMTFormat(names[INDEX_TZID],
+ names[index] = toGMTFormat(id,
index == INDEX_DST_LONG || index == INDEX_DST_SHORT,
index % 2 != 0, locale);
}
@@ -160,8 +160,8 @@
// Check if COMPAT can substitute the name
if (LocaleProviderAdapter.getAdapterPreference().contains(Type.JRE)) {
String[] compatNames = (String[])LocaleProviderAdapter.forJRE()
- .getLocaleResources(locale)
- .getTimeZoneNames(names[INDEX_TZID]);
+ .getLocaleResources(mapChineseLocale(locale))
+ .getTimeZoneNames(id);
if (compatNames != null) {
for (int i = INDEX_STD_LONG; i <= INDEX_GEN_SHORT; i++) {
// Assumes COMPAT has no empty slots
@@ -184,9 +184,8 @@
}
// last resort
- String id = names[INDEX_TZID].toUpperCase(Locale.ROOT);
- if (!id.startsWith("UT")) {
- names[index] = toGMTFormat(names[INDEX_TZID],
+ if (!id.toUpperCase(Locale.ROOT).startsWith("UT")) {
+ names[index] = toGMTFormat(id,
index == INDEX_DST_LONG || index == INDEX_DST_SHORT,
index % 2 != 0,
locale);
@@ -290,4 +289,33 @@
String.format(l, hourFormat, offset / 60, offset % 60));
}
}
+
+ // Mapping CLDR's Simplified/Traditional Chinese resources
+ // to COMPAT's zh-CN/TW
+ private Locale mapChineseLocale(Locale locale) {
+ if (locale.getLanguage() == "zh") {
+ switch (locale.getScript()) {
+ case "Hans":
+ return Locale.CHINA;
+ case "Hant":
+ return Locale.TAIWAN;
+ case "":
+ // no script, guess from country code.
+ switch (locale.getCountry()) {
+ case "":
+ case "CN":
+ case "SG":
+ return Locale.CHINA;
+ case "HK":
+ case "MO":
+ case "TW":
+ return Locale.TAIWAN;
+ }
+ break;
+ }
+ }
+
+ // no need to map
+ return locale;
+ }
}
--- a/src/java.base/share/classes/sun/util/locale/provider/LocaleResources.java Thu Mar 07 12:15:48 2019 -0800
+++ b/src/java.base/share/classes/sun/util/locale/provider/LocaleResources.java Thu Mar 07 12:56:48 2019 -0800
@@ -1,5 +1,5 @@
/*
- * Copyright (c) 2012, 2018, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2019, 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
@@ -56,7 +56,6 @@
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import sun.security.action.GetPropertyAction;
-import sun.util.calendar.ZoneInfo;
import sun.util.resources.LocaleData;
import sun.util.resources.OpenListResourceBundle;
import sun.util.resources.ParallelListResourceBundle;
@@ -271,17 +270,31 @@
if (Objects.isNull(data) || Objects.isNull(val = data.get())) {
TimeZoneNamesBundle tznb = localeData.getTimeZoneNames(locale);
- if (tznb.containsKey(key)) {
- if (key.startsWith(TZNB_EXCITY_PREFIX)) {
+ if (key.startsWith(TZNB_EXCITY_PREFIX)) {
+ if (tznb.containsKey(key)) {
val = tznb.getString(key);
assert val instanceof String;
trace("tznb: %s key: %s, val: %s\n", tznb, key, val);
+ }
+ } else {
+ String[] names = null;
+ if (tznb.containsKey(key)) {
+ names = tznb.getStringArray(key);
} else {
- String[] names = tznb.getStringArray(key);
+ var tz = TimeZoneNameUtility.canonicalTZID(key).orElse(key);
+ if (tznb.containsKey(tz)) {
+ names = tznb.getStringArray(tz);
+ }
+ }
+
+ if (names != null) {
+ names[0] = key;
trace("tznb: %s key: %s, names: %s, %s, %s, %s, %s, %s, %s\n", tznb, key,
names[0], names[1], names[2], names[3], names[4], names[5], names[6]);
val = names;
}
+ }
+ if (val != null) {
cache.put(cacheKey,
new ResourceReference(cacheKey, val, referenceQueue));
}
@@ -321,8 +334,6 @@
}
if (type == LocaleProviderAdapter.Type.CLDR) {
- // Add aliases data for CLDR
- Map<String, String> aliases = ZoneInfo.getAliasTable();
// Note: TimeZoneNamesBundle creates a String[] on each getStringArray call.
// Add timezones which are not present in this keyset,
@@ -335,9 +346,10 @@
if (keyset.contains(tzid)) {
val = rb.getStringArray(tzid);
} else {
- String tz = aliases.get(tzid);
- if (keyset.contains(tz)) {
- val = rb.getStringArray(tz);
+ var canonID = TimeZoneNameUtility.canonicalTZID(tzid)
+ .orElse(tzid);
+ if (keyset.contains(canonID)) {
+ val = rb.getStringArray(canonID);
}
}
val[0] = tzid;
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/test/jdk/java/text/Format/DateFormat/SDFTCKZoneNamesTest.java Thu Mar 07 12:56:48 2019 -0800
@@ -0,0 +1,368 @@
+/*
+ * Copyright (c) 2019, 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 8218948
+ * @summary TCK tests that check the time zone names between DFS.getZoneStrings()
+ * and SDF.format("z*")
+ * @run main SDFTCKZoneNamesTest
+ */
+import java.text.*;
+import java.util.Calendar;
+import java.util.Date;
+import java.util.List;
+import java.util.Locale;
+import java.util.TimeZone;
+
+public class SDFTCKZoneNamesTest {
+
+ StringBuffer myFormat(Date date, SimpleDateFormat sdf) {
+ String pattern = sdf.toPattern();
+ StringBuffer toAppendTo = new StringBuffer("");
+ boolean inQuote = false;
+ char prevCh = 0;
+ char ch;
+ int count = 0;
+ for (int i = 0; i < pattern.length(); i++) {
+ ch = pattern.charAt(i);
+ if (inQuote) {
+ if (ch == '\'') {
+ inQuote = false;
+ if (count == 0) toAppendTo.append(ch);
+ else count = 0;
+ } else {
+ toAppendTo.append(ch);
+ count++;
+ }
+ } else { // not inQuote
+ if (ch == '\'') {
+ inQuote = true;
+ if (count > 0) {
+ toAppendTo.append(subFormat(prevCh, count, date, sdf));
+ count = 0;
+ prevCh = 0;
+ }
+ } else if (ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z') {
+ if (ch != prevCh && count > 0) {
+ toAppendTo.append(subFormat(prevCh, count, date, sdf));
+ prevCh = ch;
+ count = 1;
+ } else {
+ if (ch != prevCh) prevCh = ch;
+ count++;
+ }
+ } else if (count > 0) {
+ toAppendTo.append(subFormat(prevCh, count, date, sdf));
+ toAppendTo.append(ch);
+ prevCh = 0;
+ count = 0;
+ } else toAppendTo.append(ch);
+ }
+ }
+ if (count > 0) {
+ toAppendTo.append(subFormat(prevCh, count, date, sdf));
+ }
+ return toAppendTo;
+ }
+
+ private String subFormat(char ch, int count, Date date, SimpleDateFormat sdf)
+ throws IllegalArgumentException {
+ int value = 0;
+ int patternCharIndex = -1;
+ int maxIntCount = 10;
+ String current = "";
+ DateFormatSymbols formatData = sdf.getDateFormatSymbols();
+ Calendar calendar = sdf.getCalendar();
+ calendar.setTime(date);
+ NumberFormat nf = sdf.getNumberFormat();
+ nf.setGroupingUsed(false);
+
+ if ((patternCharIndex = "GyMdkHmsSEDFwWahKz".indexOf(ch)) == -1)
+ throw new IllegalArgumentException("Illegal pattern character " +
+ "'" + ch + "'");
+ switch (patternCharIndex) {
+ case 0: // 'G' - ERA
+ value = calendar.get(Calendar.ERA);
+ current = formatData.getEras()[value];
+ break;
+ case 1: // 'y' - YEAR
+ value = calendar.get(Calendar.YEAR);
+
+ if (count == 2) {
+ // For formatting, if the number of pattern letters is 2,
+ // the year is truncated to 2 digits;
+ current = zeroPaddingNumber(value, 2, 2, nf);
+ } else {
+ // otherwise it is interpreted as a number.
+ current = zeroPaddingNumber(value, count, maxIntCount, nf);
+ }
+
+ break;
+ case 2: // 'M' - MONTH
+ value = calendar.get(Calendar.MONTH);
+ if (count >= 4)
+ // DateFormatSymbols::getMonths spec: "If the language requires different forms for formatting
+ // and stand-alone usages, this method returns month names in the formatting form."
+ // Because of that only formatting cases patterns may be tested. Like, "MMMM yyyy". Wrong
+ // pattern: "MMMM".
+ current = formatData.getMonths()[value];
+ else if (count == 3)
+ // DateFormatSymbols::getShortMonths spec: "If the language requires different forms for formatting
+ // and stand-alone usages, This method returns short month names in the formatting form."
+ // Because of that only formatting cases patterns may be tested. Like, "MMM yyyy". Wrong pattern:
+ // "MMM".
+ current = formatData.getShortMonths()[value];
+ else
+ current = zeroPaddingNumber(value + 1, count, maxIntCount, nf);
+ break;
+ case 3: // 'd' - DATE
+ value = calendar.get(Calendar.DATE);
+ current = zeroPaddingNumber(value, count, maxIntCount, nf);
+ break;
+ case 4: // 'k' - HOUR_OF_DAY: 1-based. eg, 23:59 + 1 hour =>> 24:59
+ if ((value = calendar.get(Calendar.HOUR_OF_DAY)) == 0)
+ current = zeroPaddingNumber(
+ calendar.getMaximum(Calendar.HOUR_OF_DAY) + 1,
+ count, maxIntCount, nf);
+ else
+ current = zeroPaddingNumber(value, count, maxIntCount, nf);
+ break;
+ case 5: // 'H' - HOUR_OF_DAY:0-based. eg, 23:59 + 1 hour =>> 00:59
+ value = calendar.get(Calendar.HOUR_OF_DAY);
+ current = zeroPaddingNumber(value, count, maxIntCount, nf);
+ break;
+ case 6: // 'm' - MINUTE
+ value = calendar.get(Calendar.MINUTE);
+ current = zeroPaddingNumber(value, count, maxIntCount, nf);
+ break;
+ case 7: // 's' - SECOND
+ value = calendar.get(Calendar.SECOND);
+ current = zeroPaddingNumber(value, count, maxIntCount, nf);
+ break;
+ case 8: // 'S' - MILLISECOND
+ value = calendar.get(Calendar.MILLISECOND);
+ /*
+ if (count > 3)
+ value = value * (int) Math.pow(10, count - 3);
+ else if (count == 2)
+ value = (value + 5) / 10;
+ else if (count == 1)
+ value = (value + 50) / 100;
+ */
+ current = zeroPaddingNumber(value, count, maxIntCount, nf);
+ break;
+ case 9: // 'E' - DAY_OF_WEEK
+ value = calendar.get(Calendar.DAY_OF_WEEK);
+ if (count >= 4)
+ current = formatData.getWeekdays()[value];
+ else // count < 4, use abbreviated form if exists
+ current = formatData.getShortWeekdays()[value];
+ break;
+ case 10: // 'D' - DAY_OF_YEAR
+ value = calendar.get(Calendar.DAY_OF_YEAR);
+ current = zeroPaddingNumber(value, count, maxIntCount, nf);
+ break;
+ case 11: // 'F' - DAY_OF_WEEK_IN_MONTH
+ value = calendar.get(Calendar.DAY_OF_WEEK_IN_MONTH);
+ current = zeroPaddingNumber(value, count, maxIntCount, nf);
+ break;
+ case 12: // 'w' - WEEK_OF_YEAR
+ value = calendar.get(Calendar.WEEK_OF_YEAR);
+ current = zeroPaddingNumber(value, count, maxIntCount, nf);
+ break;
+ case 13: // 'W' - WEEK_OF_MONTH
+ value = calendar.get(Calendar.WEEK_OF_MONTH);
+ current = zeroPaddingNumber(value, count, maxIntCount, nf);
+ break;
+ case 14: // 'a' - AM_PM
+ value = calendar.get(Calendar.AM_PM);
+ current = formatData.getAmPmStrings()[value];
+ break;
+ case 15: // 'h' - HOUR:1-based. eg, 11PM + 1 hour =>> 12 AM
+ if ((value = calendar.get(Calendar.HOUR)) == 0)
+ current = zeroPaddingNumber(
+ calendar.getLeastMaximum(Calendar.HOUR) + 1,
+ count, maxIntCount, nf);
+ else
+ current = zeroPaddingNumber(value, count, maxIntCount, nf);
+ break;
+ case 16: // 'K' - HOUR: 0-based. eg, 11PM + 1 hour =>> 0 AM
+ value = calendar.get(Calendar.HOUR);
+ current = zeroPaddingNumber(value, count, maxIntCount, nf);
+ break;
+ case 17: // 'z' - ZONE_OFFSET
+ int zoneIndex = getZoneIndex(calendar.getTimeZone().getID(), formatData);
+ if (zoneIndex == -1) {
+ StringBuffer zoneString = new StringBuffer();
+ value = calendar.get(Calendar.ZONE_OFFSET)
+ + calendar.get(Calendar.DST_OFFSET);
+ if (value < 0) {
+ zoneString.append("GMT-");
+ value = -value; // suppress the '-' sign for text display.
+ } else
+ zoneString.append("GMT+");
+ zoneString.append(
+ zeroPaddingNumber((int) (value / (60 * 60 * 1000)), 2, 2, nf));
+ zoneString.append(':');
+ zoneString.append(
+ zeroPaddingNumber(
+ (int) ((value % (60 * 60 * 1000)) / (60 * 1000)), 2, 2, nf));
+ current = zoneString.toString();
+ } else if (calendar.get(Calendar.DST_OFFSET) != 0) {
+ if (count >= 4)
+ current = formatData.getZoneStrings()[zoneIndex][3];
+ else
+ // count < 4, use abbreviated form if exists
+ current = formatData.getZoneStrings()[zoneIndex][4];
+ } else {
+ if (count >= 4)
+ current = formatData.getZoneStrings()[zoneIndex][1];
+ else
+ current = formatData.getZoneStrings()[zoneIndex][2];
+ }
+ break;
+ }
+
+ return current;
+ }
+
+
+ String zeroPaddingNumber(long value, int minDigits, int maxDigits,
+ NumberFormat nf) {
+ nf.setMinimumIntegerDigits(minDigits);
+ nf.setMaximumIntegerDigits(maxDigits);
+ return nf.format(value);
+ }
+
+
+ int getZoneIndex(String ID, DateFormatSymbols dfs) {
+ String[][] zoneStrings = dfs.getZoneStrings();
+
+ for (int index = 0; index < zoneStrings.length; index++) {
+ if (ID.equalsIgnoreCase(zoneStrings[index][0])) return index;
+ }
+ return -1;
+ }
+
+
+ final int second = 1000;
+ final int minute = 60 * second;
+ final int hour = 60 * minute;
+ final int day = 24 * hour;
+ final int month = 30 * day;
+ final int year = 365 * day;
+ final int someday = 30 * year + 3 * month + 19 * day + 5 * hour;
+
+
+ /* standalone interface */
+ public static void main(String argv[]) {
+ Locale defaultLocale = Locale.getDefault();
+ SDFTCKZoneNamesTest test = new SDFTCKZoneNamesTest();
+
+ try {
+ List.of(Locale.ROOT,
+ Locale.CHINA,
+ Locale.forLanguageTag("es-419"),
+ Locale.GERMANY,
+ Locale.forLanguageTag("hi-IN"),
+ Locale.JAPAN,
+ Locale.TAIWAN,
+ Locale.UK,
+ Locale.US,
+ Locale.forLanguageTag("uz-Cyrl-UZ"),
+ Locale.forLanguageTag("zh-SG"),
+ Locale.forLanguageTag("zh-HK"),
+ Locale.forLanguageTag("zh-MO")).stream()
+ .forEach(l -> {
+ System.out.printf("Testing locale: %s%n", l);
+ Locale.setDefault(l);
+ test.SimpleDateFormat0062();
+ });
+ } finally {
+ Locale.setDefault(defaultLocale);
+ }
+ }
+
+
+ /**
+ * Equivalence class partitioning
+ * with state, input and output values orientation
+ * for public StringBuffer format(Date date, StringBuffer result, FieldPosition fp),
+ * <br><b>pre-conditions</b>: patterns: { "'s0mething'z mm::hh,yyyy zz",
+ * "zzzz",
+ * "z"} (each pattern contains letter for TIMEZONE_FIELD),
+ * <br><b>date</b>: a Date object
+ * <br><b>result</b>: a string
+ * <br><b>fp</b>: a FieldPosition object with TIMEZONE_FIELD field
+ * <br><b>output</b>: formatted date as expected.
+ */
+ public void SimpleDateFormat0062() {
+ boolean passed = true;
+ String patterns[] = {"'s0mething'z mm::hh,yyyy zz",
+ "zzzz",
+ "z"};
+ SimpleDateFormat sdf = new SimpleDateFormat();
+ Date date = new Date(1234567890);
+ for (String[] tz : sdf.getDateFormatSymbols().getZoneStrings()) {
+ sdf.setTimeZone(TimeZone.getTimeZone(tz[0]));
+ for (int i = 0; i < patterns.length && passed; i++) {
+ StringBuffer result = new StringBuffer("qwerty");
+ FieldPosition fp = new FieldPosition(DateFormat.TIMEZONE_FIELD);
+ sdf.applyPattern(patterns[i]);
+ String expected = new
+ StringBuffer("qwerty").append(myFormat(date,
+ sdf)).toString();
+ String formatted = sdf.format(date, result, fp).toString();
+
+ if (!expected.equals(formatted)) {
+ System.out.println(
+ "method format(date, StringBuffer, FieldPosition) formats wrong");
+ System.out.println(" pattern: " + patterns[i]);
+ System.out.println(" time zone ID: " + tz[0]);
+ System.out.println(" expected result: " + expected);
+ System.out.println(" formatted result: " + formatted);
+ passed = false;
+ }
+
+ if (passed && !expected.equals(result.toString())) {
+ System.out.println(
+ "method format(Date date, StringBuffer toAppendTo, FieldPosition fp) toAppendTo is not " +
+ "equal to output");
+ System.out.println(" pattern: " + patterns[i]);
+ System.out.println(" time zone ID: " + tz[0]);
+ System.out.println(" toAppendTo : " + result);
+ System.out.println(" formatted date: " + formatted);
+ passed = false;
+ }
+ }
+ }
+ if(passed)
+ {
+ System.out.println("PASSED : OKAY");
+ }else
+ {
+ throw new RuntimeException("FAILED");
+ }
+ }
+}