blob: 5a8caf20e4fd18eedee99b1eb6fd208e6d15ab1c [file] [log] [blame]
/*
* Copyright (C) 2007 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.util;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.Formatter;
import java.util.TimeZone;
/**
* Our concrete TimeZone implementation, backed by zoneinfo data.
*
* @hide - used to implement TimeZone
*/
public final class ZoneInfo extends TimeZone {
private static final long MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
private static final long MILLISECONDS_PER_400_YEARS =
MILLISECONDS_PER_DAY * (400 * 365 + 100 - 3);
private static final long UNIX_OFFSET = 62167219200000L;
private static final int[] NORMAL = new int[] {
0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334,
};
private static final int[] LEAP = new int[] {
0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335,
};
private int mRawOffset;
private final int mEarliestRawOffset;
private final int[] mTransitions;
private final int[] mOffsets;
private final byte[] mTypes;
private final byte[] mIsDsts;
private final boolean mUseDst;
ZoneInfo(String name, int[] transitions, byte[] type, int[] gmtOffsets, byte[] isDsts) {
mTransitions = transitions;
mTypes = type;
mIsDsts = isDsts;
setID(name);
// Use the latest non-daylight offset (if any) as the raw offset.
int lastStd;
for (lastStd = mTransitions.length - 1; lastStd >= 0; lastStd--) {
if (mIsDsts[mTypes[lastStd] & 0xff] == 0) {
break;
}
}
if (lastStd < 0) {
lastStd = 0;
}
if (lastStd >= mTypes.length) {
mRawOffset = gmtOffsets[0];
} else {
mRawOffset = gmtOffsets[mTypes[lastStd] & 0xff];
}
// 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;
break;
}
}
int earliestRawOffset = (firstStd != -1) ? gmtOffsets[mTypes[firstStd] & 0xff] : 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.
mOffsets = gmtOffsets;
for (int i = 0; i < mOffsets.length; i++) {
mOffsets[i] -= mRawOffset;
}
// Is this zone still observing DST?
// We don't care if they've historically used it: most places have at least once.
// We want to know whether the last "schedule info" (the unix times in the mTransitions
// array) is in the future. If it is, DST is still relevant.
// See http://code.google.com/p/android/issues/detail?id=877.
// This test means that for somewhere like Morocco, which tried DST in 2009 but has
// no future plans (and thus no future schedule info) will report "true" from
// useDaylightTime at the start of 2009 but "false" at the end. This seems appropriate.
boolean usesDst = false;
long currentUnixTime = System.currentTimeMillis() / 1000;
if (mTransitions.length > 0) {
// (We're really dealing with uint32_t values, so long is most convenient in Java.)
long latestScheduleTime = ((long) mTransitions[mTransitions.length - 1]) & 0xffffffff;
if (currentUnixTime < latestScheduleTime) {
usesDst = true;
}
}
mUseDst = usesDst;
mRawOffset *= 1000;
mEarliestRawOffset = earliestRawOffset * 1000;
}
@Override
public int getOffset(int era, int year, int month, int day, int dayOfWeek, int millis) {
// XXX This assumes Gregorian always; Calendar switches from
// Julian to Gregorian in 1582. What calendar system are the
// arguments supposed to come from?
long calc = (year / 400) * MILLISECONDS_PER_400_YEARS;
year %= 400;
calc += year * (365 * MILLISECONDS_PER_DAY);
calc += ((year + 3) / 4) * MILLISECONDS_PER_DAY;
if (year > 0) {
calc -= ((year - 1) / 100) * MILLISECONDS_PER_DAY;
}
boolean isLeap = (year == 0 || (year % 4 == 0 && year % 100 != 0));
int[] mlen = isLeap ? LEAP : NORMAL;
calc += mlen[month] * MILLISECONDS_PER_DAY;
calc += (day - 1) * MILLISECONDS_PER_DAY;
calc += millis;
calc -= mRawOffset;
calc -= UNIX_OFFSET;
return getOffset(calc);
}
@Override
public int getOffset(long when) {
int unix = (int) (when / 1000);
int transition = Arrays.binarySearch(mTransitions, unix);
if (transition < 0) {
transition = ~transition - 1;
if (transition < 0) {
// Assume that all times before our first transition correspond to the
// oldest-known non-daylight offset. The obvious alternative would be to
// use the current raw offset, but that seems like a greater leap of faith.
return mEarliestRawOffset;
}
}
return mRawOffset + mOffsets[mTypes[transition] & 0xff] * 1000;
}
@Override public boolean inDaylightTime(Date time) {
long when = time.getTime();
int unix = (int) (when / 1000);
int transition = Arrays.binarySearch(mTransitions, unix);
if (transition < 0) {
transition = ~transition - 1;
if (transition < 0) {
// Assume that all times before our first transition are non-daylight.
// Transition data tends to start with a transition to daylight, so just
// copying the first transition would assume the opposite.
// http://code.google.com/p/android/issues/detail?id=14395
return false;
}
}
return mIsDsts[mTypes[transition] & 0xff] == 1;
}
@Override public int getRawOffset() {
return mRawOffset;
}
@Override public void setRawOffset(int off) {
mRawOffset = off;
}
@Override public boolean useDaylightTime() {
return mUseDst;
}
@Override public boolean hasSameRules(TimeZone timeZone) {
if (!(timeZone instanceof ZoneInfo)) {
return false;
}
ZoneInfo other = (ZoneInfo) timeZone;
if (mUseDst != other.mUseDst) {
return false;
}
if (!mUseDst) {
return mRawOffset == other.mRawOffset;
}
return mRawOffset == other.mRawOffset
// Arrays.equals returns true if both arrays are null
&& Arrays.equals(mOffsets, other.mOffsets)
&& Arrays.equals(mIsDsts, other.mIsDsts)
&& Arrays.equals(mTypes, other.mTypes)
&& Arrays.equals(mTransitions, other.mTransitions);
}
@Override public boolean equals(Object obj) {
if (!(obj instanceof ZoneInfo)) {
return false;
}
ZoneInfo other = (ZoneInfo) obj;
return getID().equals(other.getID()) && hasSameRules(other);
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + getID().hashCode();
result = prime * result + Arrays.hashCode(mOffsets);
result = prime * result + Arrays.hashCode(mIsDsts);
result = prime * result + mRawOffset;
result = prime * result + Arrays.hashCode(mTransitions);
result = prime * result + Arrays.hashCode(mTypes);
result = prime * result + (mUseDst ? 1231 : 1237);
return result;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
// First the basics...
sb.append(getClass().getName() + "[" + getID() + ",mRawOffset=" + mRawOffset +
",mUseDst=" + mUseDst + "]");
// ...followed by a zdump(1)-like description of all our transition data.
sb.append("\n");
Formatter f = new Formatter(sb);
for (int i = 0; i < mTransitions.length; ++i) {
int type = mTypes[i] & 0xff;
String utcTime = formatTime(mTransitions[i], TimeZone.getTimeZone("UTC"));
String localTime = formatTime(mTransitions[i], this);
int offset = mOffsets[type];
int gmtOffset = mRawOffset/1000 + offset;
f.format("%4d : time=%11d %s = %s isDst=%d offset=%5d gmtOffset=%d\n",
i, mTransitions[i], utcTime, localTime, mIsDsts[type], offset, gmtOffset);
}
return sb.toString();
}
private static String formatTime(int s, TimeZone tz) {
SimpleDateFormat sdf = new SimpleDateFormat("EEE MMM dd HH:mm:ss yyyy zzz");
sdf.setTimeZone(tz);
long ms = ((long) s) * 1000L;
return sdf.format(new Date(ms));
}
}