8236903: ZoneRules#getOffset throws DateTimeException for rules with last rules

Reviewed-by: scolebourne, rriggs
This commit is contained in:
Naoto Sato 2020-01-22 12:35:24 -08:00
parent b7cdc097e3
commit d517220df6
2 changed files with 94 additions and 12 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2012, 2018, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2012, 2020, 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
@ -69,7 +69,6 @@ import java.io.ObjectInputStream;
import java.io.Serializable;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
@ -162,6 +161,16 @@ public final class ZoneRules implements Serializable {
* The zero-length ldt array.
*/
private static final LocalDateTime[] EMPTY_LDT_ARRAY = new LocalDateTime[0];
/**
* The number of days in a 400 year cycle.
*/
private static final int DAYS_PER_CYCLE = 146097;
/**
* The number of days from year zero to year 1970.
* There are five 400 year cycles from year zero to 2000.
* There are 7 leap years from 1970 to 2000.
*/
private static final long DAYS_0000_TO_1970 = (DAYS_PER_CYCLE * 5L) - (30L * 365L + 7L);
/**
* Obtains an instance of a ZoneRules.
@ -921,10 +930,34 @@ public final class ZoneRules implements Serializable {
}
private int findYear(long epochSecond, ZoneOffset offset) {
// inline for performance
long localSecond = epochSecond + offset.getTotalSeconds();
long localEpochDay = Math.floorDiv(localSecond, 86400);
return LocalDate.ofEpochDay(localEpochDay).getYear();
long zeroDay = Math.floorDiv(localSecond, 86400) + DAYS_0000_TO_1970;
// find the march-based year
zeroDay -= 60; // adjust to 0000-03-01 so leap day is at end of four year cycle
long adjust = 0;
if (zeroDay < 0) {
// adjust negative years to positive for calculation
long adjustCycles = (zeroDay + 1) / DAYS_PER_CYCLE - 1;
adjust = adjustCycles * 400;
zeroDay += -adjustCycles * DAYS_PER_CYCLE;
}
long yearEst = (400 * zeroDay + 591) / DAYS_PER_CYCLE;
long doyEst = zeroDay - (365 * yearEst + yearEst / 4 - yearEst / 100 + yearEst / 400);
if (doyEst < 0) {
// fix estimate
yearEst--;
doyEst = zeroDay - (365 * yearEst + yearEst / 4 - yearEst / 100 + yearEst / 400);
}
yearEst += adjust; // reset any negative year
int marchDoy0 = (int) doyEst;
// convert march-based values back to january-based
int marchMonth0 = (marchDoy0 * 5 + 2) / 153;
yearEst += marchMonth0 / 10;
// Cap to the max value
return (int)Math.min(yearEst, Year.MAX_VALUE);
}
/**

View File

@ -1,5 +1,5 @@
/*
* Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
* Copyright (c) 2019, 2020, 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
@ -23,17 +23,20 @@
package test.java.time.zone;
import static org.testng.Assert.assertEquals;
import java.time.DayOfWeek;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.Month;
import java.time.Year;
import java.time.ZonedDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.zone.ZoneOffsetTransition;
import java.time.zone.ZoneOffsetTransitionRule;
import java.time.zone.ZoneRules;
import java.util.Collections;
import org.testng.annotations.Test;
import org.testng.annotations.DataProvider;
@ -41,11 +44,9 @@ import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertTrue;
/**
* @summary Test ZoneRules whether the savings are positive in time zones that have
* negative savings in the source TZ files. Also, check the transition cutover
* time beyond 24:00, which should translate into the next day.
* @summary Tests for ZoneRules class.
*
* @bug 8212970
* @bug 8212970 8236903
*/
@Test
public class TestZoneRules {
@ -107,6 +108,11 @@ public class TestZoneRules {
};
}
/**
* Test ZoneRules whether the savings are positive in time zones that have
* negative savings in the source TZ files.
* @bug 8212970
*/
@Test(dataProvider="negativeDST")
public void test_NegativeDST(ZoneId zid, LocalDate ld, ZoneOffset offset, ZoneOffset stdOffset, boolean isDST) {
Instant i = Instant.from(ZonedDateTime.of(ld, LocalTime.MIN, zid));
@ -116,10 +122,53 @@ public class TestZoneRules {
assertEquals(zr.isDaylightSavings(i), isDST);
}
/**
* Check the transition cutover time beyond 24:00, which should translate into the next day.
* @bug 8212970
*/
@Test(dataProvider="transitionBeyondDay")
public void test_TransitionBeyondDay(ZoneId zid, LocalDateTime ldt, ZoneOffset before, ZoneOffset after) {
ZoneOffsetTransition zot = ZoneOffsetTransition.of(ldt, before, after);
ZoneRules zr = zid.getRules();
assertTrue(zr.getTransitions().contains(zot));
}
/**
* Make sure ZoneRules.findYear() won't throw out-of-range DateTimeException for
* year calculation.
* @bug 8236903
*/
@Test
public void test_TransitionLastRuleYear() {
Instant maxLocalDateTime = LocalDateTime.of(Year.MAX_VALUE,
12,
31,
23,
59,
59,
999999999).toInstant(ZoneOffset.UTC);
ZoneOffset offsetZero = ZoneOffset.ofHours(0);
ZoneOffset offsetPlusOneHour = ZoneOffset.ofHours(1);
ZoneRules zoneRulesA = ZoneRules.of(offsetPlusOneHour);
ZoneOffsetTransition transition = ZoneOffsetTransition.of(LocalDateTime.ofEpochSecond(0, 0, offsetZero),
offsetZero,
offsetPlusOneHour);
ZoneOffsetTransitionRule transitionRule = ZoneOffsetTransitionRule.of(Month.JANUARY,
1,
DayOfWeek.SUNDAY,
LocalTime.MIDNIGHT,
true,
ZoneOffsetTransitionRule.TimeDefinition.STANDARD,
offsetZero,
offsetZero,
offsetPlusOneHour);
ZoneRules zoneRulesB = ZoneRules.of(offsetZero,
offsetZero,
Collections.singletonList(transition),
Collections.singletonList(transition),
Collections.singletonList(transitionRule));
ZoneOffset offsetA = zoneRulesA.getOffset(maxLocalDateTime);
ZoneOffset offsetB = zoneRulesB.getOffset(maxLocalDateTime);
assertEquals(offsetA, offsetB);
}
}