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.
*
* 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.time.ZoneId;
import java.time.ZoneOffset;
import jdk.internal.util.StaticProperty;
import sun.security.action.GetPropertyAction;
@ -74,6 +75,7 @@ import sun.util.locale.provider.TimeZoneNameUtility;
*
* <blockquote><pre>
* <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> <i>Minutes</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>Minutes:</i>
* <i>Digit</i> <i>Digit</i>
* <i>Seconds:</i>
* <i>Digit</i> <i>Digit</i>
* <i>Digit:</i> one of
* {@code 0 1 2 3 4 5 6 7 8 9}
* </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
* hours and ten minutes ahead of GMT, respectively.
* <p>
@ -102,17 +106,20 @@ import sun.util.locale.provider.TimeZoneNameUtility;
* zone ID is normalized in the following syntax:
* <blockquote><pre>
* <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
* {@code + -}
* <i>TwoDigitHours:</i>
* <i>Digit</i> <i>Digit</i>
* <i>Minutes:</i>
* <i>Digit</i> <i>Digit</i>
* <i>ColonSeconds:</i>
* {@code :} <i>Digit</i> <i>Digit</i>
* <i>Digit:</i> one of
* {@code 0 1 2 3 4 5 6 7 8 9}
* </pre></blockquote>
* 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>
*
@ -529,11 +536,11 @@ public abstract class TimeZone implements Serializable, Cloneable {
*/
public static TimeZone getTimeZone(ZoneId zoneId) {
String tzid = zoneId.getId(); // throws an NPE if null
char c = tzid.charAt(0);
if (c == '+' || c == '-') {
tzid = "GMT" + tzid;
} else if (c == 'Z' && tzid.length() == 1) {
tzid = "UTC";
if (zoneId instanceof ZoneOffset zo) {
var totalMillis = zo.getTotalSeconds() * 1_000;
return new ZoneInfo(totalMillis == 0 ? "UTC" : GMT_ID + tzid, totalMillis);
} else if (tzid.startsWith("UT") && !tzid.equals("UTC")) {
tzid = tzid.replaceFirst("(UTC|UT)(.*)", "GMT$2");
}
return getTimeZone(tzid, true);
}
@ -823,19 +830,24 @@ public abstract class TimeZone implements Serializable, Cloneable {
}
int hours = 0;
int minutes = 0;
int num = 0;
int countDelim = 0;
int len = 0;
while (index < length) {
c = id.charAt(index++);
if (c == ':') {
if (countDelim > 0) {
if (countDelim > 1) {
return null;
}
if (len > 2) {
if (len == 0 || len > 2) {
return null;
}
hours = num;
if (countDelim == 0) {
hours = num;
} else if (countDelim == 1){
minutes = num;
}
countDelim++;
num = 0;
len = 0;
@ -853,20 +865,31 @@ public abstract class TimeZone implements Serializable, Cloneable {
if (countDelim == 0) {
if (len <= 2) {
hours = num;
minutes = 0;
num = 0;
} else if (len <= 4) {
hours = num / 100;
minutes = num % 100;
num = 0;
} else {
hours = num / 100;
num %= 100;
return null;
}
} else if (countDelim == 1){
if (len == 2) {
minutes = num;
num = 0;
} else {
return null;
}
} else {
if (len != 2) {
return null;
}
}
if (hours > 23 || num > 59) {
if (hours > 23 || minutes > 59 || num > 59) {
return null;
}
int gmtOffset = (hours * 60 + num) * 60 * 1000;
int gmtOffset = (hours * 3_600 + minutes * 60 + num) * 1_000;
if (gmtOffset == 0) {
zi = ZoneInfoFile.getZoneInfo(GMT_ID);

View File

@ -178,15 +178,16 @@ public final class ZoneInfoFile {
public static String toCustomID(int gmtOffset) {
char sign;
int offset = gmtOffset / 60000;
int offset = gmtOffset / 1_000;
if (offset >= 0) {
sign = '+';
} else {
sign = '-';
offset = -offset;
}
int hh = offset / 60;
int mm = offset % 60;
int hh = offset / 3_600;
int mm = (offset % 3_600) / 60;
int ss = offset % 60;
char[] buf = new char[] { 'G', 'M', 'T', sign, '0', '0', ':', '0', '0' };
if (hh >= 10) {
@ -197,7 +198,13 @@ public final class ZoneInfoFile {
buf[7] += (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.
*
* This code is free software; you can redistribute it and/or modify it
@ -25,10 +25,11 @@
* @test
* @bug 4028006 4044013 4096694 4107276 4107570 4112869 4130885 7039469 7126465 7158483
* 8008577 8077685 8098547 8133321 8138716 8148446 8151876 8159684 8166875 8181157
* 8228469 8274407
* 8228469 8274407 8285844
* @modules java.base/sun.util.resources
* @library /java/text/testlib
* @summary test TimeZone
* @run main TimeZoneTest -verbose
*/
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 = '+';
if (min < 0) { sign = '-'; min = -min; }
int h = min/60;
min = min%60;
return "" + sign + h + ":" + ((min<10) ? "0" : "") + min;
if (sec < 0) { sign = '-'; sec = -sec; }
int h = sec / 3_600;
int m = sec % 3_600 / 60;
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
@ -240,21 +243,28 @@ public class TimeZoneTest extends IntlTest
*/
public void TestCustomParse() throws Exception {
Object[] DATA = {
// ID Expected offset in minutes
"GMT", null,
"GMT+0", new Integer(0),
"GMT+1", new Integer(60),
"GMT-0030", new Integer(-30),
"GMT+15:99", null,
"GMT+", null,
"GMT-", null,
"GMT+0:", null,
"GMT-:", null,
"GMT+0010", new Integer(10), // Interpret this as 00:10
"GMT-10", new Integer(-10*60),
"GMT+30", null,
"GMT-3:30", new Integer(-(3*60+30)),
"GMT-230", new Integer(-(2*60+30)),
// ID Expected offset in seconds
"GMT", null,
"GMT+0", 0,
"GMT+1", 60 * 60,
"GMT-0030", -30 * 60,
"GMT+15:99", null,
"GMT+", null,
"GMT-", null,
"GMT+0:", null,
"GMT-:", null,
"GMT+0010", 10 * 60, // Interpret this as 00:10
"GMT-10", -10 * 60 * 60,
"GMT+30", null,
"GMT-3:30", -(3 * 60 + 30) * 60,
"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) {
String id = (String)DATA[i];
@ -266,13 +276,13 @@ public class TimeZoneTest extends IntlTest
// returns GMT -- a dubious practice, but required for
// backward compatibility.
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");
}
}
else {
int ioffset = zone.getRawOffset()/60000;
String offset = formatMinutes(ioffset);
int ioffset = zone.getRawOffset() / 1_000;
String offset = formatSeconds(ioffset);
logln(id + " -> " + zone.getID() + " GMT" + offset);
if (exp == null) {
throw new Exception("Expected parse failure for " + id +
@ -280,7 +290,7 @@ public class TimeZoneTest extends IntlTest
", id " + zone.getID());
}
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 +
", got offset of " + offset +
", 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());
}
}