Skip to content

Commit 13633c0

Browse files
committed
8381379: Support std/dstOffset attributes in CLDR's metazone definitions
Reviewed-by: jlu
1 parent 67f590d commit 13633c0

9 files changed

Lines changed: 103 additions & 23 deletions

File tree

make/jdk/src/classes/build/tools/cldrconverter/CLDRConverter.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ public class CLDRConverter {
8787
static final String EXEMPLAR_CITY_PREFIX = "timezone.excity.";
8888
static final String ZONE_NAME_PREFIX = "timezone.displayname.";
8989
static final String METAZONE_ID_PREFIX = "metazone.id.";
90+
static final String METAZONE_DSTOFFSET_PREFIX = "metazone.dstoffset.";
9091
static final String PARENT_LOCALE_PREFIX = "parentLocale.";
9192
static final String LIKELY_SCRIPT_PREFIX = "likelyScript.";
9293
static final String META_EMPTY_ZONE_NAME = "EMPTY_ZONE";
@@ -139,6 +140,11 @@ public class CLDRConverter {
139140
private static final Map<String, String> tzdbSubstLetters = HashMap.newHashMap(512);
140141
private static final Map<String, String> tzdbLinks = HashMap.newHashMap(512);
141142

143+
// Map of explicit dst offsets for metazones
144+
// key: time zone ID
145+
// value: explicit dstOffset for the corresponding metazone name
146+
static final Map<String, String> explicitDstOffsets = HashMap.newHashMap(32);
147+
142148
static enum DraftType {
143149
UNCONFIRMED,
144150
PROVISIONAL,
@@ -867,6 +873,12 @@ private static Map<String, Object> extractZoneNames(Map<String, Object> map, Str
867873
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
868874
names.putAll(exCities);
869875

876+
// Explicit metazone offsets
877+
if (id.equals("root")) {
878+
explicitDstOffsets.forEach((k, v) ->
879+
names.put(METAZONE_DSTOFFSET_PREFIX + k, v));
880+
}
881+
870882
// If there's no UTC entry at this point, add an empty one
871883
if (!names.isEmpty() && !names.containsKey("UTC")) {
872884
names.putIfAbsent(METAZONE_ID_PREFIX + META_EMPTY_ZONE_NAME, EMPTY_ZONE);

make/jdk/src/classes/build/tools/cldrconverter/MetaZonesParseHandler.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2012, 2020, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2012, 2026, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -84,7 +84,15 @@ public void startElement(String uri, String localName, String qName, Attributes
8484

8585
if (fromLDT.isBefore(now) && toLDT.isAfter(now)) {
8686
metazone = attributes.getValue("mzone");
87+
88+
// Explicit metazone DST offsets. Only the "dst" offset is needed,
89+
// as "std" is used by default when it doesn't match.
90+
String dstOffset = attributes.getValue("dstOffset");
91+
if (dstOffset != null) {
92+
CLDRConverter.explicitDstOffsets.put(tzid, dstOffset);
93+
}
8794
}
95+
8896
pushIgnoredContainer(qName);
8997
break;
9098

make/jdk/src/classes/build/tools/cldrconverter/ResourceBundleGenerator.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2012, 2024, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2012, 2026, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -198,7 +198,8 @@ public void generateBundle(String packageName, String baseName, String localeID,
198198
} else if (value instanceof String) {
199199
String valStr = (String)value;
200200
if (type == BundleType.TIMEZONE &&
201-
!key.startsWith(CLDRConverter.EXEMPLAR_CITY_PREFIX) ||
201+
!(key.startsWith(CLDRConverter.EXEMPLAR_CITY_PREFIX) ||
202+
key.startsWith(CLDRConverter.METAZONE_DSTOFFSET_PREFIX)) ||
202203
valStr.startsWith(META_VALUE_PREFIX)) {
203204
out.printf(" { \"%s\", %s },\n", key, CLDRConverter.saveConvert(valStr, useJava));
204205
} else {

src/java.base/share/classes/java/text/SimpleDateFormat.java

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 1996, 2025, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 1996, 2026, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -41,7 +41,7 @@
4141
import java.io.IOException;
4242
import java.io.InvalidObjectException;
4343
import java.io.ObjectInputStream;
44-
import static java.text.DateFormatSymbols.*;
44+
import java.time.ZoneOffset;
4545
import java.util.Calendar;
4646
import java.util.Date;
4747
import java.util.GregorianCalendar;
@@ -57,6 +57,8 @@
5757
import sun.util.locale.provider.LocaleProviderAdapter;
5858
import sun.util.locale.provider.TimeZoneNameUtility;
5959

60+
import static java.text.DateFormatSymbols.*;
61+
6062
/**
6163
* {@code SimpleDateFormat} is a concrete class for formatting and
6264
* parsing dates in a locale-sensitive manner. It allows for formatting
@@ -1293,15 +1295,22 @@ private void subFormat(int patternCharIndex, int count,
12931295

12941296
case PATTERN_ZONE_NAME: // 'z'
12951297
if (current == null) {
1298+
TimeZone tz = calendar.getTimeZone();
1299+
String tzid = tz.getID();
1300+
int zoneOffset = calendar.get(Calendar.ZONE_OFFSET);
1301+
int dstOffset = calendar.get(Calendar.DST_OFFSET) + zoneOffset;
1302+
1303+
// Check if an explicit metazone DST offset exists
1304+
String explicitDstOffset = TimeZoneNameUtility.explicitDstOffset(tzid);
1305+
boolean daylight = explicitDstOffset != null ?
1306+
dstOffset == ZoneOffset.of(explicitDstOffset).getTotalSeconds() * 1_000 :
1307+
dstOffset != zoneOffset;
12961308
if (formatData.locale == null || formatData.isZoneStringsSet) {
1297-
int zoneIndex =
1298-
formatData.getZoneIndex(calendar.getTimeZone().getID());
1309+
int zoneIndex = formatData.getZoneIndex(tzid);
12991310
if (zoneIndex == -1) {
1300-
value = calendar.get(Calendar.ZONE_OFFSET) +
1301-
calendar.get(Calendar.DST_OFFSET);
1302-
buffer.append(ZoneInfoFile.toCustomID(value));
1311+
buffer.append(ZoneInfoFile.toCustomID(dstOffset));
13031312
} else {
1304-
int index = (calendar.get(Calendar.DST_OFFSET) == 0) ? 1: 3;
1313+
int index = daylight ? 3 : 1;
13051314
if (count < 4) {
13061315
// Use the short name
13071316
index++;
@@ -1310,8 +1319,6 @@ private void subFormat(int patternCharIndex, int count,
13101319
buffer.append(zoneStrings[zoneIndex][index]);
13111320
}
13121321
} else {
1313-
TimeZone tz = calendar.getTimeZone();
1314-
boolean daylight = (calendar.get(Calendar.DST_OFFSET) != 0);
13151322
int tzstyle = (count < 4 ? TimeZone.SHORT : TimeZone.LONG);
13161323
buffer.append(tz.getDisplayName(daylight, tzstyle, formatData.locale));
13171324
}

src/java.base/share/classes/java/time/format/DateTimeFormatterBuilder.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2012, 2025, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2012, 2026, Oracle and/or its affiliates. All rights reserved.
33
* Copyright (c) 2025, Alibaba Group Holding Limited. All Rights Reserved.
44
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
55
*
@@ -4513,7 +4513,11 @@ public boolean format(DateTimePrintContext context, StringBuilder buf, boolean o
45134513
TemporalAccessor dt = context.getTemporal();
45144514
int type = GENERIC;
45154515
if (!isGeneric) {
4516-
if (dt.isSupported(ChronoField.INSTANT_SECONDS)) {
4516+
// Check if an explicit metazone DST offset exists
4517+
String dstOffset = TimeZoneNameUtility.explicitDstOffset(zname);
4518+
if (dt.isSupported(OFFSET_SECONDS) && dstOffset != null) {
4519+
type = ZoneOffset.from(dt).equals(ZoneOffset.of(dstOffset)) ? DST : STD;
4520+
} else if (dt.isSupported(ChronoField.INSTANT_SECONDS)) {
45174521
type = zone.getRules().isDaylightSavings(Instant.from(dt)) ? DST : STD;
45184522
} else if (dt.isSupported(ChronoField.EPOCH_DAY) &&
45194523
dt.isSupported(ChronoField.NANO_OF_DAY)) {

src/java.base/share/classes/sun/util/locale/provider/LocaleResources.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ public class LocaleResources {
105105
// TimeZoneNamesBundle exemplar city prefix
106106
private static final String TZNB_EXCITY_PREFIX = "timezone.excity.";
107107

108+
// TimeZoneNamesBundle explicit metazone dst offset prefix
109+
private static final String TZNB_METAZONE_DSTOFFSET_PREFIX = "metazone.dstoffset.";
110+
108111
// null singleton cache value
109112
private static final Object NULLOBJECT = new Object();
110113

@@ -321,7 +324,8 @@ public Object getTimeZoneNames(String key) {
321324

322325
if (Objects.isNull(data) || Objects.isNull(val = data.get())) {
323326
TimeZoneNamesBundle tznb = localeData.getTimeZoneNames(locale);
324-
if (key.startsWith(TZNB_EXCITY_PREFIX)) {
327+
if (key.startsWith(TZNB_EXCITY_PREFIX) ||
328+
key.startsWith(TZNB_METAZONE_DSTOFFSET_PREFIX)) {
325329
if (tznb.containsKey(key)) {
326330
val = tznb.getString(key);
327331
assert val instanceof String;
@@ -378,7 +382,8 @@ String[][] getZoneStrings() {
378382
Set<String[]> value = new LinkedHashSet<>();
379383
Set<String> tzIds = new HashSet<>(Arrays.asList(TimeZone.getAvailableIDs()));
380384
for (String key : keyset) {
381-
if (!key.startsWith(TZNB_EXCITY_PREFIX)) {
385+
if (!key.startsWith(TZNB_EXCITY_PREFIX) &&
386+
!key.startsWith(TZNB_METAZONE_DSTOFFSET_PREFIX)) {
382387
value.add(rb.getStringArray(key));
383388
tzIds.remove(key);
384389
}

src/java.base/share/classes/sun/util/locale/provider/TimeZoneNameUtility.java

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
import java.util.spi.TimeZoneNameProvider;
3838
import sun.util.calendar.ZoneInfo;
3939
import sun.util.cldr.CLDRLocaleProviderAdapter;
40-
import static sun.util.locale.provider.LocaleProviderAdapter.Type;
40+
import static sun.util.locale.provider.LocaleProviderAdapter.Type.CLDR;
4141

4242
/**
4343
* Utility class that deals with the localized time zone names
@@ -169,10 +169,22 @@ public static Optional<String> convertLDMLShortID(String shortID) {
169169
* Returns the canonical ID for the given ID
170170
*/
171171
public static Optional<String> canonicalTZID(String id) {
172-
return ((CLDRLocaleProviderAdapter)LocaleProviderAdapter.forType(Type.CLDR))
172+
return ((CLDRLocaleProviderAdapter)LocaleProviderAdapter.forType(CLDR))
173173
.canonicalTZID(id);
174174
}
175175

176+
/**
177+
* {@return the explicit metazone DST offset for the specified time zone ID, if exists}
178+
* @param tzid the time zone ID
179+
*/
180+
public static String explicitDstOffset(String tzid) {
181+
return (String) (LocaleProviderAdapter.forType(CLDR) instanceof CLDRLocaleProviderAdapter ca ?
182+
ca.getLocaleResources(Locale.ROOT)
183+
.getTimeZoneNames("metazone.dstoffset." +
184+
ca.canonicalTZID(tzid).orElse(tzid)) :
185+
null);
186+
}
187+
176188
private static String[] retrieveDisplayNamesImpl(String id, Locale locale) {
177189
LocaleServiceProviderPool pool =
178190
LocaleServiceProviderPool.getPool(TimeZoneNameProvider.class);

src/java.base/share/classes/sun/util/resources/TimeZoneNamesBundle.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2005, 2022, Oracle and/or its affiliates. All rights reserved.
2+
* Copyright (c) 2005, 2026, Oracle and/or its affiliates. All rights reserved.
33
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
44
*
55
* This code is free software; you can redistribute it and/or modify it
@@ -43,8 +43,6 @@
4343
import java.util.Map;
4444
import java.util.LinkedHashMap;
4545
import java.util.LinkedHashSet;
46-
import java.util.MissingResourceException;
47-
import java.util.Objects;
4846
import java.util.Set;
4947

5048
/**

test/jdk/sun/util/resources/cldr/TimeZoneNamesTest.java

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,23 +24,29 @@
2424
/*
2525
* @test
2626
* @bug 8181157 8202537 8234347 8236548 8261279 8322647 8174269 8346948
27-
* 8354548
27+
* 8354548 8381379
2828
* @modules jdk.localedata
2929
* @summary Checks CLDR time zone names are generated correctly at
3030
* either build or runtime
3131
* @run junit TimeZoneNamesTest
3232
*/
3333

3434
import java.text.DateFormatSymbols;
35+
import java.text.SimpleDateFormat;
3536
import java.time.ZoneId;
37+
import java.time.ZonedDateTime;
38+
import java.time.format.DateTimeFormatter;
3639
import java.time.format.TextStyle;
3740
import java.util.Arrays;
41+
import java.util.Date;
3842
import java.util.Locale;
3943
import java.util.Objects;
4044
import java.util.TimeZone;
45+
import java.util.stream.Stream;
4146

4247
import org.junit.jupiter.api.Test;
4348
import org.junit.jupiter.params.ParameterizedTest;
49+
import org.junit.jupiter.params.provider.Arguments;
4450
import org.junit.jupiter.params.provider.MethodSource;
4551
import static org.junit.jupiter.api.Assertions.assertEquals;
4652
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -217,6 +223,18 @@ private static Object[][] sampleTZs() {
217223
};
218224
}
219225

226+
private static Stream<Arguments> explicitDstOffsets() {
227+
return Stream.of(
228+
Arguments.of(ZonedDateTime.of(2026, 4, 5, 0, 0, 0, 0, ZoneId.of("Europe/Dublin")), "Irish Standard Time"),
229+
Arguments.of(ZonedDateTime.of(2026, 12, 5, 0, 0, 0, 0, ZoneId.of("Europe/Dublin")), "Greenwich Mean Time"),
230+
Arguments.of(ZonedDateTime.of(2026, 4, 5, 0, 0, 0, 0, ZoneId.of("Eire")), "Irish Standard Time"),
231+
Arguments.of(ZonedDateTime.of(2026, 12, 5, 0, 0, 0, 0, ZoneId.of("Eire")), "Greenwich Mean Time"),
232+
Arguments.of(ZonedDateTime.of(2026, 4, 5, 0, 0, 0, 0, ZoneId.of("America/Vancouver")), "Pacific Daylight Time"),
233+
// This needs to change once TZDB adopts -7 offset year round, and CLDR uses explicit dst offset
234+
// namely, "Pacific Standard Time" -> "Pacific Daylight Time"
235+
Arguments.of(ZonedDateTime.of(2026, 12, 5, 0, 0, 0, 0, ZoneId.of("America/Vancouver")), "Pacific Standard Time")
236+
);
237+
}
220238

221239
@ParameterizedTest
222240
@MethodSource("sampleTZs")
@@ -248,4 +266,19 @@ public void test_getZoneStrings() {
248266
.anyMatch(name -> Objects.isNull(name) || name.isEmpty()),
249267
"getZoneStrings() returned array containing non-empty string element(s)");
250268
}
269+
270+
// Explicit metazone dst offset test. As of CLDR v48, only Europe/Dublin utilizes
271+
// this attribute, but will be used for America/Vancouver once CLDR adopts the
272+
// explicit offset for that zone, which warrants the test data modification.
273+
@ParameterizedTest
274+
@MethodSource("explicitDstOffsets")
275+
public void test_ExplicitMetazoneOffsets(ZonedDateTime zdt, String expected) {
276+
// java.time
277+
assertEquals(expected, DateTimeFormatter.ofPattern("zzzz").format(zdt));
278+
279+
// java.text/util
280+
var sdf = new SimpleDateFormat("zzzz");
281+
sdf.setTimeZone(TimeZone.getTimeZone(zdt.getZone()));
282+
assertEquals(expected, sdf.format(Date.from(zdt.toInstant())));
283+
}
251284
}

0 commit comments

Comments
 (0)