8285844: TimeZone.getTimeZone(ZoneOffset) does not work for all ZoneOffsets and returns GMT unexpected

Reviewed-by: uschindler, scolebourne, joehw
This commit is contained in:
Naoto Sato 2022-05-16 15:45:01 +00:00
parent dbd3737085
commit b884db8f7c
4 changed files with 155 additions and 45 deletions

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 1996, 2021, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 1996, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* *
* This code is free software; you can redistribute it and/or modify it * This code is free software; you can redistribute it and/or modify it
@ -40,6 +40,7 @@ package java.util;
import java.io.Serializable; import java.io.Serializable;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.ZoneOffset;
import jdk.internal.util.StaticProperty; import jdk.internal.util.StaticProperty;
import sun.security.action.GetPropertyAction; import sun.security.action.GetPropertyAction;
@ -74,6 +75,7 @@ import sun.util.locale.provider.TimeZoneNameUtility;
* *
* <blockquote><pre> * <blockquote><pre>
* <a id="CustomID"><i>CustomID:</i></a> * <a id="CustomID"><i>CustomID:</i></a>
* {@code GMT} <i>Sign</i> <i>Hours</i> {@code :} <i>Minutes</i> {@code :} <i>Seconds</i>
* {@code GMT} <i>Sign</i> <i>Hours</i> {@code :} <i>Minutes</i> * {@code GMT} <i>Sign</i> <i>Hours</i> {@code :} <i>Minutes</i>
* {@code GMT} <i>Sign</i> <i>Hours</i> <i>Minutes</i> * {@code GMT} <i>Sign</i> <i>Hours</i> <i>Minutes</i>
* {@code GMT} <i>Sign</i> <i>Hours</i> * {@code GMT} <i>Sign</i> <i>Hours</i>
@ -84,11 +86,13 @@ import sun.util.locale.provider.TimeZoneNameUtility;
* <i>Digit</i> <i>Digit</i> * <i>Digit</i> <i>Digit</i>
* <i>Minutes:</i> * <i>Minutes:</i>
* <i>Digit</i> <i>Digit</i> * <i>Digit</i> <i>Digit</i>
* <i>Seconds:</i>
* <i>Digit</i> <i>Digit</i>
* <i>Digit:</i> one of * <i>Digit:</i> one of
* {@code 0 1 2 3 4 5 6 7 8 9} * {@code 0 1 2 3 4 5 6 7 8 9}
* </pre></blockquote> * </pre></blockquote>
* *
* <i>Hours</i> must be between 0 to 23 and <i>Minutes</i> must be * <i>Hours</i> must be between 0 to 23 and <i>Minutes</i>/<i>Seconds</i> must be
* between 00 to 59. For example, "GMT+10" and "GMT+0010" mean ten * between 00 to 59. For example, "GMT+10" and "GMT+0010" mean ten
* hours and ten minutes ahead of GMT, respectively. * hours and ten minutes ahead of GMT, respectively.
* <p> * <p>
@ -102,17 +106,20 @@ import sun.util.locale.provider.TimeZoneNameUtility;
* zone ID is normalized in the following syntax: * zone ID is normalized in the following syntax:
* <blockquote><pre> * <blockquote><pre>
* <a id="NormalizedCustomID"><i>NormalizedCustomID:</i></a> * <a id="NormalizedCustomID"><i>NormalizedCustomID:</i></a>
* {@code GMT} <i>Sign</i> <i>TwoDigitHours</i> {@code :} <i>Minutes</i> * {@code GMT} <i>Sign</i> <i>TwoDigitHours</i> {@code :} <i>Minutes</i> [<i>ColonSeconds</i>]
* <i>Sign:</i> one of * <i>Sign:</i> one of
* {@code + -} * {@code + -}
* <i>TwoDigitHours:</i> * <i>TwoDigitHours:</i>
* <i>Digit</i> <i>Digit</i> * <i>Digit</i> <i>Digit</i>
* <i>Minutes:</i> * <i>Minutes:</i>
* <i>Digit</i> <i>Digit</i> * <i>Digit</i> <i>Digit</i>
* <i>ColonSeconds:</i>
* {@code :} <i>Digit</i> <i>Digit</i>
* <i>Digit:</i> one of * <i>Digit:</i> one of
* {@code 0 1 2 3 4 5 6 7 8 9} * {@code 0 1 2 3 4 5 6 7 8 9}
* </pre></blockquote> * </pre></blockquote>
* For example, TimeZone.getTimeZone("GMT-8").getID() returns "GMT-08:00". * For example, TimeZone.getTimeZone("GMT-8").getID() returns "GMT-08:00".
* <i>ColonSeconds</i> part only appears if the seconds value is non-zero.
* *
* <h2>Three-letter time zone IDs</h2> * <h2>Three-letter time zone IDs</h2>
* *
@ -529,11 +536,11 @@ public abstract class TimeZone implements Serializable, Cloneable {
*/ */
public static TimeZone getTimeZone(ZoneId zoneId) { public static TimeZone getTimeZone(ZoneId zoneId) {
String tzid = zoneId.getId(); // throws an NPE if null String tzid = zoneId.getId(); // throws an NPE if null
char c = tzid.charAt(0); if (zoneId instanceof ZoneOffset zo) {
if (c == '+' || c == '-') { var totalMillis = zo.getTotalSeconds() * 1_000;
tzid = "GMT" + tzid; return new ZoneInfo(totalMillis == 0 ? "UTC" : GMT_ID + tzid, totalMillis);
} else if (c == 'Z' && tzid.length() == 1) { } else if (tzid.startsWith("UT") && !tzid.equals("UTC")) {
tzid = "UTC"; tzid = tzid.replaceFirst("(UTC|UT)(.*)", "GMT$2");
} }
return getTimeZone(tzid, true); return getTimeZone(tzid, true);
} }
@ -823,19 +830,24 @@ public abstract class TimeZone implements Serializable, Cloneable {
} }
int hours = 0; int hours = 0;
int minutes = 0;
int num = 0; int num = 0;
int countDelim = 0; int countDelim = 0;
int len = 0; int len = 0;
while (index < length) { while (index < length) {
c = id.charAt(index++); c = id.charAt(index++);
if (c == ':') { if (c == ':') {
if (countDelim > 0) { if (countDelim > 1) {
return null; return null;
} }
if (len > 2) { if (len == 0 || len > 2) {
return null; return null;
} }
hours = num; if (countDelim == 0) {
hours = num;
} else if (countDelim == 1){
minutes = num;
}
countDelim++; countDelim++;
num = 0; num = 0;
len = 0; len = 0;
@ -853,20 +865,31 @@ public abstract class TimeZone implements Serializable, Cloneable {
if (countDelim == 0) { if (countDelim == 0) {
if (len <= 2) { if (len <= 2) {
hours = num; hours = num;
minutes = 0;
num = 0;
} else if (len <= 4) {
hours = num / 100;
minutes = num % 100;
num = 0; num = 0;
} else { } else {
hours = num / 100; return null;
num %= 100; }
} else if (countDelim == 1){
if (len == 2) {
minutes = num;
num = 0;
} else {
return null;
} }
} else { } else {
if (len != 2) { if (len != 2) {
return null; return null;
} }
} }
if (hours > 23 || num > 59) { if (hours > 23 || minutes > 59 || num > 59) {
return null; return null;
} }
int gmtOffset = (hours * 60 + num) * 60 * 1000; int gmtOffset = (hours * 3_600 + minutes * 60 + num) * 1_000;
if (gmtOffset == 0) { if (gmtOffset == 0) {
zi = ZoneInfoFile.getZoneInfo(GMT_ID); zi = ZoneInfoFile.getZoneInfo(GMT_ID);

View File

@ -178,15 +178,16 @@ public final class ZoneInfoFile {
public static String toCustomID(int gmtOffset) { public static String toCustomID(int gmtOffset) {
char sign; char sign;
int offset = gmtOffset / 60000; int offset = gmtOffset / 1_000;
if (offset >= 0) { if (offset >= 0) {
sign = '+'; sign = '+';
} else { } else {
sign = '-'; sign = '-';
offset = -offset; offset = -offset;
} }
int hh = offset / 60; int hh = offset / 3_600;
int mm = offset % 60; int mm = (offset % 3_600) / 60;
int ss = offset % 60;
char[] buf = new char[] { 'G', 'M', 'T', sign, '0', '0', ':', '0', '0' }; char[] buf = new char[] { 'G', 'M', 'T', sign, '0', '0', ':', '0', '0' };
if (hh >= 10) { if (hh >= 10) {
@ -197,7 +198,13 @@ public final class ZoneInfoFile {
buf[7] += (char)(mm / 10); buf[7] += (char)(mm / 10);
buf[8] += (char)(mm % 10); buf[8] += (char)(mm % 10);
} }
return new String(buf); var id = new String(buf);
if (ss != 0) {
buf[7] = (char)('0' + ss / 10);
buf[8] = (char)('0' + ss % 10);
id += new String(buf, 6, 3);
}
return id;
} }
/////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////

View File

@ -1,5 +1,5 @@
/* /*
* Copyright (c) 1997, 2021, Oracle and/or its affiliates. All rights reserved. * Copyright (c) 1997, 2022, Oracle and/or its affiliates. All rights reserved.
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
* *
* This code is free software; you can redistribute it and/or modify it * This code is free software; you can redistribute it and/or modify it
@ -25,10 +25,11 @@
* @test * @test
* @bug 4028006 4044013 4096694 4107276 4107570 4112869 4130885 7039469 7126465 7158483 * @bug 4028006 4044013 4096694 4107276 4107570 4112869 4130885 7039469 7126465 7158483
* 8008577 8077685 8098547 8133321 8138716 8148446 8151876 8159684 8166875 8181157 * 8008577 8077685 8098547 8133321 8138716 8148446 8151876 8159684 8166875 8181157
* 8228469 8274407 * 8228469 8274407 8285844
* @modules java.base/sun.util.resources * @modules java.base/sun.util.resources
* @library /java/text/testlib * @library /java/text/testlib
* @summary test TimeZone * @summary test TimeZone
* @run main TimeZoneTest -verbose
*/ */
import java.io.*; import java.io.*;
@ -220,12 +221,14 @@ public class TimeZoneTest extends IntlTest
} }
} }
static final String formatMinutes(int min) { static final String formatSeconds(int sec) {
char sign = '+'; char sign = '+';
if (min < 0) { sign = '-'; min = -min; } if (sec < 0) { sign = '-'; sec = -sec; }
int h = min/60; int h = sec / 3_600;
min = min%60; int m = sec % 3_600 / 60;
return "" + sign + h + ":" + ((min<10) ? "0" : "") + min; sec = sec % 60;
return "" + sign + h + ":" + ((m<10) ? "0" : "") + m +
(sec > 0 ? ":" + ((sec < 10) ? "0" : "") + sec : "");
} }
/** /**
* As part of the VM fix (see CCC approved RFE 4028006, bug * As part of the VM fix (see CCC approved RFE 4028006, bug
@ -240,21 +243,28 @@ public class TimeZoneTest extends IntlTest
*/ */
public void TestCustomParse() throws Exception { public void TestCustomParse() throws Exception {
Object[] DATA = { Object[] DATA = {
// ID Expected offset in minutes // ID Expected offset in seconds
"GMT", null, "GMT", null,
"GMT+0", new Integer(0), "GMT+0", 0,
"GMT+1", new Integer(60), "GMT+1", 60 * 60,
"GMT-0030", new Integer(-30), "GMT-0030", -30 * 60,
"GMT+15:99", null, "GMT+15:99", null,
"GMT+", null, "GMT+", null,
"GMT-", null, "GMT-", null,
"GMT+0:", null, "GMT+0:", null,
"GMT-:", null, "GMT-:", null,
"GMT+0010", new Integer(10), // Interpret this as 00:10 "GMT+0010", 10 * 60, // Interpret this as 00:10
"GMT-10", new Integer(-10*60), "GMT-10", -10 * 60 * 60,
"GMT+30", null, "GMT+30", null,
"GMT-3:30", new Integer(-(3*60+30)), "GMT-3:30", -(3 * 60 + 30) * 60,
"GMT-230", new Integer(-(2*60+30)), "GMT-230", -(2 * 60 + 30) * 60,
"GMT+00:00:01", 1,
"GMT-00:00:01", -1,
"GMT+00000", null,
"GMT+00:00:01:", null,
"GMT+00:00:012", null,
"GMT+00:00:0", null,
"GMT+00:00:", null,
}; };
for (int i=0; i<DATA.length; i+=2) { for (int i=0; i<DATA.length; i+=2) {
String id = (String)DATA[i]; String id = (String)DATA[i];
@ -266,13 +276,13 @@ public class TimeZoneTest extends IntlTest
// returns GMT -- a dubious practice, but required for // returns GMT -- a dubious practice, but required for
// backward compatibility. // backward compatibility.
if (exp != null) { if (exp != null) {
throw new Exception("Expected offset of " + formatMinutes(exp.intValue()) + throw new Exception("Expected offset of " + formatSeconds(exp.intValue()) +
" for " + id + ", got parse failure"); " for " + id + ", got parse failure");
} }
} }
else { else {
int ioffset = zone.getRawOffset()/60000; int ioffset = zone.getRawOffset() / 1_000;
String offset = formatMinutes(ioffset); String offset = formatSeconds(ioffset);
logln(id + " -> " + zone.getID() + " GMT" + offset); logln(id + " -> " + zone.getID() + " GMT" + offset);
if (exp == null) { if (exp == null) {
throw new Exception("Expected parse failure for " + id + throw new Exception("Expected parse failure for " + id +
@ -280,7 +290,7 @@ public class TimeZoneTest extends IntlTest
", id " + zone.getID()); ", id " + zone.getID());
} }
else if (ioffset != exp.intValue()) { else if (ioffset != exp.intValue()) {
throw new Exception("Expected offset of " + formatMinutes(exp.intValue()) + throw new Exception("Expected offset of " + formatSeconds(exp.intValue()) +
", id Custom, for " + id + ", id Custom, for " + id +
", got offset of " + offset + ", got offset of " + offset +
", id " + zone.getID()); ", id " + zone.getID());

View File

@ -0,0 +1,70 @@
/*
* Copyright (c) 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
* 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.
*/
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.util.TimeZone;
import org.testng.annotations.Test;
import org.testng.annotations.DataProvider;
import static org.testng.Assert.assertEquals;
/**
* @test
* @bug 8285844
* @summary Checks round-trips between TimeZone and ZoneId are consistent
* @run testng ZoneIdRoundTripTest
*/
@Test
public class ZoneIdRoundTripTest {
@DataProvider
private Object[][] testZoneIds() {
return new Object[][] {
{ZoneId.of("Z"), 0},
{ZoneId.of("UT"), 0},
{ZoneId.of("UTC"), 0},
{ZoneId.of("GMT"), 0},
{ZoneId.of("+00:01"), 60_000},
{ZoneId.of("-00:01"), -60_000},
{ZoneId.of("+00:00:01"), 1_000},
{ZoneId.of("-00:00:01"), -1_000},
{ZoneId.of("UT+00:00:01"), 1_000},
{ZoneId.of("UT-00:00:01"), -1_000},
{ZoneId.of("UTC+00:00:01"), 1_000},
{ZoneId.of("UTC-00:00:01"), -1_000},
{ZoneId.of("GMT+00:00:01"), 1_000},
{ZoneId.of("GMT-00:00:01"), -1_000},
{ZoneOffset.of("+00:00:01"), 1_000},
{ZoneOffset.of("-00:00:01"), -1_000},
};
}
@Test(dataProvider="testZoneIds")
public void test_ZoneIdRoundTrip(ZoneId zid, int offset) {
var tz = TimeZone.getTimeZone(zid);
assertEquals(tz.getRawOffset(), offset);
assertEquals(tz.toZoneId().normalized(), zid.normalized());
}
}