blob: 8b9715f01d8ad8a03558d799608bf87ec9a6fb15 [file] [log] [blame]
/*
* Copyright (C) 2016 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package libcore.libcore.util;
import junit.framework.TestCase;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.nio.ByteBuffer;
import java.time.Duration;
import java.time.Instant;
import java.util.Arrays;
import java.util.Date;
import libcore.io.BufferIterator;
import libcore.timezone.ZoneInfoDb;
import libcore.timezone.testing.ZoneInfoTestHelper;
import libcore.util.ZoneInfo;
/**
* Tests for {@link ZoneInfo}
*/
public class ZoneInfoTest extends TestCase {
/**
* Checks that a {@link ZoneInfo} cannot be created without any types.
*/
public void testMakeTimeZone_NoTypes() throws Exception {
long[][] transitions = {};
int[][] types = {};
try {
createZoneInfo(transitions, types);
fail();
} catch (IOException expected) {
}
}
/**
* Checks that a {@link ZoneInfo} can be created with one type and no transitions.
*/
public void testMakeTimeZone_OneType_NoTransitions() throws Exception {
long[][] transitions = {};
int[][] types = {
{ 4800, 0 }
};
ZoneInfo zoneInfo = createZoneInfo(transitions, types);
// If there are no transitions then the offset should be constant irrespective of the time.
Instant[] times = {
Instant.ofEpochMilli(Long.MIN_VALUE),
Instant.ofEpochMilli(0),
Instant.ofEpochMilli(Long.MAX_VALUE),
};
assertOffsetAt(zoneInfo, offsetFromSeconds(4800), times);
// No transitions means no DST.
assertFalse("Doesn't use DST", zoneInfo.useDaylightTime());
assertDSTSavings(zoneInfo, offsetFromSeconds(0));
// The raw offset should be the offset of the first type.
assertRawOffset(zoneInfo, offsetFromSeconds(4800));
}
/**
* Checks that a {@link ZoneInfo} can be created with one non-DST transition.
*/
public void testReadTimeZone_OneNonDstTransition() throws Exception {
long[][] transitions = {
{ 0, 0 }
};
int[][] types = {
{ 3600, 0 }
};
ZoneInfo zoneInfo = createZoneInfo(transitions, types);
// Any time before the first transition is assumed to use the first standard transition.
Instant[] times = { timeFromSeconds(-2), timeFromSeconds(0), timeFromSeconds(2) };
assertOffsetAt(zoneInfo, offsetFromSeconds(3600), times);
// No transitions means no DST.
assertFalse("Doesn't use DST", zoneInfo.useDaylightTime());
assertDSTSavings(zoneInfo, offsetFromSeconds(0));
// The raw offset should be the offset of the first type.
assertRawOffset(zoneInfo, offsetFromSeconds(3600));
}
/**
* Checks that a {@link ZoneInfo} cannot be created with one DST but no non-DSTs transitions.
*/
public void testReadTimeZone_OneDstTransition() throws Exception {
long[][] transitions = {
{ 0, 0 }
};
int[][] types = {
{ 3600, 1 }
};
try {
createZoneInfo(transitions, types);
fail("Did not detect no non-DST transitions");
} catch (IllegalStateException expected) {
}
}
/**
* Checks to make sure that rounding the time from milliseconds to seconds does not cause issues
* around the boundary of negative transitions.
*/
public void testReadTimeZone_NegativeTransition() throws Exception {
long[][] transitions = {
{ -2000, 0 },
{ -5, 1 },
{ 0, 2 },
};
int[][] types = {
{ 1800, 0 },
{ 3600, 1 },
{ 5400, 0 }
};
ZoneInfo zoneInfo = createZoneInfo(transitions, types);
Instant transitionTime = timeFromSeconds(-5);
// Even a millisecond before a transition means that the transition is not active.
Instant beforeTransitionTime = transitionTime.minusMillis(1);
assertOffsetAt(zoneInfo, offsetFromSeconds(1800), beforeTransitionTime);
assertInDaylightTime(zoneInfo, beforeTransitionTime, false);
// A time equal to the transition point activates the transition.
assertOffsetAt(zoneInfo, offsetFromSeconds(3600), transitionTime);
assertInDaylightTime(zoneInfo, transitionTime, true);
// A time after the transition point but before the next activates the transition.
Instant afterTransitionTime = transitionTime.plusMillis(1);
assertOffsetAt(zoneInfo, offsetFromSeconds(3600), afterTransitionTime);
assertInDaylightTime(zoneInfo, afterTransitionTime, true);
assertFalse("Doesn't use DST", zoneInfo.useDaylightTime());
assertDSTSavings(zoneInfo, offsetFromSeconds(0));
assertRawOffset(zoneInfo, offsetFromSeconds(5400));
}
/**
* Checks to make sure that rounding the time from milliseconds to seconds does not cause issues
* around the boundary of positive transitions.
*/
public void testReadTimeZone_PositiveTransition() throws Exception {
long[][] transitions = {
{ 0, 0 },
{ 5, 1 },
{ 2000, 2 },
};
int[][] types = {
{ 1800, 0 },
{ 3600, 1 },
{ 5400, 0 }
};
ZoneInfo zoneInfo = createZoneInfo(transitions, types);
Instant transitionTime = timeFromSeconds(5);
// Even a millisecond before a transition means that the transition is not active.
Instant beforeTransitionTime = transitionTime.minusMillis(1);
assertOffsetAt(zoneInfo, offsetFromSeconds(1800), beforeTransitionTime);
assertInDaylightTime(zoneInfo, beforeTransitionTime, false);
// A time equal to the transition point activates the transition.
assertOffsetAt(zoneInfo, offsetFromSeconds(3600), transitionTime);
assertInDaylightTime(zoneInfo, transitionTime, true);
// A time after the transition point but before the next activates the transition.
Instant afterTransitionTime = transitionTime.plusMillis(1);
assertOffsetAt(zoneInfo, offsetFromSeconds(3600), afterTransitionTime);
assertInDaylightTime(zoneInfo, afterTransitionTime, true);
assertFalse("Doesn't use DST", zoneInfo.useDaylightTime());
assertDSTSavings(zoneInfo, offsetFromSeconds(0));
assertRawOffset(zoneInfo, offsetFromSeconds(5400));
}
/**
* Checks that creating a {@link ZoneInfo} with future DST transitions but no past DST
* transitions where the transition times are negative is not affected by rounding issues.
*/
public void testReadTimeZone_HasFutureDST_NoPastDST_NegativeTransitions() throws Exception {
long[][] transitions = {
{ -2000, 0 },
{ -500, 1 },
{ -100, 2 },
};
int[][] types = {
{ 1800, 0 },
{ 3600, 0 },
{ 5400, 1 }
};
// The expected DST savings is the difference between the DST offset (which includes the
// raw offset) and the preceding non-DST offset (which should just be the raw offset).
// Or in other words (5400 - 3600) * 1000
Duration expectedDSTSavings = offsetFromSeconds(5400 - 3600);
ZoneInfo zoneInfo = createZoneInfo(transitions, types, timeFromSeconds(-700));
assertTrue("Should use DST but doesn't", zoneInfo.useDaylightTime());
assertDSTSavings(zoneInfo, expectedDSTSavings);
// Now create one a few milliseconds before the DST transition to make sure that rounding
// errors don't cause a problem.
zoneInfo = createZoneInfo(transitions, types, timeFromSeconds(-100).minusMillis(5));
assertTrue("Should use DST but doesn't", zoneInfo.useDaylightTime());
assertDSTSavings(zoneInfo, expectedDSTSavings);
}
/**
* Checks that creating a {@link ZoneInfo} with future DST transitions but no past DST
* transitions where the transition times are positive is not affected by rounding issues.
*/
public void testReadTimeZone_HasFutureDST_NoPastDST_PositiveTransitions() throws Exception {
long[][] transitions = {
{ 4000, 0 },
{ 5500, 1 },
{ 6000, 2 },
};
int[][] types = {
{ 1800, 0 },
{ 3600, 0 },
{ 7200, 1 }
};
// The expected DST savings is the difference between the DST offset (which includes the
// raw offset) and the preceding non-DST offset (which should just be the raw offset).
// Or in other words (7200 - 3600) * 1000
Duration expectedDSTSavings = offsetFromSeconds(7200 - 3600);
ZoneInfo zoneInfo = createZoneInfo(transitions, types, timeFromSeconds(4500) /* currentTime */);
assertTrue("Should use DST but doesn't", zoneInfo.useDaylightTime());
assertDSTSavings(zoneInfo, expectedDSTSavings);
// Now create one a few milliseconds before the DST transition to make sure that rounding
// errors don't cause a problem.
zoneInfo = createZoneInfo(transitions, types, timeFromSeconds(6000).minusMillis(5));
assertTrue("Should use DST but doesn't", zoneInfo.useDaylightTime());
assertDSTSavings(zoneInfo, expectedDSTSavings);
}
/**
* Checks that creating a {@link ZoneInfo} with past DST transitions but no future DST
* transitions where the transition times are negative is not affected by rounding issues.
*/
public void testReadTimeZone_HasPastDST_NoFutureDST_NegativeTransitions() throws Exception {
long[][] transitions = {
{ -5000, 0 },
{ -2000, 1 },
{ -500, 0 },
{ 0, 2 },
};
int[][] types = {
{ 3600, 0 },
{ 1800, 1 },
{ 5400, 0 }
};
ZoneInfo zoneInfo = createZoneInfo(transitions, types, timeFromSeconds(-1) /* currentTime */);
assertFalse("Shouldn't use DST but does", zoneInfo.useDaylightTime());
assertDSTSavings(zoneInfo, offsetFromSeconds(0));
// Now create one a few milliseconds after the DST transition to make sure that rounding
// errors don't cause a problem.
zoneInfo = createZoneInfo(transitions, types, timeFromSeconds(-2000).plusMillis(5));
assertFalse("Shouldn't use DST but does", zoneInfo.useDaylightTime());
assertDSTSavings(zoneInfo, offsetFromSeconds(0));
}
/**
* Checks that creating a {@link ZoneInfo} with past DST transitions but no future DST
* transitions where the transition times are positive is not affected by rounding issues.
*/
public void testReadTimeZone_HasPastDST_NoFutureDST_PositiveTransitions() throws Exception {
long[][] transitions = {
{ 1000, 0 },
{ 4000, 1 },
{ 5500, 0 },
{ 6000, 2 },
};
int[][] types = {
{ 3600, 0 },
{ 1800, 1 },
{ 5400, 0 }
};
ZoneInfo zoneInfo = createZoneInfo(transitions, types, timeFromSeconds(4700));
assertFalse("Shouldn't use DST but does", zoneInfo.useDaylightTime());
assertDSTSavings(zoneInfo, offsetFromSeconds(0));
// Now create one a few milliseconds after the DST transition to make sure that rounding
// errors don't cause a problem.
zoneInfo = createZoneInfo(transitions, types, timeFromSeconds(4000).plusMillis(5));
assertFalse("Shouldn't use DST but does", zoneInfo.useDaylightTime());
assertDSTSavings(zoneInfo, offsetFromSeconds(0));
}
/**
* TimeZone APIs use Java long times in millis.
*
* <p>Android has historically mishandled the lookup of offset for times before Integer.MIN_VALUE
* seconds for various reasons. 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 seconds with a java int.
Instant before32BitTime = timeFromSeconds(Integer.MIN_VALUE).minusMillis(1);
// Times around the 32-bit seconds minimum.
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 },
};
// Simulates a zone with a single transition.
{
long[][] 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);
}
// Simulation a zone where there is an explicit transition at Integer.MIN_VALUE seconds. This
// used to be common when Android used 32-bit data from the TZif file.
{
long[][] transitions = {
{ Integer.MIN_VALUE, 1 /* type 1 */ },
{ 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 seconds.
*/
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 a zone where there is no explicit transition at Integer.MAX_VALUE
// seconds.
{
long[][] transitions = {
{ 1000, 0 },
{ 2000, 1 },
};
ZoneInfo oldZoneInfo = createZoneInfo(transitions, types, currentTime);
assertOffsetAt(oldZoneInfo, expectedLateOffset, timesToCheck);
}
// Create a simulation of a zone where there is an explicit transition at Integer.MAX_VALUE
// seconds.
{
long[][] 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 ZoneInfo can handle up to 256 types.
*/
public void testReadTimeZone_MaxTypeCount() throws Exception {
long[][] transitions = {
{ -2000, 255 },
};
// Create 256 types, each with zero offset and without DST except the last, which is offset by
// one hour but also without DST.
int[][] types = new int[256][];
Arrays.fill(types, new int[2]);
types[255] = new int[] { 3600, 0 };
ZoneInfo zoneInfo = createZoneInfo(getName(), transitions, types,
timeFromSeconds(Integer.MIN_VALUE));
assertFalse("Shouldn't use DST but does", zoneInfo.useDaylightTime());
assertDSTSavings(zoneInfo, offsetFromSeconds(0));
// Make sure that WallTime works properly with a ZoneInfo with 256 types.
ZoneInfo.WallTime wallTime = new ZoneInfo.WallTime();
wallTime.localtime(0, zoneInfo);
wallTime.mktime(zoneInfo);
}
/**
* Create an instance for every available time zone for which we have data to ensure that they
* can all be handled correctly.
*
* <p>This is to ensure that ZoneInfo can read all time zone data without failing, it doesn't
* check that it reads it correctly or that the data itself is correct. This is a sanity test
* to ensure that any additional checks added to the code that reads the data source and
* creates the {@link ZoneInfo} instances does not prevent any of the time zones being loaded.
*/
public void testReadTimeZone_All() throws Exception {
ZoneInfoDb instance = ZoneInfoDb.getInstance();
String[] availableIDs = instance.getAvailableIDs();
Arrays.sort(availableIDs);
for (String id : availableIDs) {
BufferIterator bufferIterator = instance.getBufferIterator(id);
// Create a ZoneInfo at the earliest possible time to allow us to use the
// useDaylightTime() method to check whether it ever has or ever will support daylight
// savings time.
ZoneInfo zoneInfo = ZoneInfo.readTimeZone(id, bufferIterator, Long.MIN_VALUE);
assertNotNull("TimeZone " + id + " was not created", zoneInfo);
assertEquals(id, zoneInfo.getID());
}
}
public void testReadTimeZone_Valid() throws Exception {
ZoneInfoTestHelper.ZicDataBuilder builder =
new ZoneInfoTestHelper.ZicDataBuilder()
.initializeToValid();
assertNotNull(createZoneInfo(getName(), Instant.now(), builder.build()));
}
public void testReadTimeZone_BadMagic() {
ZoneInfoTestHelper.ZicDataBuilder builder =
new ZoneInfoTestHelper.ZicDataBuilder()
.initializeToValid()
.setMagic(0xdeadbeef); // Bad magic.
try {
createZoneInfo(getName(), Instant.now(), builder.build());
fail();
} catch (IOException expected) {}
}
/**
* Checks to make sure that ZoneInfo rejects more than 256 types.
*/
public void testReadTimeZone_TooManyTypes() {
int typeCount = 257; // Max types allowed is 256
int transitionCount = 5;
long[][] transitions = createTransitions(transitionCount, typeCount);
int[][] types = createTypes(typeCount);
ZoneInfoTestHelper.ZicDataBuilder builder =
new ZoneInfoTestHelper.ZicDataBuilder()
.initializeToValid()
.setTransitionsAndTypes(transitions, types);
byte[] bytes = builder.build();
try {
createZoneInfo(getName(), Instant.now(), bytes);
fail("Did not detect too many types");
} catch (IOException expected) {
}
}
/**
* Checks to make sure that ZoneInfo rejects more than 2000 transitions.
*/
public void testReadTimeZone_TooManyTransitions() {
int typeCount = 5;
int transitionCount = 2001; // Max transitions allowed is 2000.
long[][] transitions = createTransitions(transitionCount, typeCount);
int[][] types = createTypes(typeCount);
ZoneInfoTestHelper.ZicDataBuilder builder =
new ZoneInfoTestHelper.ZicDataBuilder()
.initializeToValid()
.setTransitionsAndTypes(transitions, types);
byte[] bytes = builder.build();
try {
createZoneInfo(getName(), Instant.now(), bytes);
fail("Did not detect too many transitions");
} catch (IOException expected) {
}
}
public void testReadTimeZone_TransitionsNotSorted() {
long[][] transitions = {
{ 1000, 0 },
{ 3000, 1 }, // Out of transition order.
{ 2000, 0 },
};
int[][] types = {
{ 3600, 0 },
{ 1800, 1 },
};
ZoneInfoTestHelper.ZicDataBuilder builder =
new ZoneInfoTestHelper.ZicDataBuilder()
.initializeToValid()
.setTransitionsAndTypes(transitions, types);
byte[] bytes = builder.build();
try {
createZoneInfo(getName(), Instant.now(), bytes);
fail();
} catch (IOException expected) {
}
}
public void testReadTimeZone_InvalidTypeIndex() {
long[][] transitions = {
{ 1000, 0 },
{ 2000, 2 }, // Invalid type index - only 0 and 1 defined below.
{ 3000, 0 },
};
int[][] types = {
{ 3600, 0 },
{ 1800, 1 },
};
ZoneInfoTestHelper.ZicDataBuilder builder =
new ZoneInfoTestHelper.ZicDataBuilder()
.initializeToValid()
.setTransitionsAndTypes(transitions, types);
byte[] bytes = builder.build();
try {
createZoneInfo(getName(), Instant.now(), bytes);
fail();
} catch (IOException expected) {
}
}
public void testReadTimeZone_InvalidIsDst() {
long[][] transitions = {
{ 1000, 0 },
{ 2000, 1 },
{ 3000, 0 },
};
int[][] types = {
{ 3600, 0 },
{ 1800, 2 }, // Invalid isDst - must be 0 or 1
};
ZoneInfoTestHelper.ZicDataBuilder builder =
new ZoneInfoTestHelper.ZicDataBuilder()
.initializeToValid()
.setTransitionsAndTypes(transitions, types);
byte[] bytes = builder.build();
try {
createZoneInfo(getName(), Instant.now(), bytes);
fail();
} catch (IOException expected) {
}
}
/**
* Checks that we can read the serialized form of a {@link ZoneInfo} created in pre-OpenJDK
* AOSP.
*
* <p>One minor difference is that in pre-OpenJDK {@link ZoneInfo#mDstSavings} can be non-zero
* even if {@link ZoneInfo#mUseDst} was false. That was not visible externally (except through
* the {@link ZoneInfo#toString()} method) as the {@link ZoneInfo#getDSTSavings()} would check
* {@link ZoneInfo#mUseDst} and if it was false then would return 0. This checks to make sure
* that is handled properly. See {@link ZoneInfo#readObject(ObjectInputStream)}.
*/
public void testReadSerialized() throws Exception {
ZoneInfo zoneInfoRead;
try (InputStream is = getClass().getResourceAsStream("ZoneInfoTest_ZoneInfo.golden.ser");
ObjectInputStream ois = new ObjectInputStream(is)) {
Object object = ois.readObject();
assertTrue("Not a ZoneInfo instance", object instanceof ZoneInfo);
zoneInfoRead = (ZoneInfo) object;
}
long[][] transitions = {
{ -5000, 0 },
{ -2000, 1 },
{ -500, 0 },
{ 0, 2 },
};
int[][] types = {
{ 3600, 0 },
{ 1800, 1 },
{ 5400, 0 }
};
ZoneInfo zoneInfoCreated = createZoneInfo(
"test", transitions, types, timeFromSeconds(-1));
assertEquals("Read ZoneInfo does not match created one", zoneInfoCreated, zoneInfoRead);
assertEquals("useDaylightTime() mismatch",
zoneInfoCreated.useDaylightTime(), zoneInfoRead.useDaylightTime());
assertEquals("getDSTSavings() mismatch",
zoneInfoCreated.getDSTSavings(), zoneInfoRead.getDSTSavings());
}
private static void assertRawOffset(ZoneInfo zoneInfo, Duration expectedOffset) {
assertEquals(expectedOffset.toMillis(), zoneInfo.getRawOffset());
}
private static void assertDSTSavings(ZoneInfo zoneInfo, Duration expectedDSTSavings) {
assertEquals(expectedDSTSavings.toMillis(), zoneInfo.getDSTSavings());
}
private static void assertInDaylightTime(ZoneInfo zoneInfo, Instant time, boolean expectedValue) {
assertEquals(expectedValue, zoneInfo.inDaylightTime(new Date(time.toEpochMilli())));
}
private static void assertOffsetAt(
ZoneInfo zoneInfo, Duration expectedOffset, Instant... times) {
for (Instant time : times) {
assertEquals("Unexpected offset at " + time,
expectedOffset.toMillis(), zoneInfo.getOffset(time.toEpochMilli()));
}
}
private static Instant timeFromSeconds(long timeInSeconds) {
return Instant.ofEpochSecond(timeInSeconds);
}
private static long timeToSeconds(Instant time) {
return time.getEpochSecond();
}
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(long[][] transitions, int[][] types)
throws Exception {
return createZoneInfo(getName(), transitions, types, Instant.now());
}
private ZoneInfo createZoneInfo(long[][] transitions, int[][] types, Instant currentTime)
throws Exception {
return createZoneInfo(getName(), transitions, types, currentTime);
}
private ZoneInfo createZoneInfo(String name, long[][] transitions, int[][] types,
Instant currentTime) throws Exception {
ZoneInfoTestHelper.ZicDataBuilder builder =
new ZoneInfoTestHelper.ZicDataBuilder()
.setTransitionsAndTypes(transitions, types);
return createZoneInfo(name, currentTime, builder.build());
}
private static ZoneInfo createZoneInfo(String name, Instant currentTime, byte[] bytes)
throws IOException {
ByteBufferIterator bufferIterator = new ByteBufferIterator(ByteBuffer.wrap(bytes));
return ZoneInfo.readTimeZone(
"TimeZone for '" + name + "'", bufferIterator, currentTime.toEpochMilli());
}
/**
* Creates {@code typeCount} "types" for use with
* {@link ZoneInfoTestHelper.ZicDataBuilder#setTypes(int[][])} and related methods. Each type is
* given an arbitrary offset and "isDst" value.
*/
private static int[][] createTypes(int typeCount) {
int[][] types = new int[typeCount][2];
for (int i = 0; i < typeCount; i++) {
// [0] holds the offset from UTC in seconds.
types[i][0] = typeCount;
// [1] holds isDst: 0 == STD, 1 == DST
types[i][1] = typeCount % 2;
}
return types;
}
/**
* Creates {@code transitionCount} "transition pairs" for use with
* {@link ZoneInfoTestHelper.ZicDataBuilder#setTransitions(long[][])} and related methods. Each
* transition is given an arbitrary (but increasing) time referencing an arbitrary type.
*/
private static long[][] createTransitions(int transitionCount, int typeCount) {
long[][] transitions = new long[transitionCount][2];
for (int i = 0; i < transitionCount; i++) {
// [0] holds the transition time.
transitions[i][0] = (i * 3600) + 100;
// [1] holds the type index to use. Must be > 0 and < typeCount to be valid.
transitions[i][1] = i % typeCount;
}
return transitions;
}
/**
* A {@link BufferIterator} that wraps a {@link ByteBuffer}.
*/
private static class ByteBufferIterator extends BufferIterator {
private final ByteBuffer buffer;
public ByteBufferIterator(ByteBuffer buffer) {
this.buffer = buffer;
}
@Override
public void seek(int offset) {
buffer.position(offset);
}
@Override
public void skip(int byteCount) {
buffer.position(buffer.position() + byteCount);
}
@Override
public int pos() {
return buffer.position();
}
@Override
public void readByteArray(byte[] bytes, int arrayOffset, int byteCount) {
buffer.get(bytes, arrayOffset, byteCount);
}
@Override
public byte readByte() {
return buffer.get();
}
@Override
public int readInt() {
int value = buffer.asIntBuffer().get();
// Using a separate view does not update the position of this buffer so do it
// explicitly.
skip(Integer.BYTES);
return value;
}
@Override
public void readIntArray(int[] ints, int arrayOffset, int intCount) {
buffer.asIntBuffer().get(ints, arrayOffset, intCount);
// Using a separate view does not update the position of this buffer so do it
// explicitly.
skip(Integer.BYTES * intCount);
}
@Override
public void readLongArray(long[] longs, int arrayOffset, int longCount) {
buffer.asLongBuffer().get(longs, arrayOffset, longCount);
// Using a separate view does not update the position of this buffer so do it
// explicitly.
skip(Long.BYTES * longCount);
}
@Override
public short readShort() {
short value = buffer.asShortBuffer().get();
// Using a separate view does not update the position of this buffer so do it
// explicitly.
skip(Short.BYTES);
return value;
}
}
}