| // © 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<transitionTimes64.length; i++){ |
| ret+=transitionTimes64[i]^(transitionTimes64[i]>>>8); |
| } |
| } |
| for(int i=0; i<typeOffsets.length; i++){ |
| ret+=typeOffsets[i]^(typeOffsets[i]>>>8); |
| } |
| if (typeMapData != null) { |
| for(int i=0; i<typeMapData.length; i++){ |
| ret+=typeMapData[i] & 0xFF; |
| } |
| } |
| return ret; |
| } |
| |
| // |
| // BasicTimeZone methods |
| // |
| |
| /* (non-Javadoc) |
| * @see com.ibm.icu.util.BasicTimeZone#getNextTransition(long, boolean) |
| */ |
| @Override |
| public TimeZoneTransition getNextTransition(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.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; |
| } |
| } |