Merge changes from topics "bug118835133_2", "bug118835133_1"
* changes:
Fix android.text.format.Time behavior
Add a test for behavior around Integer.MAX_VALUE
Change behavior for times < first transition
diff --git a/luni/src/main/java/libcore/util/ZoneInfo.java b/luni/src/main/java/libcore/util/ZoneInfo.java
index 25cd402..65df611 100644
--- a/luni/src/main/java/libcore/util/ZoneInfo.java
+++ b/luni/src/main/java/libcore/util/ZoneInfo.java
@@ -86,7 +86,16 @@
// Proclaim serialization compatibility with pre-OpenJDK AOSP
static final long serialVersionUID = -4598738130123921552L;
+ /**
+ * The (best guess) non-DST offset used "today". It is stored in milliseconds.
+ * See also {@link #mOffsets} which holds values relative to this value, albeit in seconds.
+ */
private int mRawOffset;
+
+ /**
+ * The earliest non-DST offset for the zone. It is stored in milliseconds and is absolute, i.e.
+ * it is not relative to mRawOffset.
+ */
private final int mEarliestRawOffset;
/**
@@ -283,15 +292,16 @@
setID(name);
// Find the latest daylight and standard offsets (if any).
- int lastStd = -1;
- int lastDst = -1;
- for (int i = mTransitions.length - 1; (lastStd == -1 || lastDst == -1) && i >= 0; --i) {
- int type = mTypes[i] & 0xff;
- if (lastStd == -1 && mIsDsts[type] == 0) {
- lastStd = i;
+ int lastStdTransitionIndex = -1;
+ int lastDstTransitionIndex = -1;
+ for (int i = mTransitions.length - 1;
+ (lastStdTransitionIndex == -1 || lastDstTransitionIndex == -1) && i >= 0; --i) {
+ int typeIndex = mTypes[i] & 0xff;
+ if (lastStdTransitionIndex == -1 && mIsDsts[typeIndex] == 0) {
+ lastStdTransitionIndex = i;
}
- if (lastDst == -1 && mIsDsts[type] != 0) {
- lastDst = i;
+ if (lastDstTransitionIndex == -1 && mIsDsts[typeIndex] != 0) {
+ lastDstTransitionIndex = i;
}
}
@@ -300,19 +310,19 @@
// If there are no transitions then use the first GMT offset.
mRawOffset = gmtOffsets[0];
} else {
- if (lastStd == -1) {
+ if (lastStdTransitionIndex == -1) {
throw new IllegalStateException( "ZoneInfo requires at least one non-DST "
+ "transition to be provided for each timezone that has at least one "
+ "transition but could not find one for '" + name + "'");
}
- mRawOffset = gmtOffsets[mTypes[lastStd] & 0xff];
+ mRawOffset = gmtOffsets[mTypes[lastStdTransitionIndex] & 0xff];
}
- if (lastDst != -1) {
+ if (lastDstTransitionIndex != -1) {
// Check to see if the last DST transition is in the future or the past. If it is in
// the past then we treat it as if it doesn't exist, at least for the purposes of
// setting mDstSavings and mUseDst.
- long lastDSTTransitionTime = mTransitions[lastDst];
+ long lastDSTTransitionTime = mTransitions[lastDstTransitionIndex];
// Convert the current time in millis into seconds. Unlike other places that convert
// time in milliseconds into seconds in order to compare with transition time this
@@ -328,11 +338,11 @@
// useDaylightTime at the start of 2009 but "false" at the end. This seems appropriate.
if (lastDSTTransitionTime < currentUnixTimeSeconds) {
// The last DST transition is before now so treat it as if it doesn't exist.
- lastDst = -1;
+ lastDstTransitionIndex = -1;
}
}
- if (lastDst == -1) {
+ if (lastDstTransitionIndex == -1) {
// There were no DST transitions or at least no future DST transitions so DST is not
// used.
mDstSavings = 0;
@@ -340,22 +350,31 @@
} else {
// Use the latest transition's pair of offsets to compute the DST savings.
// This isn't generally useful, but it's exposed by TimeZone.getDSTSavings.
- int lastGmtOffset = gmtOffsets[mTypes[lastStd] & 0xff];
- int lastDstOffset = gmtOffsets[mTypes[lastDst] & 0xff];
+ int lastGmtOffset = gmtOffsets[mTypes[lastStdTransitionIndex] & 0xff];
+ int lastDstOffset = gmtOffsets[mTypes[lastDstTransitionIndex] & 0xff];
mDstSavings = (lastDstOffset - lastGmtOffset) * 1000;
mUseDst = true;
}
- // Cache the oldest known raw offset, in case we're asked about times that predate our
- // transition data.
- int firstStd = -1;
- for (int i = 0; i < mTransitions.length; ++i) {
- if (mIsDsts[mTypes[i] & 0xff] == 0) {
- firstStd = i;
+ // From the tzfile docs (Jan 2019):
+ // The localtime(3) function uses the first standard-time ttinfo structure
+ // in the file (or simply the first ttinfo structure in the absence of a
+ // standard-time structure) if either tzh_timecnt is zero or the time
+ // argument is less than the first transition time recorded in the file.
+ //
+ // Cache the raw offset associated with the first nonDst type, in case we're asked about
+ // times that predate our transition data. Android falls back to mRawOffset if there are
+ // only DST ttinfo structures (assumed rare).
+ int firstStdTypeIndex = -1;
+ for (int i = 0; i < mIsDsts.length; ++i) {
+ if (mIsDsts[i] == 0) {
+ firstStdTypeIndex = i;
break;
}
}
- int earliestRawOffset = (firstStd != -1) ? gmtOffsets[mTypes[firstStd] & 0xff] : mRawOffset;
+
+ int earliestRawOffset = (firstStdTypeIndex != -1)
+ ? gmtOffsets[firstStdTypeIndex] : mRawOffset;
// Rather than keep offsets from UTC, we use offsets from local time, so the raw offset
// can be changed and automatically affect all the offsets.
@@ -753,8 +772,9 @@
int offsetIndex = zoneInfo.findOffsetIndexForTimeInSeconds(timeSeconds);
if (offsetIndex == -1) {
// -1 means timeSeconds is "before the first recorded transition". The first
- // recorded transition is treated as a transition from non-DST and the raw
- // offset.
+ // recorded transition is treated as a transition from non-DST and the
+ // earliest known raw offset.
+ offsetSeconds = zoneInfo.mEarliestRawOffset / 1000;
isDst = 0;
} else {
offsetSeconds += zoneInfo.mOffsets[offsetIndex];
@@ -763,7 +783,7 @@
}
// Perform arithmetic that might underflow before setting fields.
- int wallTimeSeconds = checkedAdd(timeSeconds, offsetSeconds);
+ int wallTimeSeconds = checked32BitAdd(timeSeconds, offsetSeconds);
// Set fields.
calendar.setTimeInMillis(wallTimeSeconds * 1000L);
@@ -816,7 +836,7 @@
try {
final int wallTimeSeconds = (int) longWallTimeSeconds;
final int rawOffsetSeconds = zoneInfo.mRawOffset / 1000;
- final int rawTimeSeconds = checkedSubtract(wallTimeSeconds, rawOffsetSeconds);
+ final int rawTimeSeconds = checked32BitSubtract(wallTimeSeconds, rawOffsetSeconds);
if (zoneInfo.mTransitions.length == 0) {
// There is no transition information. There is just a raw offset for all time.
@@ -901,10 +921,11 @@
int jOffsetSeconds = rawOffsetSeconds + offsetsToTry[j];
int targetIntervalOffsetSeconds = targetInterval.getTotalOffsetSeconds();
int adjustmentSeconds = targetIntervalOffsetSeconds - jOffsetSeconds;
- int adjustedWallTimeSeconds = checkedAdd(oldWallTimeSeconds, adjustmentSeconds);
+ int adjustedWallTimeSeconds =
+ checked32BitAdd(oldWallTimeSeconds, adjustmentSeconds);
if (targetInterval.containsWallTime(adjustedWallTimeSeconds)) {
// Perform any arithmetic that might overflow.
- int returnValue = checkedSubtract(adjustedWallTimeSeconds,
+ int returnValue = checked32BitSubtract(adjustedWallTimeSeconds,
targetIntervalOffsetSeconds);
// Modify field state and return the result.
@@ -1038,8 +1059,8 @@
// the result might be a DST or a non-DST answer for wall times that can
// exist in two OffsetIntervals.
int totalOffsetSeconds = offsetInterval.getTotalOffsetSeconds();
- int returnValue = checkedSubtract(wallTimeSeconds,
- totalOffsetSeconds);
+ int returnValue =
+ checked32BitSubtract(wallTimeSeconds, totalOffsetSeconds);
copyFieldsFromCalendar();
this.isDst = offsetInterval.getIsDst();
@@ -1226,16 +1247,19 @@
* Crucially this means that there was a "gap" after PST when PDT started, and an overlap when
* PDT ended and PST began.
*
- * <p>For convenience all wall-time values are represented as the number of seconds since the
- * beginning of the Unix epoch <em>in UTC</em>. To convert from a wall-time to the actual time
- * in the offset it is necessary to <em>subtract</em> the {@code totalOffsetSeconds}.
+ * <p>Although wall-time means "local time", for convenience all wall-time values are stored in
+ * the number of seconds since the beginning of the Unix epoch to get that time <em>in UTC</em>.
+ * To convert from a wall-time to the actual UTC time it is necessary to <em>subtract</em> the
+ * {@code totalOffsetSeconds}.
* For example: If the offset in PST is -07:00 hours, then:
* timeInPstSeconds = wallTimeUtcSeconds - offsetSeconds
* i.e. 13:00 UTC - (-07:00) = 20:00 UTC = 13:00 PST
*/
static class OffsetInterval {
+ /** The time the interval starts in seconds since start of epoch, inclusive. */
private final int startWallTimeSeconds;
+ /** The time the interval ends in seconds since start of epoch, exclusive. */
private final int endWallTimeSeconds;
private final int isDst;
private final int totalOffsetSeconds;
@@ -1243,40 +1267,60 @@
/**
* Creates an {@link OffsetInterval}.
*
- * <p>If {@code transitionIndex} is -1, the transition is synthesized to be a non-DST offset
- * that runs from the beginning of time until the first transition in {@code timeZone} and
- * has an offset of {@code timezone.mRawOffset}. If {@code transitionIndex} is the last
- * transition that transition is considered to run until the end of representable time.
+ * <p>If {@code transitionIndex} is -1, where possible the transition is synthesized to run
+ * from the beginning of 32-bit time until the first transition in {@code timeZone} with
+ * offset information based on the first type defined. If {@code transitionIndex} is the
+ * last transition, that transition is considered to run until the end of 32-bit time.
* Otherwise, the information is extracted from {@code timeZone.mTransitions},
- * {@code timeZone.mOffsets} an {@code timeZone.mIsDsts}.
+ * {@code timeZone.mOffsets} and {@code timeZone.mIsDsts}.
+ *
+ * <p>This method can return null when:
+ * <ol>
+ * <li>the {@code transitionIndex} is outside the allowed range, i.e.
+ * {@code transitionIndex < -1 || transitionIndex >= [the number of transitions]}.</li>
+ * <li>when calculations result in a zero-length interval. This is only expected to occur
+ * when dealing with transitions close to (or exactly at) {@code Integer.MIN_VALUE} and
+ * {@code Integer.MAX_VALUE} and where it's difficult to convert from UTC to local times.
+ * </li>
+ * </ol>
*/
- public static OffsetInterval create(ZoneInfo timeZone, int transitionIndex)
- throws CheckedArithmeticException {
-
+ public static OffsetInterval create(ZoneInfo timeZone, int transitionIndex) {
if (transitionIndex < -1 || transitionIndex >= timeZone.mTransitions.length) {
return null;
}
- int rawOffsetSeconds = timeZone.mRawOffset / 1000;
if (transitionIndex == -1) {
- int endWallTimeSeconds = checkedAdd(timeZone.mTransitions[0], rawOffsetSeconds);
- return new OffsetInterval(Integer.MIN_VALUE, endWallTimeSeconds, 0 /* isDst */,
- rawOffsetSeconds);
+ int totalOffsetSeconds = timeZone.mEarliestRawOffset / 1000;
+ int isDst = 0;
+
+ int startWallTimeSeconds = Integer.MIN_VALUE;
+ int endWallTimeSeconds =
+ saturated32BitAdd(timeZone.mTransitions[0], totalOffsetSeconds);
+ if (startWallTimeSeconds == endWallTimeSeconds) {
+ // There's no point in returning an OffsetInterval that lasts 0 seconds.
+ return null;
+ }
+ return new OffsetInterval(startWallTimeSeconds, endWallTimeSeconds, isDst,
+ totalOffsetSeconds);
}
+ int rawOffsetSeconds = timeZone.mRawOffset / 1000;
int type = timeZone.mTypes[transitionIndex] & 0xff;
int totalOffsetSeconds = timeZone.mOffsets[type] + rawOffsetSeconds;
int endWallTimeSeconds;
if (transitionIndex == timeZone.mTransitions.length - 1) {
- // If this is the last transition, make up the end time.
endWallTimeSeconds = Integer.MAX_VALUE;
} else {
- endWallTimeSeconds = checkedAdd(timeZone.mTransitions[transitionIndex + 1],
- totalOffsetSeconds);
+ endWallTimeSeconds = saturated32BitAdd(
+ timeZone.mTransitions[transitionIndex + 1], totalOffsetSeconds);
}
int isDst = timeZone.mIsDsts[type];
int startWallTimeSeconds =
- checkedAdd(timeZone.mTransitions[transitionIndex], totalOffsetSeconds);
+ saturated32BitAdd(timeZone.mTransitions[transitionIndex], totalOffsetSeconds);
+ if (startWallTimeSeconds == endWallTimeSeconds) {
+ // There's no point in returning an OffsetInterval that lasts 0 seconds.
+ return null;
+ }
return new OffsetInterval(
startWallTimeSeconds, endWallTimeSeconds, isDst, totalOffsetSeconds);
}
@@ -1317,11 +1361,11 @@
}
/**
- * Calculate (a + b).
+ * Calculate (a + b). The result must be in the Integer range otherwise an exception is thrown.
*
* @throws CheckedArithmeticException if overflow or underflow occurs
*/
- private static int checkedAdd(long a, int b) throws CheckedArithmeticException {
+ private static int checked32BitAdd(long a, int b) throws CheckedArithmeticException {
// Adapted from Guava IntMath.checkedAdd();
long result = a + b;
if (result != (int) result) {
@@ -1331,16 +1375,30 @@
}
/**
- * Calculate (a - b).
+ * Calculate (a - b). The result must be in the Integer range otherwise an exception is thrown.
*
* @throws CheckedArithmeticException if overflow or underflow occurs
*/
- private static int checkedSubtract(int a, int b) throws CheckedArithmeticException {
+ private static int checked32BitSubtract(long a, int b) throws CheckedArithmeticException {
// Adapted from Guava IntMath.checkedSubtract();
- long result = (long) a - b;
+ long result = a - b;
if (result != (int) result) {
throw new CheckedArithmeticException();
}
return (int) result;
}
+
+ /**
+ * Calculate (a + b). If the result would overflow or underflow outside of the Integer range
+ * Integer.MAX_VALUE or Integer.MIN_VALUE will be returned, respectively.
+ */
+ private static int saturated32BitAdd(long a, int b) {
+ long result = a + b;
+ if (result > Integer.MAX_VALUE) {
+ return Integer.MAX_VALUE;
+ } else if (result < Integer.MIN_VALUE) {
+ return Integer.MIN_VALUE;
+ }
+ return (int) result;
+ }
}
diff --git a/luni/src/test/java/libcore/java/util/TimeZoneTest.java b/luni/src/test/java/libcore/java/util/TimeZoneTest.java
index 6243359..68b6a54 100644
--- a/luni/src/test/java/libcore/java/util/TimeZoneTest.java
+++ b/luni/src/test/java/libcore/java/util/TimeZoneTest.java
@@ -82,17 +82,25 @@
}
public void testPreHistoricOffsets() throws Exception {
- // "Africa/Bissau" has just a few transitions and hasn't changed in a long time.
- // 1912-01-01 00:02:19-0100 ... 1912-01-01 00:02:20-0100
- // 1974-12-31 23:59:59-0100 ... 1975-01-01 01:00:00+0000
+ // Note: This test changed after P to account for previously incorrect handling of
+ // prehistoric offsets. http://b/118835133
+ // "Africa/Bissau" has just a few transitions:
+ // Date, Offset, IsDst
+ // 1901-12-13 20:45:52,-3740,0 (Integer.MIN_VALUE, implicit with zic <= 2014b)
+ // 1912-01-01 01:00:00,-3600,0
+ // 1975-01-01 01:00:00,0,0
TimeZone tz = TimeZone.getTimeZone("Africa/Bissau");
- // Times before our first transition should assume we're still following that transition.
- assertNonDaylightOffset(-3600, parseIsoTime("1911-01-01T00:00:00.0+0000"), tz);
+ // Before Integer.MIN_VALUE.
+ assertNonDaylightOffset(-3740, parseIsoTime("1900-01-01T00:00:00.0+0000"), tz);
+ // Times before 1912-01-01 01:00:00
+ assertNonDaylightOffset(-3740, parseIsoTime("1911-01-01T00:00:00.0+0000"), tz);
+
+ // Times after 1912-01-01 01:00:00 should use that transition.
assertNonDaylightOffset(-3600, parseIsoTime("1912-01-01T12:00:00.0-0100"), tz);
- // Times after our last transition should assume we're still following that transition.
+ // Times after 1975-01-01 01:00:00 should use that transition.
assertNonDaylightOffset(0, parseIsoTime("1980-01-01T00:00:00.0+0000"), tz);
}
@@ -309,18 +317,43 @@
return String.format("GMT%c%02d:%02d", sign, offset / 60, offset % 60);
}
- // http://b/18839557
+ /**
+ * This test is to check for 32-bit integer overflow / underflow in TimeZone offset
+ * calculations. A bug (http://b/18839557) was reported when someone noticed that Android's
+ * TimeZone didn't produce the same answers as other libraries at times just outside the range
+ * of Integer seconds. The reason was because of int overflow / underflow which has been fixed.
+ * At the time of writing, Android's java.util.TimeZone implementation only supports reading
+ * TZif version 1 data (32-bit times) and provides one additional "before first transition"
+ * type. This makes Android's time zone information outside of the Integer range unreliable and
+ * unlikely to match libraries that use 64-bit times for transitions and/or calculate times
+ * outside of the range using rules (e.g. like ICU4J does).
+ */
public void testOverflowing32BitUnixDates() {
final TimeZone tz = TimeZone.getTimeZone("America/New_York");
+ // These times are significant because they are outside the 32-bit range for seconds.
+ final long beforeInt32Seconds = -2206292400L; // Thu, 01 Feb 1900 05:00:00 GMT
+ final long afterInt32Seconds = 2206292400L; // Wed, 30 Nov 2039 19:00:00 GMT
+
+ final long lowerTimeMillis = beforeInt32Seconds * 1000L;
+ final long upperTimeMillis = afterInt32Seconds * 1000L;
+
// This timezone didn't have any daylight savings prior to 1917 and this
- // date is sometime in 1901.
- assertFalse(tz.inDaylightTime(new Date(-2206292400000L)));
- assertEquals(-18000000, tz.getOffset(-2206292400000L));
+ // date is in 1900.
+ assertFalse(tz.inDaylightTime(new Date(lowerTimeMillis)));
+
+ // http://b/118835133:
+ // zic <= 2014b produces data that suggests before -1633280400 seconds (Sun, 31 Mar 1918
+ // 07:00:00 GMT) the offset was -18000000.
+ // zic > 2014b produces data that suggests before Integer.MIN_VALUE seconds the offset was
+ // -17762000 and between Integer.MIN_VALUE and -1633280400 it was -18000000. Once Android
+ // moves to zic > 2014b the -18000000 can be removed.
+ int actualOffset = tz.getOffset(lowerTimeMillis);
+ assertTrue(-18000000 == actualOffset || -17762000 == actualOffset);
// Nov 30th 2039, no daylight savings as per current rules.
- assertFalse(tz.inDaylightTime(new Date(2206292400000L)));
- assertEquals(-18000000, tz.getOffset(2206292400000L));
+ assertFalse(tz.inDaylightTime(new Date(upperTimeMillis)));
+ assertEquals(-18000000, tz.getOffset(upperTimeMillis));
}
public void testTimeZoneIDLocalization() {
diff --git a/luni/src/test/java/libcore/libcore/util/ZoneInfoTest.java b/luni/src/test/java/libcore/libcore/util/ZoneInfoTest.java
index 91f1607..a151ee4 100644
--- a/luni/src/test/java/libcore/libcore/util/ZoneInfoTest.java
+++ b/luni/src/test/java/libcore/libcore/util/ZoneInfoTest.java
@@ -315,6 +315,134 @@
}
/**
+ * TimeZone APIs use long times in millis. Android uses TZif version 1 format data which
+ * uses 32-bit time values for transitions so it only gives accurate results for times in that
+ * range.
+ *
+ * <p>Newer versions of zic after 2014b introduce an explicit transition at the earliest
+ * representable time, which is Integer.MIN_VALUE for TZif version 1 files. Previously the type
+ * used was left implicit and readers were expected to use the first non-DST type in the file.
+ *
+ * <p>Testing newer zic versions demonstrated that Android had been mishandling the lookup of
+ * offset for times before the first transition. The logic has been corrected. This test would
+ * fail on versions of Android <= P.
+ */
+ public void testReadTimeZone_Bug118835133_extraFirstTransition() throws Exception {
+ // A time before the first representable time in a TZif version 1 file.
+ Instant before32BitTime = timeFromSeconds(Integer.MIN_VALUE).minusMillis(1);
+
+ // Times between the start of the 32-bit time range and the first "official" transition.
+ Instant[] earlyTimes = {
+ timeFromSeconds(Integer.MIN_VALUE),
+ timeFromSeconds(Integer.MIN_VALUE).plusMillis(1),
+ };
+
+ Instant firstRealTransitionTime = timeFromSeconds(1000);
+ Instant afterFirstRealTransitionTime = firstRealTransitionTime.plusSeconds(1);
+ Instant[] afterFirstRealTransitionTimes = {
+ firstRealTransitionTime,
+ afterFirstRealTransitionTime,
+ };
+
+ // A time to use as currentTime when building the TimeZone. Important for the getRawOffset()
+ // calculation.
+ Instant currentTime = afterFirstRealTransitionTime;
+
+ Duration type0Offset = offsetFromSeconds(0);
+ Duration type1Offset = offsetFromSeconds(1800);
+ Duration type2Offset = offsetFromSeconds(3600);
+ int[][] types = {
+ { offsetToSeconds(type0Offset), 0 }, // 1st type, used before first known transition.
+ { offsetToSeconds(type1Offset), 0 },
+ { offsetToSeconds(type2Offset), 0 },
+ };
+
+ // Creates a simulation of zic version <= 2014b where there is usually no explicit transition at
+ // Integer.MIN_VALUE seconds in TZif version 1 data.
+ {
+ int[][] transitions = {
+ { timeToSeconds(firstRealTransitionTime), 2 /* type 2 */ },
+ };
+ ZoneInfo oldZoneInfo = createZoneInfo(transitions, types, currentTime);
+ assertRawOffset(oldZoneInfo, type2Offset);
+
+ // We use the first non-DST type for times before the first transition.
+ assertOffsetAt(oldZoneInfo, type0Offset, before32BitTime);
+ assertOffsetAt(oldZoneInfo, type0Offset, earlyTimes);
+
+ // This is after the first transition, so type 2.
+ assertOffsetAt(oldZoneInfo, type2Offset, afterFirstRealTransitionTimes);
+ }
+
+ // Creates a simulation of zic version > 2014b where there is usually an explicit transition at
+ // Integer.MIN_VALUE seconds for TZif version 1 data.
+ {
+ int[][] transitions = {
+ { Integer.MIN_VALUE, 1 /* type 1 */ }, // The extra transition added by zic.
+ { timeToSeconds(firstRealTransitionTime), 2 /* type 2 */ },
+ };
+ ZoneInfo newZoneInfo = createZoneInfo(transitions, types, currentTime);
+ assertRawOffset(newZoneInfo, type2Offset);
+
+ // We use the first non-DST type for times before the first transition.
+ assertOffsetAt(newZoneInfo, type0Offset, before32BitTime);
+
+ // After the first transition, so type 1.
+ assertOffsetAt(newZoneInfo, type1Offset, earlyTimes);
+
+ // This is after the second transition, so type 2.
+ assertOffsetAt(newZoneInfo, type2Offset, afterFirstRealTransitionTimes);
+ }
+ }
+
+ /**
+ * Newer versions of zic after 2014b sometime introduce an explicit transition at
+ * Integer.MAX_VALUE.
+ */
+ public void testReadTimeZone_Bug118835133_extraLastTransition() throws Exception {
+ // An arbitrary time to use as currentTime. Not important for this test.
+ Instant currentTime = timeFromSeconds(4000);
+
+ // Offset before time 1000 should be consistent.
+ Instant[] timesToCheck = {
+ timeFromSeconds(2100), // arbitrary time > 2000
+ timeFromSeconds(Integer.MAX_VALUE).minusMillis(1),
+ timeFromSeconds(Integer.MAX_VALUE),
+ timeFromSeconds(Integer.MAX_VALUE).plusMillis(1),
+ };
+
+ int latestOffsetSeconds = 3600;
+ int[][] types = {
+ { 1800, 0 },
+ { latestOffsetSeconds, 0 },
+ };
+ Duration expectedLateOffset = offsetFromSeconds(latestOffsetSeconds);
+
+ // Create a simulation of zic version <= 2014b where there is usually no explicit transition at
+ // Integer.MAX_VALUE seconds.
+ {
+ int[][] transitions = {
+ { 1000, 0 },
+ { 2000, 1 },
+ };
+ ZoneInfo oldZoneInfo = createZoneInfo(transitions, types, currentTime);
+ assertOffsetAt(oldZoneInfo, expectedLateOffset, timesToCheck);
+ }
+
+ // Create a simulation of zic version > 2014b where there is sometimes an explicit transition at
+ // Integer.MAX_VALUE seconds.
+ {
+ int[][] transitions = {
+ { 1000, 0 },
+ { 2000, 1 },
+ { Integer.MAX_VALUE, 1}, // The extra transition.
+ };
+ ZoneInfo newZoneInfo = createZoneInfo(transitions, types, currentTime);
+ assertOffsetAt(newZoneInfo, expectedLateOffset, timesToCheck);
+ }
+ }
+
+ /**
* Checks to make sure that it can handle up to 256 types.
*/
public void testReadTimeZone_LotsOfTypes() throws Exception {
@@ -580,10 +708,26 @@
return Instant.ofEpochSecond(timeInSeconds);
}
+ private static int timeToSeconds(Instant time) {
+ long seconds = time.getEpochSecond();
+ if (seconds < Integer.MIN_VALUE || seconds > Integer.MAX_VALUE) {
+ fail("Time out of seconds range: " + time);
+ }
+ return (int) seconds;
+ }
+
private static Duration offsetFromSeconds(int offsetSeconds) {
return Duration.ofSeconds(offsetSeconds);
}
+ private static int offsetToSeconds(Duration offset) {
+ long seconds = offset.getSeconds();
+ if (seconds < Integer.MIN_VALUE || seconds > Integer.MAX_VALUE) {
+ fail("Offset out of seconds range: " + offset);
+ }
+ return (int) seconds;
+ }
+
private ZoneInfo createZoneInfo(int[][] transitions, int[][] types)
throws Exception {
return createZoneInfo(getName(), transitions, types, Instant.now());