com.ibm.icu.impl.OlsonTimeZone Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of icu4j Show documentation
Show all versions of icu4j Show documentation
International Component for Unicode for Java (ICU4J) is a mature, widely used Java library
providing Unicode and Globalization support
// © 2016 and later: Unicode, Inc. and others.
// License & terms of use: http://www.unicode.org/copyright.html
/*
*******************************************************************************
* Copyright (C) 2005-2016, International Business Machines Corporation and
* others. All Rights Reserved.
*******************************************************************************
*/
package com.ibm.icu.impl;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Arrays;
import java.util.Date;
import java.util.MissingResourceException;
import com.ibm.icu.util.AnnualTimeZoneRule;
import com.ibm.icu.util.BasicTimeZone;
import com.ibm.icu.util.Calendar;
import com.ibm.icu.util.DateTimeRule;
import com.ibm.icu.util.GregorianCalendar;
import com.ibm.icu.util.InitialTimeZoneRule;
import com.ibm.icu.util.SimpleTimeZone;
import com.ibm.icu.util.TimeArrayTimeZoneRule;
import com.ibm.icu.util.TimeZone;
import com.ibm.icu.util.TimeZoneRule;
import com.ibm.icu.util.TimeZoneTransition;
import com.ibm.icu.util.UResourceBundle;
/**
* A time zone based on the Olson tz database. Olson time zones change
* behavior over time. The raw offset, rules, presence or absence of
* daylight savings time, and even the daylight savings amount can all
* vary.
*
* This class uses a resource bundle named "zoneinfo". Zoneinfo is a
* table containing different kinds of resources. In several places,
* zones are referred to using integers. A zone's integer is a number
* from 0..n-1, where n is the number of zones, with the zones sorted
* in lexicographic order.
*
* 1. Zones. These have keys corresponding to the Olson IDs, e.g.,
* "Asia/Shanghai". Each resource describes the behavior of the given
* zone. Zones come in two different formats.
*
* a. Zone (table). A zone is a table resource contains several
* type of resources below:
*
* - typeOffsets:intvector (Required)
*
* Sets of UTC raw/dst offset pairs in seconds. Entries at
* 2n represents raw offset and 2n+1 represents dst offset
* paired with the raw offset at 2n. The very first pair represents
* the initial zone offset (before the first transition) always.
*
* - trans:intvector (Optional)
*
* List of transition times represented by 32bit seconds from the
* epoch (1970-01-01T00:00Z) in ascending order.
*
* - transPre32/transPost32:intvector (Optional)
*
* List of transition times before/after 32bit minimum seconds.
* Each time is represented by a pair of 32bit integer.
*
* - typeMap:bin (Optional)
*
* Array of bytes representing the mapping between each transition
* time (transPre32/trans/transPost32) and its corresponding offset
* data (typeOffsets).
*
* - finalRule:string (Optional)
*
* If a recurrent transition rule is applicable to a zone forever
* after the final transition time, finalRule represents the rule
* in Rules data.
*
* - finalRaw:int (Optional)
*
* When finalRule is available, finalRaw is required and specifies
* the raw (base) offset of the rule.
*
* - finalYear:int (Optional)
*
* When finalRule is available, finalYear is required and specifies
* the start year of the rule.
*
* - links:intvector (Optional)
*
* When this zone data is shared with other zones, links specifies
* all zones including the zone itself. Each zone is referenced by
* integer index.
*
* b. Link (int, length 1). A link zone is an int resource. The
* integer is the zone number of the target zone. The key of this
* resource is an alternate name for the target zone. This data
* is corresponding to Link data in the tz database.
*
*
* 2. Rules. These have keys corresponding to the Olson rule IDs,
* with an underscore prepended, e.g., "_EU". Each resource describes
* the behavior of the given rule using an intvector, containing the
* onset list, the cessation list, and the DST savings. The onset and
* cessation lists consist of the month, dowim, dow, time, and time
* mode. The end result is that the 11 integers describing the rule
* can be passed directly into the SimpleTimeZone 13-argument
* constructor (the other two arguments will be the raw offset, taken
* from the complex zone element 5, and the ID string, which is not
* used), with the times and the DST savings multiplied by 1000 to
* scale from seconds to milliseconds.
*
* 3. Regions. An array specifies mapping between zones and regions.
* Each item is either a 2-letter ISO country code or "001"
* (UN M.49 - World). This data is generated from "zone.tab"
* in the tz database.
*/
public class OlsonTimeZone extends BasicTimeZone {
// Generated by serialver from JDK 1.4.1_01
static final long serialVersionUID = -6281977362477515376L;
/* (non-Javadoc)
* @see com.ibm.icu.util.TimeZone#getOffset(int, int, int, int, int, int)
*/
@Override
public int getOffset(int era, int year, int month, int day, int dayOfWeek, int milliseconds) {
if (month < Calendar.JANUARY || month > Calendar.DECEMBER) {
throw new IllegalArgumentException("Month is not in the legal range: " +month);
} else {
return getOffset(era, year, month, day, dayOfWeek, milliseconds, Grego.monthLength(year, month));
}
}
/**
* TimeZone API.
*/
public int getOffset(int era, int year, int month,int dom, int dow, int millis, int monthLength){
if ((era != GregorianCalendar.AD && era != GregorianCalendar.BC)
|| month < Calendar.JANUARY
|| month > Calendar.DECEMBER
|| dom < 1
|| dom > monthLength
|| dow < Calendar.SUNDAY
|| dow > Calendar.SATURDAY
|| millis < 0
|| millis >= Grego.MILLIS_PER_DAY
|| monthLength < 28
|| monthLength > 31) {
throw new IllegalArgumentException();
}
if (era == GregorianCalendar.BC) {
year = -year;
}
if (finalZone != null && year >= finalStartYear) {
return finalZone.getOffset(era, year, month, dom, dow, millis);
}
// Compute local epoch millis from input fields
long time = Grego.fieldsToDay(year, month, dom) * Grego.MILLIS_PER_DAY + millis;
int[] offsets = new int[2];
getHistoricalOffset(time, true, LOCAL_DST, LOCAL_STD, offsets);
return offsets[0] + offsets[1];
}
/* (non-Javadoc)
* @see com.ibm.icu.util.TimeZone#setRawOffset(int)
*/
@Override
public void setRawOffset(int offsetMillis) {
if (isFrozen()) {
throw new UnsupportedOperationException("Attempt to modify a frozen OlsonTimeZone instance.");
}
if (getRawOffset() == offsetMillis) {
return;
}
long current = System.currentTimeMillis();
if (current < finalStartMillis) {
SimpleTimeZone stz = new SimpleTimeZone(offsetMillis, getID());
boolean bDst = useDaylightTime();
if (bDst) {
TimeZoneRule[] currentRules = getSimpleTimeZoneRulesNear(current);
if (currentRules.length != 3) {
// DST was observed at the beginning of this year, so useDaylightTime
// returned true. getSimpleTimeZoneRulesNear requires at least one
// future transition for making a pair of rules. This implementation
// rolls back the time before the latest offset transition.
TimeZoneTransition tzt = getPreviousTransition(current, false);
if (tzt != null) {
currentRules = getSimpleTimeZoneRulesNear(tzt.getTime() - 1);
}
}
if (currentRules.length == 3
&& (currentRules[1] instanceof AnnualTimeZoneRule)
&& (currentRules[2] instanceof AnnualTimeZoneRule)) {
// A pair of AnnualTimeZoneRule
AnnualTimeZoneRule r1 = (AnnualTimeZoneRule)currentRules[1];
AnnualTimeZoneRule r2 = (AnnualTimeZoneRule)currentRules[2];
DateTimeRule start, end;
int offset1 = r1.getRawOffset() + r1.getDSTSavings();
int offset2 = r2.getRawOffset() + r2.getDSTSavings();
int sav;
if (offset1 > offset2) {
start = r1.getRule();
end = r2.getRule();
sav = offset1 - offset2;
} else {
start = r2.getRule();
end = r1.getRule();
sav = offset2 - offset1;
}
// getSimpleTimeZoneRulesNear always return rules using DOW / WALL_TIME
stz.setStartRule(start.getRuleMonth(), start.getRuleWeekInMonth(), start.getRuleDayOfWeek(),
start.getRuleMillisInDay());
stz.setEndRule(end.getRuleMonth(), end.getRuleWeekInMonth(), end.getRuleDayOfWeek(),
end.getRuleMillisInDay());
// set DST saving amount and start year
stz.setDSTSavings(sav);
} else {
// This could only happen if last rule is DST
// and the rule used forever. For example, Asia/Dhaka
// in tzdata2009i stays in DST forever.
// Hack - set DST starting at midnight on Jan 1st,
// ending 23:59:59.999 on Dec 31st
stz.setStartRule(0, 1, 0);
stz.setEndRule(11, 31, Grego.MILLIS_PER_DAY - 1);
}
}
int[] fields = Grego.timeToFields(current, null);
finalStartYear = fields[0];
finalStartMillis = Grego.fieldsToDay(fields[0], 0, 1);
if (bDst) {
// we probably do not need to set start year of final rule
// to finalzone itself, but we always do this for now.
stz.setStartYear(finalStartYear);
}
finalZone = stz;
} else {
finalZone.setRawOffset(offsetMillis);
}
transitionRulesInitialized = false;
}
@Override
public Object clone() {
if (isFrozen()) {
return this;
}
return cloneAsThawed();
}
/**
* TimeZone API.
*/
@Override
public void getOffset(long date, boolean local, int[] offsets) {
if (finalZone != null && date >= finalStartMillis) {
finalZone.getOffset(date, local, offsets);
} else {
getHistoricalOffset(date, local,
LOCAL_FORMER, LOCAL_LATTER, offsets);
}
}
/**
* {@inheritDoc}
*/
@Override
public void getOffsetFromLocal(long date,
LocalOption nonExistingTimeOpt, LocalOption duplicatedTimeOpt, int[] offsets) {
if (finalZone != null && date >= finalStartMillis) {
finalZone.getOffsetFromLocal(date, nonExistingTimeOpt, duplicatedTimeOpt, offsets);
} else {
getHistoricalOffset(date, true, getLocalOptionValue(nonExistingTimeOpt), getLocalOptionValue(duplicatedTimeOpt), offsets);
}
}
/* (non-Javadoc)
* @see com.ibm.icu.util.TimeZone#getRawOffset()
*/
@Override
public int getRawOffset() {
int[] ret = new int[2];
getOffset(System.currentTimeMillis(), false, ret);
return ret[0];
}
/* (non-Javadoc)
* @see com.ibm.icu.util.TimeZone#useDaylightTime()
*/
@Override
public boolean useDaylightTime() {
// If DST was observed in 1942 (for example) but has never been
// observed from 1943 to the present, most clients will expect
// this method to return FALSE. This method determines whether
// DST is in use in the current year (at any point in the year)
// and returns TRUE if so.
long current = System.currentTimeMillis();
if (finalZone != null && current >= finalStartMillis) {
return (finalZone != null && finalZone.useDaylightTime());
}
int[] fields = Grego.timeToFields(current, null);
// Find start of this year, and start of next year
long start = Grego.fieldsToDay(fields[0], 0, 1) * SECONDS_PER_DAY;
long limit = Grego.fieldsToDay(fields[0] + 1, 0, 1) * SECONDS_PER_DAY;
// Return TRUE if DST is observed at any time during the current
// year.
for (int i = 0; i < transitionCount; ++i) {
if (transitionTimes64[i] >= limit) {
break;
}
if ((transitionTimes64[i] >= start && dstOffsetAt(i) != 0)
|| (transitionTimes64[i] > start && i > 0 && dstOffsetAt(i - 1) != 0)) {
return true;
}
}
return false;
}
/* (non-Javadoc)
* @see com.ibm.icu.util.TimeZone#observesDaylightTime()
*/
@Override
public boolean observesDaylightTime() {
long current = System.currentTimeMillis();
if (finalZone != null) {
if (finalZone.useDaylightTime()) {
return true;
} else if (current >= finalStartMillis) {
return false;
}
}
// Return TRUE if DST is observed at any future time
long currentSec = Grego.floorDivide(current, Grego.MILLIS_PER_SECOND);
int trsIdx = transitionCount - 1;
if (dstOffsetAt(trsIdx) != 0) {
return true;
}
while (trsIdx >= 0) {
if (transitionTimes64[trsIdx] <= currentSec) {
break;
}
if (dstOffsetAt(trsIdx - 1) != 0) {
return true;
}
trsIdx--;
}
return false;
}
/**
* TimeZone API
* Returns the amount of time to be added to local standard time
* to get local wall clock time.
*/
@Override
public int getDSTSavings() {
if (finalZone != null){
return finalZone.getDSTSavings();
}
return super.getDSTSavings();
}
/* (non-Javadoc)
* @see com.ibm.icu.util.TimeZone#inDaylightTime(java.util.Date)
*/
@Override
public boolean inDaylightTime(Date date) {
int[] temp = new int[2];
getOffset(date.getTime(), false, temp);
return temp[1] != 0;
}
/* (non-Javadoc)
* @see com.ibm.icu.util.TimeZone#hasSameRules(com.ibm.icu.util.TimeZone)
*/
@Override
public boolean hasSameRules(TimeZone other) {
if (this == other) {
return true;
}
// The super class implementation only check raw offset and
// use of daylight saving time.
if (!super.hasSameRules(other)) {
return false;
}
if (!(other instanceof OlsonTimeZone)) {
// We cannot reasonably compare rules in different types
return false;
}
// Check final zone
OlsonTimeZone o = (OlsonTimeZone)other;
if (finalZone == null) {
if (o.finalZone != null) {
return false;
}
} else {
if (o.finalZone == null
|| finalStartYear != o.finalStartYear
|| !(finalZone.hasSameRules(o.finalZone))) {
return false;
}
}
// Check transitions
// Note: The code below actually fails to compare two equivalent rules in
// different representation properly.
if (transitionCount != o.transitionCount ||
!Arrays.equals(transitionTimes64, o.transitionTimes64) ||
typeCount != o.typeCount ||
!Arrays.equals(typeMapData, o.typeMapData) ||
!Arrays.equals(typeOffsets, o.typeOffsets)){
return false;
}
return true;
}
/**
* Returns the canonical ID of this system time zone
*/
public String getCanonicalID() {
if (canonicalID == null) {
synchronized(this) {
if (canonicalID == null) {
canonicalID = getCanonicalID(getID());
assert(canonicalID != null);
if (canonicalID == null) {
// This should never happen...
canonicalID = getID();
}
}
}
}
return canonicalID;
}
/**
* Construct a GMT+0 zone with no transitions. This is done when a
* constructor fails so the resultant object is well-behaved.
*/
private void constructEmpty(){
transitionCount = 0;
transitionTimes64 = null;
typeMapData = null;
typeCount = 1;
typeOffsets = new int[]{0,0};
finalZone = null;
finalStartYear = Integer.MAX_VALUE;
finalStartMillis = Double.MAX_VALUE;
transitionRulesInitialized = false;
}
/**
* Construct from a resource bundle
* @param top the top-level zoneinfo resource bundle. This is used
* to lookup the rule that `res' may refer to, if there is one.
* @param res the resource bundle of the zone to be constructed
* @param id time zone ID
*/
public OlsonTimeZone(UResourceBundle top, UResourceBundle res, String id){
super(id);
construct(top, res, id);
}
private void construct(UResourceBundle top, UResourceBundle res, String id){
if ((top == null || res == null)) {
throw new IllegalArgumentException();
}
if(DEBUG) System.out.println("OlsonTimeZone(" + res.getKey() +")");
UResourceBundle r;
int[] transPre32, trans32, transPost32;
transPre32 = trans32 = transPost32 = null;
transitionCount = 0;
// Pre-32bit second transitions
try {
r = res.get("transPre32");
transPre32 = r.getIntVector();
if (transPre32.length % 2 != 0) {
// elements in the pre-32bit must be an even number
throw new IllegalArgumentException("Invalid Format");
}
transitionCount += transPre32.length / 2;
} catch (MissingResourceException e) {
// Pre-32bit transition data is optional
}
// 32bit second transitions
try {
r = res.get("trans");
trans32 = r.getIntVector();
transitionCount += trans32.length;
} catch (MissingResourceException e) {
// 32bit transition data is optional
}
// Post-32bit second transitions
try {
r = res.get("transPost32");
transPost32 = r.getIntVector();
if (transPost32.length % 2 != 0) {
// elements in the post-32bit must be an even number
throw new IllegalArgumentException("Invalid Format");
}
transitionCount += transPost32.length / 2;
} catch (MissingResourceException e) {
// Post-32bit transition data is optional
}
if (transitionCount > 0) {
transitionTimes64 = new long[transitionCount];
int idx = 0;
if (transPre32 != null) {
for (int i = 0; i < transPre32.length / 2; i++, idx++) {
transitionTimes64[idx] =
((transPre32[i * 2]) & 0x00000000FFFFFFFFL) << 32
| ((transPre32[i * 2 + 1]) & 0x00000000FFFFFFFFL);
}
}
if (trans32 != null) {
for (int i = 0; i < trans32.length; i++, idx++) {
transitionTimes64[idx] = trans32[i];
}
}
if (transPost32 != null) {
for (int i = 0; i < transPost32.length / 2; i++, idx++) {
transitionTimes64[idx] =
((transPost32[i * 2]) & 0x00000000FFFFFFFFL) << 32
| ((transPost32[i * 2 + 1]) & 0x00000000FFFFFFFFL);
}
}
} else {
transitionTimes64 = null;
}
// Type offsets list must be of even size, with size >= 2
r = res.get("typeOffsets");
typeOffsets = r.getIntVector();
if ((typeOffsets.length < 2 || typeOffsets.length > 0x7FFE || typeOffsets.length % 2 != 0)) {
throw new IllegalArgumentException("Invalid Format");
}
typeCount = typeOffsets.length / 2;
// Type map data must be of the same size as the transition count
if (transitionCount > 0) {
r = res.get("typeMap");
typeMapData = r.getBinary(null);
if (typeMapData == null || typeMapData.length != transitionCount) {
throw new IllegalArgumentException("Invalid Format");
}
} else {
typeMapData = null;
}
// Process final rule and data, if any
finalZone = null;
finalStartYear = Integer.MAX_VALUE;
finalStartMillis = Double.MAX_VALUE;
String ruleID = null;
try {
ruleID = res.getString("finalRule");
r = res.get("finalRaw");
int ruleRaw = r.getInt() * Grego.MILLIS_PER_SECOND;
r = loadRule(top, ruleID);
int[] ruleData = r.getIntVector();
if (ruleData == null || ruleData.length != 11) {
throw new IllegalArgumentException("Invalid Format");
}
finalZone = new SimpleTimeZone(ruleRaw, id,
ruleData[0], ruleData[1], ruleData[2],
ruleData[3] * Grego.MILLIS_PER_SECOND,
ruleData[4],
ruleData[5], ruleData[6], ruleData[7],
ruleData[8] * Grego.MILLIS_PER_SECOND,
ruleData[9],
ruleData[10] * Grego.MILLIS_PER_SECOND);
r = res.get("finalYear");
finalStartYear = r.getInt();
// Note: Setting finalStartYear to the finalZone is problematic. When a date is around
// year boundary, SimpleTimeZone may return false result when DST is observed at the
// beginning of year. We could apply safe margin (day or two), but when one of recurrent
// rules falls around year boundary, it could return false result. Without setting the
// start year, finalZone works fine around the year boundary of the start year.
// finalZone.setStartYear(finalStartYear);
// Compute the millis for Jan 1, 0:00 GMT of the finalYear
// Note: finalStartMillis is used for detecting either if
// historic transition data or finalZone to be used. In an
// extreme edge case - for example, two transitions fall into
// small windows of time around the year boundary, this may
// result incorrect offset computation. But I think it will
// never happen practically. Yoshito - Feb 20, 2010
finalStartMillis = Grego.fieldsToDay(finalStartYear, 0, 1) * Grego.MILLIS_PER_DAY;
} catch (MissingResourceException e) {
if (ruleID != null) {
// ruleID is found, but missing other data required for
// creating finalZone
throw new IllegalArgumentException("Invalid Format");
}
}
}
// This constructor is used for testing purpose only
public OlsonTimeZone(String id){
super(id);
UResourceBundle top = UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME,
ZONEINFORES, ICUResourceBundle.ICU_DATA_CLASS_LOADER);
UResourceBundle res = ZoneMeta.openOlsonResource(top, id);
construct(top, res, id);
}
/* (non-Javadoc)
* @see com.ibm.icu.util.TimeZone#setID(java.lang.String)
*/
@Override
public void setID(String id){
if (isFrozen()) {
throw new UnsupportedOperationException("Attempt to modify a frozen OlsonTimeZone instance.");
}
// Before updating the ID, preserve the original ID's canonical ID.
if (canonicalID == null) {
canonicalID = getCanonicalID(getID());
assert(canonicalID != null);
if (canonicalID == null) {
// This should never happen...
canonicalID = getID();
}
}
if (finalZone != null){
finalZone.setID(id);
}
super.setID(id);
transitionRulesInitialized = false;
}
// Maximum absolute offset in seconds = 1 day.
// getHistoricalOffset uses this constant as safety margin of
// quick zone transition checking.
private static final int MAX_OFFSET_SECONDS = 86400; // 60 * 60 * 24;
private void getHistoricalOffset(long date, boolean local,
int NonExistingTimeOpt, int DuplicatedTimeOpt, int[] offsets) {
if (transitionCount != 0) {
long sec = Grego.floorDivide(date, Grego.MILLIS_PER_SECOND);
if (!local && sec < transitionTimes64[0]) {
// Before the first transition time
offsets[0] = initialRawOffset() * Grego.MILLIS_PER_SECOND;
offsets[1] = initialDstOffset() * Grego.MILLIS_PER_SECOND;
} else {
// Linear search from the end is the fastest approach, since
// most lookups will happen at/near the end.
int transIdx;
for (transIdx = transitionCount - 1; transIdx >= 0; transIdx--) {
long transition = transitionTimes64[transIdx];
if (local && (sec >= (transition - MAX_OFFSET_SECONDS))) {
int offsetBefore = zoneOffsetAt(transIdx - 1);
boolean dstBefore = dstOffsetAt(transIdx - 1) != 0;
int offsetAfter = zoneOffsetAt(transIdx);
boolean dstAfter = dstOffsetAt(transIdx) != 0;
boolean dstToStd = dstBefore && !dstAfter;
boolean stdToDst = !dstBefore && dstAfter;
if (offsetAfter - offsetBefore >= 0) {
// Positive transition, which makes a non-existing local time range
if (((NonExistingTimeOpt & STD_DST_MASK) == LOCAL_STD && dstToStd)
|| ((NonExistingTimeOpt & STD_DST_MASK) == LOCAL_DST && stdToDst)) {
transition += offsetBefore;
} else if (((NonExistingTimeOpt & STD_DST_MASK) == LOCAL_STD && stdToDst)
|| ((NonExistingTimeOpt & STD_DST_MASK) == LOCAL_DST && dstToStd)) {
transition += offsetAfter;
} else if ((NonExistingTimeOpt & FORMER_LATTER_MASK) == LOCAL_LATTER) {
transition += offsetBefore;
} else {
// Interprets the time with rule before the transition,
// default for non-existing time range
transition += offsetAfter;
}
} else {
// Negative transition, which makes a duplicated local time range
if (((DuplicatedTimeOpt & STD_DST_MASK) == LOCAL_STD && dstToStd)
|| ((DuplicatedTimeOpt & STD_DST_MASK) == LOCAL_DST && stdToDst)) {
transition += offsetAfter;
} else if (((DuplicatedTimeOpt & STD_DST_MASK) == LOCAL_STD && stdToDst)
|| ((DuplicatedTimeOpt & STD_DST_MASK) == LOCAL_DST && dstToStd)) {
transition += offsetBefore;
} else if ((DuplicatedTimeOpt & FORMER_LATTER_MASK) == LOCAL_FORMER) {
transition += offsetBefore;
} else {
// Interprets the time with rule after the transition,
// default for duplicated local time range
transition += offsetAfter;
}
}
}
if (sec >= transition) {
break;
}
}
// transIdx could be -1 when local=true
offsets[0] = rawOffsetAt(transIdx) * Grego.MILLIS_PER_SECOND;
offsets[1] = dstOffsetAt(transIdx) * Grego.MILLIS_PER_SECOND;
}
} else {
// No transitions, single pair of offsets only
offsets[0] = initialRawOffset() * Grego.MILLIS_PER_SECOND;
offsets[1] = initialDstOffset() * Grego.MILLIS_PER_SECOND;
}
}
private int getInt(byte val){
return val & 0xFF;
}
/*
* Following 3 methods return an offset at the given transition time index.
* When the index is negative, return the initial offset.
*/
private int zoneOffsetAt(int transIdx) {
int typeIdx = transIdx >= 0 ? getInt(typeMapData[transIdx]) * 2 : 0;
return typeOffsets[typeIdx] + typeOffsets[typeIdx + 1];
}
private int rawOffsetAt(int transIdx) {
int typeIdx = transIdx >= 0 ? getInt(typeMapData[transIdx]) * 2 : 0;
return typeOffsets[typeIdx];
}
private int dstOffsetAt(int transIdx) {
int typeIdx = transIdx >= 0 ? getInt(typeMapData[transIdx]) * 2 : 0;
return typeOffsets[typeIdx + 1];
}
private int initialRawOffset() {
return typeOffsets[0];
}
private int initialDstOffset() {
return typeOffsets[1];
}
// temp
@Override
public String toString() {
StringBuilder buf = new StringBuilder();
buf.append(super.toString());
buf.append('[');
buf.append("transitionCount=" + transitionCount);
buf.append(",typeCount=" + typeCount);
buf.append(",transitionTimes=");
if (transitionTimes64 != null) {
buf.append('[');
for (int i = 0; i < transitionTimes64.length; ++i) {
if (i > 0) {
buf.append(',');
}
buf.append(Long.toString(transitionTimes64[i]));
}
buf.append(']');
} else {
buf.append("null");
}
buf.append(",typeOffsets=");
if (typeOffsets != null) {
buf.append('[');
for (int i = 0; i < typeOffsets.length; ++i) {
if (i > 0) {
buf.append(',');
}
buf.append(Integer.toString(typeOffsets[i]));
}
buf.append(']');
} else {
buf.append("null");
}
buf.append(",typeMapData=");
if (typeMapData != null) {
buf.append('[');
for (int i = 0; i < typeMapData.length; ++i) {
if (i > 0) {
buf.append(',');
}
buf.append(Byte.toString(typeMapData[i]));
}
} else {
buf.append("null");
}
buf.append(",finalStartYear=" + finalStartYear);
buf.append(",finalStartMillis=" + finalStartMillis);
buf.append(",finalZone=" + finalZone);
buf.append(']');
return buf.toString();
}
/**
* Number of transitions, 0..~370
*/
private int transitionCount;
/**
* Number of types, 1..255
*/
private int typeCount;
/**
* Time of each transition in seconds from 1970 epoch.
*/
private long[] transitionTimes64;
/**
* Offset from GMT in seconds for each type.
* Length is equal to typeCount
*/
private int[] typeOffsets;
/**
* Type description data, consisting of transitionCount uint8_t
* type indices (from 0..typeCount-1).
* Length is equal to transitionCount
*/
private byte[] typeMapData;
/**
* For year >= finalStartYear, the finalZone will be used.
*/
private int finalStartYear = Integer.MAX_VALUE;
/**
* For date >= finalStartMillis, the finalZone will be used.
*/
private double finalStartMillis = Double.MAX_VALUE;
/**
* A SimpleTimeZone that governs the behavior for years >= finalYear.
* If and only if finalYear == INT32_MAX then finalZone == 0.
*/
private SimpleTimeZone finalZone = null; // owned, may be NULL
/**
* The canonical ID of this zone. Initialized when {@link #getCanonicalID()}
* is invoked first time, or {@link #setID(String)} is called.
*/
private volatile String canonicalID = null;
private static final String ZONEINFORES = "zoneinfo64";
private static final boolean DEBUG = ICUDebug.enabled("olson");
private static final int SECONDS_PER_DAY = 24*60*60;
private static UResourceBundle loadRule(UResourceBundle top, String ruleid) {
UResourceBundle r = top.get("Rules");
r = r.get(ruleid);
return r;
}
@Override
public boolean equals(Object obj){
if (!super.equals(obj)) return false; // super does class check
OlsonTimeZone z = (OlsonTimeZone) obj;
return (Utility.arrayEquals(typeMapData, z.typeMapData) ||
// If the pointers are not equal, the zones may still
// be equal if their rules and transitions are equal
(finalStartYear == z.finalStartYear &&
// Don't compare finalMillis; if finalYear is ==, so is finalMillis
((finalZone == null && z.finalZone == null) ||
(finalZone != null && z.finalZone != null &&
finalZone.equals(z.finalZone)) &&
transitionCount == z.transitionCount &&
typeCount == z.typeCount &&
Utility.arrayEquals(transitionTimes64, z.transitionTimes64) &&
Utility.arrayEquals(typeOffsets, z.typeOffsets) &&
Utility.arrayEquals(typeMapData, z.typeMapData)
)));
}
@Override
public int hashCode(){
int ret = (int) (finalStartYear ^ (finalStartYear>>>4) +
transitionCount ^ (transitionCount>>>6) +
typeCount ^ (typeCount>>>8) +
Double.doubleToLongBits(finalStartMillis)+
(finalZone == null ? 0 : finalZone.hashCode()) +
super.hashCode());
if (transitionTimes64 != null) {
for(int i=0; i>>8);
}
}
for(int i=0; i>>8);
}
if (typeMapData != null) {
for(int i=0; i= firstFinalTZTransition.getTime()) {
if (finalZone.useDaylightTime()) {
//return finalZone.getNextTransition(base, inclusive);
return finalZoneWithStartYear.getNextTransition(base, inclusive);
} else {
// No more transitions
return null;
}
}
}
if (historicRules != null) {
// Find a historical transition
int ttidx = transitionCount - 1;
for (; ttidx >= firstTZTransitionIdx; ttidx--) {
long t = transitionTimes64[ttidx] * Grego.MILLIS_PER_SECOND;
if (base > t || (!inclusive && base == t)) {
break;
}
}
if (ttidx == transitionCount - 1) {
return firstFinalTZTransition;
} else if (ttidx < firstTZTransitionIdx) {
return firstTZTransition;
} else {
// Create a TimeZoneTransition
TimeZoneRule to = historicRules[getInt(typeMapData[ttidx + 1])];
TimeZoneRule from = historicRules[getInt(typeMapData[ttidx])];
long startTime = transitionTimes64[ttidx+1] * Grego.MILLIS_PER_SECOND;
// The transitions loaded from zoneinfo.res may contain non-transition data
if (from.getName().equals(to.getName()) && from.getRawOffset() == to.getRawOffset()
&& from.getDSTSavings() == to.getDSTSavings()) {
return getNextTransition(startTime, false);
}
return new TimeZoneTransition(startTime, from, to);
}
}
return null;
}
/* (non-Javadoc)
* @see com.ibm.icu.util.BasicTimeZone#getPreviousTransition(long, boolean)
*/
@Override
public TimeZoneTransition getPreviousTransition(long base, boolean inclusive) {
initTransitionRules();
if (finalZone != null) {
if (inclusive && base == firstFinalTZTransition.getTime()) {
return firstFinalTZTransition;
} else if (base > firstFinalTZTransition.getTime()) {
if (finalZone.useDaylightTime()) {
//return finalZone.getPreviousTransition(base, inclusive);
return finalZoneWithStartYear.getPreviousTransition(base, inclusive);
} else {
return firstFinalTZTransition;
}
}
}
if (historicRules != null) {
// Find a historical transition
int ttidx = transitionCount - 1;
for (; ttidx >= firstTZTransitionIdx; ttidx--) {
long t = transitionTimes64[ttidx] * Grego.MILLIS_PER_SECOND;
if (base > t || (inclusive && base == t)) {
break;
}
}
if (ttidx < firstTZTransitionIdx) {
// No more transitions
return null;
} else if (ttidx == firstTZTransitionIdx) {
return firstTZTransition;
} else {
// Create a TimeZoneTransition
TimeZoneRule to = historicRules[getInt(typeMapData[ttidx])];
TimeZoneRule from = historicRules[getInt(typeMapData[ttidx-1])];
long startTime = transitionTimes64[ttidx] * Grego.MILLIS_PER_SECOND;
// The transitions loaded from zoneinfo.res may contain non-transition data
if (from.getName().equals(to.getName()) && from.getRawOffset() == to.getRawOffset()
&& from.getDSTSavings() == to.getDSTSavings()) {
return getPreviousTransition(startTime, false);
}
return new TimeZoneTransition(startTime, from, to);
}
}
return null;
}
/* (non-Javadoc)
* @see com.ibm.icu.util.BasicTimeZone#getTimeZoneRules()
*/
@Override
public TimeZoneRule[] getTimeZoneRules() {
initTransitionRules();
int size = 1;
if (historicRules != null) {
// historicRules may contain null entries when original zoneinfo data
// includes non transition data.
for (int i = 0; i < historicRules.length; i++) {
if (historicRules[i] != null) {
size++;
}
}
}
if (finalZone != null) {
if (finalZone.useDaylightTime()) {
size += 2;
} else {
size++;
}
}
TimeZoneRule[] rules = new TimeZoneRule[size];
int idx = 0;
rules[idx++] = initialRule;
if (historicRules != null) {
for (int i = 0; i < historicRules.length; i++) {
if (historicRules[i] != null) {
rules[idx++] = historicRules[i];
}
}
}
if (finalZone != null) {
if (finalZone.useDaylightTime()) {
TimeZoneRule[] stzr = finalZoneWithStartYear.getTimeZoneRules();
// Adding only transition rules
rules[idx++] = stzr[1];
rules[idx++] = stzr[2];
} else {
// Create a TimeArrayTimeZoneRule at finalMillis
rules[idx++] = new TimeArrayTimeZoneRule(getID() + "(STD)", finalZone.getRawOffset(), 0,
new long[] {(long)finalStartMillis}, DateTimeRule.UTC_TIME);
}
}
return rules;
}
private transient InitialTimeZoneRule initialRule;
private transient TimeZoneTransition firstTZTransition;
private transient int firstTZTransitionIdx;
private transient TimeZoneTransition firstFinalTZTransition;
private transient TimeArrayTimeZoneRule[] historicRules;
private transient SimpleTimeZone finalZoneWithStartYear; // hack
private transient boolean transitionRulesInitialized;
private synchronized void initTransitionRules() {
if (transitionRulesInitialized) {
return;
}
initialRule = null;
firstTZTransition = null;
firstFinalTZTransition = null;
historicRules = null;
firstTZTransitionIdx = 0;
finalZoneWithStartYear = null;
String stdName = getID() + "(STD)";
String dstName = getID() + "(DST)";
int raw, dst;
// Create initial rule
raw = initialRawOffset() * Grego.MILLIS_PER_SECOND;
dst = initialDstOffset() * Grego.MILLIS_PER_SECOND;
initialRule = new InitialTimeZoneRule((dst == 0 ? stdName : dstName), raw, dst);
if (transitionCount > 0) {
int transitionIdx, typeIdx;
// We probably no longer need to check the first "real" transition
// here, because the new tzcode remove such transitions already.
// For now, keeping this code for just in case. Feb 19, 2010 Yoshito
for (transitionIdx = 0; transitionIdx < transitionCount; transitionIdx++) {
if (getInt(typeMapData[transitionIdx]) != 0) { // type 0 is the initial type
break;
}
firstTZTransitionIdx++;
}
if (transitionIdx == transitionCount) {
// Actually no transitions...
} else {
// Build historic rule array
long[] times = new long[transitionCount];
for (typeIdx = 0; typeIdx < typeCount; typeIdx++) {
// Gather all start times for each pair of offsets
int nTimes = 0;
for (transitionIdx = firstTZTransitionIdx; transitionIdx < transitionCount; transitionIdx++) {
if (typeIdx == getInt(typeMapData[transitionIdx])) {
long tt = transitionTimes64[transitionIdx] * Grego.MILLIS_PER_SECOND;
if (tt < finalStartMillis) {
// Exclude transitions after finalMillis
times[nTimes++] = tt;
}
}
}
if (nTimes > 0) {
long[] startTimes = new long[nTimes];
System.arraycopy(times, 0, startTimes, 0, nTimes);
// Create a TimeArrayTimeZoneRule
raw = typeOffsets[typeIdx*2]*Grego.MILLIS_PER_SECOND;
dst = typeOffsets[typeIdx*2 + 1]*Grego.MILLIS_PER_SECOND;
if (historicRules == null) {
historicRules = new TimeArrayTimeZoneRule[typeCount];
}
historicRules[typeIdx] = new TimeArrayTimeZoneRule((dst == 0 ? stdName : dstName),
raw, dst, startTimes, DateTimeRule.UTC_TIME);
}
}
// Create initial transition
typeIdx = getInt(typeMapData[firstTZTransitionIdx]);
firstTZTransition = new TimeZoneTransition(transitionTimes64[firstTZTransitionIdx] * Grego.MILLIS_PER_SECOND,
initialRule, historicRules[typeIdx]);
}
}
if (finalZone != null) {
// Get the first occurrence of final rule starts
long startTime = (long)finalStartMillis;
TimeZoneRule firstFinalRule;
if (finalZone.useDaylightTime()) {
/*
* Note: When an OlsonTimeZone is constructed, we should set the final year
* as the start year of finalZone. However, the boundary condition used for
* getting offset from finalZone has some problems. So setting the start year
* in the finalZone will cause a problem. For now, we do not set the valid
* start year when the construction time and create a clone and set the
* start year when extracting rules.
*/
finalZoneWithStartYear = (SimpleTimeZone)finalZone.clone();
finalZoneWithStartYear.setStartYear(finalStartYear);
TimeZoneTransition tzt = finalZoneWithStartYear.getNextTransition(startTime, false);
firstFinalRule = tzt.getTo();
startTime = tzt.getTime();
} else {
finalZoneWithStartYear = finalZone;
firstFinalRule = new TimeArrayTimeZoneRule(finalZone.getID(),
finalZone.getRawOffset(), 0, new long[] {startTime}, DateTimeRule.UTC_TIME);
}
TimeZoneRule prevRule = null;
if (transitionCount > 0) {
prevRule = historicRules[getInt(typeMapData[transitionCount - 1])];
}
if (prevRule == null) {
// No historic transitions, but only finalZone available
prevRule = initialRule;
}
firstFinalTZTransition = new TimeZoneTransition(startTime, prevRule, firstFinalRule);
}
transitionRulesInitialized = true;
}
// Note: This class does not support back level serialization compatibility
// very well. ICU 4.4 introduced the 64bit transition data. It is probably
// possible to implement this class to make old version of ICU to deserialize
// object stream serialized by ICU 4.4+. However, such implementation will
// introduce unnecessary complexity other than serialization support.
// I decided to provide minimum level of backward compatibility, which
// only support ICU 4.4+ to create an instance of OlsonTimeZone by reloading
// the zone rules from bundles. ICU 4.2 or older version of ICU cannot
// deserialize object stream created by ICU 4.4+. Yoshito -Feb 22, 2010
private static final int currentSerialVersion = 1;
private int serialVersionOnStream = currentSerialVersion;
private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException {
stream.defaultReadObject();
if (serialVersionOnStream < 1) {
// No version - 4.2 or older
// Just reloading the rule from bundle
boolean initialized = false;
String tzid = getID();
if (tzid != null) {
try {
UResourceBundle top = UResourceBundle.getBundleInstance(ICUData.ICU_BASE_NAME,
ZONEINFORES, ICUResourceBundle.ICU_DATA_CLASS_LOADER);
UResourceBundle res = ZoneMeta.openOlsonResource(top, tzid);
construct(top, res, tzid);
initialized = true;
} catch (Exception ignored) {
// throw away
}
}
if (!initialized) {
// final resort
constructEmpty();
}
}
// need to rebuild transition rules when requested
transitionRulesInitialized = false;
}
// Freezable stuffs
private transient volatile boolean isFrozen = false;
/* (non-Javadoc)
* @see com.ibm.icu.util.TimeZone#isFrozen()
*/
@Override
public boolean isFrozen() {
return isFrozen;
}
/* (non-Javadoc)
* @see com.ibm.icu.util.TimeZone#freeze()
*/
@Override
public TimeZone freeze() {
isFrozen = true;
return this;
}
/* (non-Javadoc)
* @see com.ibm.icu.util.TimeZone#cloneAsThawed()
*/
@Override
public TimeZone cloneAsThawed() {
OlsonTimeZone tz = (OlsonTimeZone)super.cloneAsThawed();
if (finalZone != null) {
tz.finalZone = (SimpleTimeZone) finalZone.clone();
}
// Following data are read-only and never changed.
// Therefore, shallow copies should be sufficient.
//
// transitionTimes64
// typeMapData
// typeOffsets
tz.isFrozen = false;
return tz;
}
}