diff --git a/NOTICE b/NOTICE index c56a5e4ea..eef39c165 100644 --- a/NOTICE +++ b/NOTICE @@ -1,2 +1,12 @@ +This product includes software developed by Alexey Andreev +(http://teavm.org). + + This product includes software developed by The Apache Software -Foundation (http://www.apache.org/). \ No newline at end of file +Foundation (http://www.apache.org/). + +============================================================================= += NOTICE file corresponding to section 4d of the Apache License Version 2.0 = +============================================================================= +This product includes software developed by +Joda.org (http://www.joda.org/). \ No newline at end of file diff --git a/teavm-classlib/src/main/java/org/teavm/classlib/impl/tz/CachedDateTimeZone.java b/teavm-classlib/src/main/java/org/teavm/classlib/impl/tz/CachedDateTimeZone.java new file mode 100644 index 000000000..01048976a --- /dev/null +++ b/teavm-classlib/src/main/java/org/teavm/classlib/impl/tz/CachedDateTimeZone.java @@ -0,0 +1,171 @@ +/* + * Copyright 2001-2012 Stephen Colebourne + * + * 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 org.teavm.classlib.impl.tz; + +import org.teavm.classlib.impl.Base46; + +/** + * Improves the performance of requesting time zone offsets and name keys by + * caching the results. Time zones that have simple rules or are fixed should + * not be cached, as it is unlikely to improve performance. + *

+ * CachedDateTimeZone is thread-safe and immutable. + * + * @author Brian S O'Neill + * @since 1.0 + */ +public class CachedDateTimeZone extends StorableDateTimeZone { + + private static final int cInfoCacheMask; + + static { + cInfoCacheMask = 511; + } + + /** + * Returns a new CachedDateTimeZone unless given zone is already cached. + */ + public static CachedDateTimeZone forZone(StorableDateTimeZone zone) { + if (zone instanceof CachedDateTimeZone) { + return (CachedDateTimeZone)zone; + } + return new CachedDateTimeZone(zone); + } + + /* + * Caching is performed by breaking timeline down into periods of 2^32 + * milliseconds, or about 49.7 days. A year has about 7.3 periods, usually + * with only 2 time zone offset periods. Most of the 49.7 day periods will + * have no transition, about one quarter have one transition, and very rare + * cases have multiple transitions. + */ + + private final StorableDateTimeZone iZone; + + private final transient Info[] iInfoCache = new Info[cInfoCacheMask + 1]; + + private CachedDateTimeZone(StorableDateTimeZone zone) { + super(zone.getID()); + iZone = zone; + } + + @Override + public void write(StringBuilder sb) { + Base46.encodeUnsigned(sb, CACHED); + iZone.write(sb); + } + + /** + * Returns the DateTimeZone being wrapped. + */ + public DateTimeZone getUncachedZone() { + return iZone; + } + + @Override + public int getOffset(long instant) { + return getInfo(instant).getOffset(instant); + } + + @Override + public int getStandardOffset(long instant) { + return getInfo(instant).getStandardOffset(instant); + } + + @Override + public boolean isFixed() { + return iZone.isFixed(); + } + + @Override + public long nextTransition(long instant) { + return iZone.nextTransition(instant); + } + + @Override + public long previousTransition(long instant) { + return iZone.previousTransition(instant); + } + + // Although accessed by multiple threads, this method doesn't need to be + // synchronized. + + private Info getInfo(long millis) { + int period = (int)(millis >> 32); + Info[] cache = iInfoCache; + int index = period & cInfoCacheMask; + Info info = cache[index]; + if (info == null || (int)((info.iPeriodStart >> 32)) != period) { + info = createInfo(millis); + cache[index] = info; + } + return info; + } + + private Info createInfo(long millis) { + long periodStart = millis & (0xffffffffL << 32); + Info info = new Info(iZone, periodStart); + + long end = periodStart | 0xffffffffL; + Info chain = info; + while (true) { + long next = iZone.nextTransition(periodStart); + if (next == periodStart || next > end) { + break; + } + periodStart = next; + chain = (chain.iNextInfo = new Info(iZone, periodStart)); + } + + return info; + } + + private final static class Info { + // For first Info in chain, iPeriodStart's lower 32 bits are clear. + public final long iPeriodStart; + public final DateTimeZone iZoneRef; + + Info iNextInfo; + + private int iOffset = Integer.MIN_VALUE; + private int iStandardOffset = Integer.MIN_VALUE; + + Info(DateTimeZone zone, long periodStart) { + iPeriodStart = periodStart; + iZoneRef = zone; + } + + public int getOffset(long millis) { + if (iNextInfo == null || millis < iNextInfo.iPeriodStart) { + if (iOffset == Integer.MIN_VALUE) { + iOffset = iZoneRef.getOffset(iPeriodStart); + } + return iOffset; + } + return iNextInfo.getOffset(millis); + } + + public int getStandardOffset(long millis) { + if (iNextInfo == null || millis < iNextInfo.iPeriodStart) { + if (iStandardOffset == Integer.MIN_VALUE) { + iStandardOffset = iZoneRef.getStandardOffset(iPeriodStart); + } + return iStandardOffset; + } + return iNextInfo.getStandardOffset(millis); + } + } +} diff --git a/teavm-classlib/src/main/java/org/teavm/classlib/impl/tz/DateTimeZone.java b/teavm-classlib/src/main/java/org/teavm/classlib/impl/tz/DateTimeZone.java new file mode 100644 index 000000000..09ae82a25 --- /dev/null +++ b/teavm-classlib/src/main/java/org/teavm/classlib/impl/tz/DateTimeZone.java @@ -0,0 +1,500 @@ +/* + * Copyright 2001-2014 Stephen Colebourne + * + * 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 org.teavm.classlib.impl.tz; + +/** + * DateTimeZone represents a time zone. + *

+ * A time zone is a system of rules to convert time from one geographic + * location to another. For example, Paris, France is one hour ahead of + * London, England. Thus when it is 10:00 in London, it is 11:00 in Paris. + *

+ * All time zone rules are expressed, for historical reasons, relative to + * Greenwich, London. Local time in Greenwich is referred to as Greenwich Mean + * Time (GMT). This is similar, but not precisely identical, to Universal + * Coordinated Time, or UTC. This library only uses the term UTC. + *

+ * Using this system, America/Los_Angeles is expressed as UTC-08:00, or UTC-07:00 + * in the summer. The offset -08:00 indicates that America/Los_Angeles time is + * obtained from UTC by adding -08:00, that is, by subtracting 8 hours. + *

+ * The offset differs in the summer because of daylight saving time, or DST. + * The following definitions of time are generally used: + *

+ *

+ * Unlike the Java TimeZone class, DateTimeZone is immutable. It also only + * supports long format time zone ids. Thus EST and ECT are not accepted. + * However, the factory that accepts a TimeZone will attempt to convert from + * the old short id to a suitable long id. + *

+ * There are four approaches to loading time-zone data, which are tried in this order: + *

    + *
  1. load the specific {@link Provider} specified by the system property + * {@code org.joda.time.DateTimeZone.Provider}. + *
  2. load {@link ZoneInfoProvider} using the data in the filing system folder + * pointed to by system property {@code org.joda.time.DateTimeZone.Folder}. + *
  3. load {@link ZoneInfoProvider} using the data in the classpath location + * {@code org/joda/time/tz/data}. + *
  4. load {@link UTCProvider} + *
+ *

+ * Unless you override the standard behaviour, the default if the third approach. + *

+ * DateTimeZone is thread-safe and immutable, and all subclasses must be as + * well. + * + * @author Brian S O'Neill + * @author Stephen Colebourne + * @since 1.0 + */ +public abstract class DateTimeZone { + static final long MILLIS_PER_HOUR = 3600_000; + + // Instance fields and methods + //-------------------------------------------------------------------- + + private final String iID; + + /** + * Constructor. + * + * @param id the id to use + * @throws IllegalArgumentException if the id is null + */ + protected DateTimeZone(String id) { + if (id == null) { + throw new IllegalArgumentException("Id must not be null"); + } + iID = id; + } + + // Principal methods + //-------------------------------------------------------------------- + + /** + * Gets the ID of this datetime zone. + * + * @return the ID of this datetime zone + */ + public final String getID() { + return iID; + } + + /** + * Gets the millisecond offset to add to UTC to get local time. + * + * @param instant milliseconds from 1970-01-01T00:00:00Z to get the offset for + * @return the millisecond offset to add to UTC to get local time + */ + public abstract int getOffset(long instant); + + /** + * Gets the standard millisecond offset to add to UTC to get local time, + * when standard time is in effect. + * + * @param instant milliseconds from 1970-01-01T00:00:00Z to get the offset for + * @return the millisecond offset to add to UTC to get local time + */ + public abstract int getStandardOffset(long instant); + + /** + * Checks whether, at a particular instant, the offset is standard or not. + *

+ * This method can be used to determine whether Summer Time (DST) applies. + * As a general rule, if the offset at the specified instant is standard, + * then either Winter time applies, or there is no Summer Time. If the + * instant is not standard, then Summer Time applies. + *

+ * The implementation of the method is simply whether {@link #getOffset(long)} + * equals {@link #getStandardOffset(long)} at the specified instant. + * + * @param instant milliseconds from 1970-01-01T00:00:00Z to get the offset for + * @return true if the offset at the given instant is the standard offset + * @since 1.5 + */ + public boolean isStandardOffset(long instant) { + return getOffset(instant) == getStandardOffset(instant); + } + + /** + * Gets the millisecond offset to subtract from local time to get UTC time. + * This offset can be used to undo adding the offset obtained by getOffset. + * + *

+     * millisLocal == millisUTC   + getOffset(millisUTC)
+     * millisUTC   == millisLocal - getOffsetFromLocal(millisLocal)
+     * 
+ * + * NOTE: After calculating millisLocal, some error may be introduced. At + * offset transitions (due to DST or other historical changes), ranges of + * local times may map to different UTC times. + *

+ * For overlaps (where the local time is ambiguous), this method returns the + * offset applicable before the gap. The effect of this is that any instant + * calculated using the offset from an overlap will be in "summer" time. + *

+ * For gaps, this method returns the offset applicable before the gap, ie "winter" offset. + * However, the effect of this is that any instant calculated using the offset + * from a gap will be after the gap, in "summer" time. + *

+ * For example, consider a zone with a gap from 01:00 to 01:59:
+ * Input: 00:00 (before gap) Output: Offset applicable before gap DateTime: 00:00
+ * Input: 00:30 (before gap) Output: Offset applicable before gap DateTime: 00:30
+ * Input: 01:00 (in gap) Output: Offset applicable before gap DateTime: 02:00
+ * Input: 01:30 (in gap) Output: Offset applicable before gap DateTime: 02:30
+ * Input: 02:00 (after gap) Output: Offset applicable after gap DateTime: 02:00
+ * Input: 02:30 (after gap) Output: Offset applicable after gap DateTime: 02:30
+ *

+ * NOTE: Prior to v2.0, the DST overlap behaviour was not defined and varied by hemisphere. + * Prior to v1.5, the DST gap behaviour was also not defined. + * In v2.4, the documentation was clarified again. + * + * @param instantLocal the millisecond instant, relative to this time zone, to get the offset for + * @return the millisecond offset to subtract from local time to get UTC time + */ + public int getOffsetFromLocal(long instantLocal) { + // get the offset at instantLocal (first estimate) + final int offsetLocal = getOffset(instantLocal); + // adjust instantLocal using the estimate and recalc the offset + final long instantAdjusted = instantLocal - offsetLocal; + final int offsetAdjusted = getOffset(instantAdjusted); + // if the offsets differ, we must be near a DST boundary + if (offsetLocal != offsetAdjusted) { + // we need to ensure that time is always after the DST gap + // this happens naturally for positive offsets, but not for negative + if ((offsetLocal - offsetAdjusted) < 0) { + // if we just return offsetAdjusted then the time is pushed + // back before the transition, whereas it should be + // on or after the transition + long nextLocal = nextTransition(instantAdjusted); + if (nextLocal == (instantLocal - offsetLocal)) { + nextLocal = Long.MAX_VALUE; + } + long nextAdjusted = nextTransition(instantLocal - offsetAdjusted); + if (nextAdjusted == (instantLocal - offsetAdjusted)) { + nextAdjusted = Long.MAX_VALUE; + } + if (nextLocal != nextAdjusted) { + return offsetLocal; + } + } + } else if (offsetLocal >= 0) { + long prev = previousTransition(instantAdjusted); + if (prev < instantAdjusted) { + int offsetPrev = getOffset(prev); + int diff = offsetPrev - offsetLocal; + if (instantAdjusted - prev <= diff) { + return offsetPrev; + } + } + } + return offsetAdjusted; + } + + /** + * Converts a standard UTC instant to a local instant with the same + * local time. This conversion is used before performing a calculation + * so that the calculation can be done using a simple local zone. + * + * @param instantUTC the UTC instant to convert to local + * @return the local instant with the same local time + * @throws ArithmeticException if the result overflows a long + * @since 1.5 + */ + public long convertUTCToLocal(long instantUTC) { + int offset = getOffset(instantUTC); + long instantLocal = instantUTC + offset; + // If there is a sign change, but the two values have the same sign... + if ((instantUTC ^ instantLocal) < 0 && (instantUTC ^ offset) >= 0) { + throw new ArithmeticException("Adding time zone offset caused overflow"); + } + return instantLocal; + } + + /** + * Converts a local instant to a standard UTC instant with the same + * local time attempting to use the same offset as the original. + *

+ * This conversion is used after performing a calculation + * where the calculation was done using a simple local zone. + * Whenever possible, the same offset as the original offset will be used. + * This is most significant during a daylight savings overlap. + * + * @param instantLocal the local instant to convert to UTC + * @param strict whether the conversion should reject non-existent local times + * @param originalInstantUTC the original instant that the calculation is based on + * @return the UTC instant with the same local time, + * @throws ArithmeticException if the result overflows a long + * @throws IllegalArgumentException if the zone has no equivalent local time + * @since 2.0 + */ + public long convertLocalToUTC(long instantLocal, boolean strict, long originalInstantUTC) { + int offsetOriginal = getOffset(originalInstantUTC); + long instantUTC = instantLocal - offsetOriginal; + int offsetLocalFromOriginal = getOffset(instantUTC); + if (offsetLocalFromOriginal == offsetOriginal) { + return instantUTC; + } + return convertLocalToUTC(instantLocal, strict); + } + + /** + * Converts a local instant to a standard UTC instant with the same + * local time. This conversion is used after performing a calculation + * where the calculation was done using a simple local zone. + * + * @param instantLocal the local instant to convert to UTC + * @param strict whether the conversion should reject non-existent local times + * @return the UTC instant with the same local time, + * @throws ArithmeticException if the result overflows a long + * @throws IllegalInstantException if the zone has no equivalent local time + * @since 1.5 + */ + public long convertLocalToUTC(long instantLocal, boolean strict) { + // get the offset at instantLocal (first estimate) + int offsetLocal = getOffset(instantLocal); + // adjust instantLocal using the estimate and recalc the offset + int offset = getOffset(instantLocal - offsetLocal); + // if the offsets differ, we must be near a DST boundary + if (offsetLocal != offset) { + // if strict then always check if in DST gap + // otherwise only check if zone in Western hemisphere (as the + // value of offset is already correct for Eastern hemisphere) + if (strict || offsetLocal < 0) { + // determine if we are in the DST gap + long nextLocal = nextTransition(instantLocal - offsetLocal); + if (nextLocal == (instantLocal - offsetLocal)) { + nextLocal = Long.MAX_VALUE; + } + long nextAdjusted = nextTransition(instantLocal - offset); + if (nextAdjusted == (instantLocal - offset)) { + nextAdjusted = Long.MAX_VALUE; + } + if (nextLocal != nextAdjusted) { + // yes we are in the DST gap + if (strict) { + // DST gap is not acceptable + throw new RuntimeException(getID()); + } else { + // DST gap is acceptable, but for the Western hemisphere + // the offset is wrong and will result in local times + // before the cutover so use the offsetLocal instead + offset = offsetLocal; + } + } + } + } + // check for overflow + long instantUTC = instantLocal - offset; + // If there is a sign change, but the two values have different signs... + if ((instantLocal ^ instantUTC) < 0 && (instantLocal ^ offset) < 0) { + throw new ArithmeticException("Subtracting time zone offset caused overflow"); + } + return instantUTC; + } + + /** + * Gets the millisecond instant in another zone keeping the same local time. + *

+ * The conversion is performed by converting the specified UTC millis to local + * millis in this zone, then converting back to UTC millis in the new zone. + * + * @param newZone the new zone, null means default + * @param oldInstant the UTC millisecond instant to convert + * @return the UTC millisecond instant with the same local time in the new zone + */ + public long getMillisKeepLocal(DateTimeZone newZone, long oldInstant) { + if (newZone == this) { + return oldInstant; + } + long instantLocal = convertUTCToLocal(oldInstant); + return newZone.convertLocalToUTC(instantLocal, false, oldInstant); + } + +// //----------------------------------------------------------------------- +// /** +// * Checks if the given {@link LocalDateTime} is within an overlap. +// *

+// * When switching from Daylight Savings Time to standard time there is +// * typically an overlap where the same clock hour occurs twice. This +// * method identifies whether the local datetime refers to such an overlap. +// * +// * @param localDateTime the time to check, not null +// * @return true if the given datetime refers to an overlap +// */ +// public boolean isLocalDateTimeOverlap(LocalDateTime localDateTime) { +// if (isFixed()) { +// return false; +// } +// long instantLocal = localDateTime.toDateTime(DateTimeZone.UTC).getMillis(); +// // get the offset at instantLocal (first estimate) +// int offsetLocal = getOffset(instantLocal); +// // adjust instantLocal using the estimate and recalc the offset +// int offset = getOffset(instantLocal - offsetLocal); +// // if the offsets differ, we must be near a DST boundary +// if (offsetLocal != offset) { +// long nextLocal = nextTransition(instantLocal - offsetLocal); +// long nextAdjusted = nextTransition(instantLocal - offset); +// if (nextLocal != nextAdjusted) { +// // in DST gap +// return false; +// } +// long diff = Math.abs(offset - offsetLocal); +// DateTime dateTime = localDateTime.toDateTime(this); +// DateTime adjusted = dateTime.plus(diff); +// if (dateTime.getHourOfDay() == adjusted.getHourOfDay() && +// dateTime.getMinuteOfHour() == adjusted.getMinuteOfHour() && +// dateTime.getSecondOfMinute() == adjusted.getSecondOfMinute()) { +// return true; +// } +// adjusted = dateTime.minus(diff); +// if (dateTime.getHourOfDay() == adjusted.getHourOfDay() && +// dateTime.getMinuteOfHour() == adjusted.getMinuteOfHour() && +// dateTime.getSecondOfMinute() == adjusted.getSecondOfMinute()) { +// return true; +// } +// return false; +// } +// return false; +// } +// +// +// DateTime dateTime = null; +// try { +// dateTime = localDateTime.toDateTime(this); +// } catch (IllegalArgumentException ex) { +// return false; // it is a gap, not an overlap +// } +// long offset1 = Math.abs(getOffset(dateTime.getMillis() + 1) - getStandardOffset(dateTime.getMillis() + 1)); +// long offset2 = Math.abs(getOffset(dateTime.getMillis() - 1) - getStandardOffset(dateTime.getMillis() - 1)); +// long offset = Math.max(offset1, offset2); +// if (offset == 0) { +// return false; +// } +// DateTime adjusted = dateTime.plus(offset); +// if (dateTime.getHourOfDay() == adjusted.getHourOfDay() && +// dateTime.getMinuteOfHour() == adjusted.getMinuteOfHour() && +// dateTime.getSecondOfMinute() == adjusted.getSecondOfMinute()) { +// return true; +// } +// adjusted = dateTime.minus(offset); +// if (dateTime.getHourOfDay() == adjusted.getHourOfDay() && +// dateTime.getMinuteOfHour() == adjusted.getMinuteOfHour() && +// dateTime.getSecondOfMinute() == adjusted.getSecondOfMinute()) { +// return true; +// } +// return false; + +// long millis = dateTime.getMillis(); +// long nextTransition = nextTransition(millis); +// long previousTransition = previousTransition(millis); +// long deltaToPreviousTransition = millis - previousTransition; +// long deltaToNextTransition = nextTransition - millis; +// if (deltaToNextTransition < deltaToPreviousTransition) { +// int offset = getOffset(nextTransition); +// int standardOffset = getStandardOffset(nextTransition); +// if (Math.abs(offset - standardOffset) >= deltaToNextTransition) { +// return true; +// } +// } else { +// int offset = getOffset(previousTransition); +// int standardOffset = getStandardOffset(previousTransition); +// if (Math.abs(offset - standardOffset) >= deltaToPreviousTransition) { +// return true; +// } +// } +// return false; +// } + + /** + * Adjusts the offset to be the earlier or later one during an overlap. + * + * @param instant the instant to adjust + * @param earlierOrLater false for earlier, true for later + * @return the adjusted instant millis + */ + public long adjustOffset(long instant, boolean earlierOrLater) { + // a bit messy, but will work in all non-pathological cases + + // evaluate 3 hours before and after to work out if anything is happening + long instantBefore = instant - 3 * MILLIS_PER_HOUR; + long instantAfter = instant + 3 * MILLIS_PER_HOUR; + long offsetBefore = getOffset(instantBefore); + long offsetAfter = getOffset(instantAfter); + if (offsetBefore <= offsetAfter) { + return instant; // not an overlap (less than is a gap, equal is normal case) + } + + // work out range of instants that have duplicate local times + long diff = offsetBefore - offsetAfter; + long transition = nextTransition(instantBefore); + long overlapStart = transition - diff; + long overlapEnd = transition + diff; + if (instant < overlapStart || instant >= overlapEnd) { + return instant; // not an overlap + } + + // calculate result + long afterStart = instant - overlapStart; + if (afterStart >= diff) { + // currently in later offset + return earlierOrLater ? instant : instant - diff; + } else { + // currently in earlier offset + return earlierOrLater ? instant + diff : instant; + } + } +// System.out.println(new DateTime(transitionStart, DateTimeZone.UTC) + " " + new DateTime(transitionStart, this)); + + //----------------------------------------------------------------------- + /** + * Returns true if this time zone has no transitions. + * + * @return true if no transitions + */ + public abstract boolean isFixed(); + + /** + * Advances the given instant to where the time zone offset or name changes. + * If the instant returned is exactly the same as passed in, then + * no changes occur after the given instant. + * + * @param instant milliseconds from 1970-01-01T00:00:00Z + * @return milliseconds from 1970-01-01T00:00:00Z + */ + public abstract long nextTransition(long instant); + + /** + * Retreats the given instant to where the time zone offset or name changes. + * If the instant returned is exactly the same as passed in, then + * no changes occur before the given instant. + * + * @param instant milliseconds from 1970-01-01T00:00:00Z + * @return milliseconds from 1970-01-01T00:00:00Z + */ + public abstract long previousTransition(long instant); +} diff --git a/teavm-classlib/src/main/java/org/teavm/classlib/impl/tz/DateTimeZoneBuilder.java b/teavm-classlib/src/main/java/org/teavm/classlib/impl/tz/DateTimeZoneBuilder.java new file mode 100644 index 000000000..57e539e23 --- /dev/null +++ b/teavm-classlib/src/main/java/org/teavm/classlib/impl/tz/DateTimeZoneBuilder.java @@ -0,0 +1,1259 @@ +/* + * Copyright 2001-2013 Stephen Colebourne + * + * 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 org.teavm.classlib.impl.tz; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.GregorianCalendar; +import java.util.Iterator; +import org.teavm.classlib.impl.Base46; + +/** + * DateTimeZoneBuilder allows complex DateTimeZones to be constructed. Since + * creating a new DateTimeZone this way is a relatively expensive operation, + * built zones can be written to a file. Reading back the encoded data is a + * quick operation. + *

+ * DateTimeZoneBuilder itself is mutable and not thread-safe, but the + * DateTimeZone objects that it builds are thread-safe and immutable. + *

+ * It is intended that {@link ZoneInfoCompiler} be used to read time zone data + * files, indirectly calling DateTimeZoneBuilder. The following complex + * example defines the America/Los_Angeles time zone, with all historical + * transitions: + * + *

+ * DateTimeZone America_Los_Angeles = new DateTimeZoneBuilder()
+ *     .addCutover(-2147483648, 'w', 1, 1, 0, false, 0)
+ *     .setStandardOffset(-28378000)
+ *     .setFixedSavings("LMT", 0)
+ *     .addCutover(1883, 'w', 11, 18, 0, false, 43200000)
+ *     .setStandardOffset(-28800000)
+ *     .addRecurringSavings("PDT", 3600000, 1918, 1919, 'w',  3, -1, 7, false, 7200000)
+ *     .addRecurringSavings("PST",       0, 1918, 1919, 'w', 10, -1, 7, false, 7200000)
+ *     .addRecurringSavings("PWT", 3600000, 1942, 1942, 'w',  2,  9, 0, false, 7200000)
+ *     .addRecurringSavings("PPT", 3600000, 1945, 1945, 'u',  8, 14, 0, false, 82800000)
+ *     .addRecurringSavings("PST",       0, 1945, 1945, 'w',  9, 30, 0, false, 7200000)
+ *     .addRecurringSavings("PDT", 3600000, 1948, 1948, 'w',  3, 14, 0, false, 7200000)
+ *     .addRecurringSavings("PST",       0, 1949, 1949, 'w',  1,  1, 0, false, 7200000)
+ *     .addRecurringSavings("PDT", 3600000, 1950, 1966, 'w',  4, -1, 7, false, 7200000)
+ *     .addRecurringSavings("PST",       0, 1950, 1961, 'w',  9, -1, 7, false, 7200000)
+ *     .addRecurringSavings("PST",       0, 1962, 1966, 'w', 10, -1, 7, false, 7200000)
+ *     .addRecurringSavings("PST",       0, 1967, 2147483647, 'w', 10, -1, 7, false, 7200000)
+ *     .addRecurringSavings("PDT", 3600000, 1967, 1973, 'w', 4, -1,  7, false, 7200000)
+ *     .addRecurringSavings("PDT", 3600000, 1974, 1974, 'w', 1,  6,  0, false, 7200000)
+ *     .addRecurringSavings("PDT", 3600000, 1975, 1975, 'w', 2, 23,  0, false, 7200000)
+ *     .addRecurringSavings("PDT", 3600000, 1976, 1986, 'w', 4, -1,  7, false, 7200000)
+ *     .addRecurringSavings("PDT", 3600000, 1987, 2147483647, 'w', 4, 1, 7, true, 7200000)
+ *     .toDateTimeZone("America/Los_Angeles", true);
+ * 
+ * + * @author Brian S O'Neill + * @see ZoneInfoCompiler + * @see ZoneInfoProvider + * @since 1.0 + */ +public class DateTimeZoneBuilder { + private static StorableDateTimeZone buildFixedZone(String id, int wallOffset, int standardOffset) { + return new FixedDateTimeZone(id, wallOffset, standardOffset); + } + + // List of RuleSets. + private final ArrayList iRuleSets; + + public DateTimeZoneBuilder() { + iRuleSets = new ArrayList<>(10); + } + + /** + * Adds a cutover for added rules. The standard offset at the cutover + * defaults to 0. Call setStandardOffset afterwards to change it. + * + * @param year the year of cutover + * @param mode 'u' - cutover is measured against UTC, 'w' - against wall + * offset, 's' - against standard offset + * @param monthOfYear the month from 1 (January) to 12 (December) + * @param dayOfMonth if negative, set to ((last day of month) - ~dayOfMonth). + * For example, if -1, set to last day of month + * @param dayOfWeek from 1 (Monday) to 7 (Sunday), if 0 then ignore + * @param advanceDayOfWeek if dayOfMonth does not fall on dayOfWeek, advance to + * dayOfWeek when true, retreat when false. + * @param millisOfDay additional precision for specifying time of day of cutover + */ + public DateTimeZoneBuilder addCutover(int year, + char mode, + int monthOfYear, + int dayOfMonth, + int dayOfWeek, + boolean advanceDayOfWeek, + int millisOfDay) + { + if (iRuleSets.size() > 0) { + OfYear ofYear = new OfYear + (mode, monthOfYear, dayOfMonth, dayOfWeek, advanceDayOfWeek, millisOfDay); + RuleSet lastRuleSet = iRuleSets.get(iRuleSets.size() - 1); + lastRuleSet.setUpperLimit(year, ofYear); + } + iRuleSets.add(new RuleSet()); + return this; + } + + /** + * Sets the standard offset to use for newly added rules until the next + * cutover is added. + * @param standardOffset the standard offset in millis + */ + public DateTimeZoneBuilder setStandardOffset(int standardOffset) { + getLastRuleSet().setStandardOffset(standardOffset); + return this; + } + + /** + * Set a fixed savings rule at the cutover. + */ + public DateTimeZoneBuilder setFixedSavings(String nameKey, int saveMillis) { + getLastRuleSet().setFixedSavings(nameKey, saveMillis); + return this; + } + + /** + * Add a recurring daylight saving time rule. + * + * @param nameKey the name key of new rule + * @param saveMillis the milliseconds to add to standard offset + * @param fromYear the first year that rule is in effect, MIN_VALUE indicates + * beginning of time + * @param toYear the last year (inclusive) that rule is in effect, MAX_VALUE + * indicates end of time + * @param mode 'u' - transitions are calculated against UTC, 'w' - + * transitions are calculated against wall offset, 's' - transitions are + * calculated against standard offset + * @param monthOfYear the month from 1 (January) to 12 (December) + * @param dayOfMonth if negative, set to ((last day of month) - ~dayOfMonth). + * For example, if -1, set to last day of month + * @param dayOfWeek from 1 (Monday) to 7 (Sunday), if 0 then ignore + * @param advanceDayOfWeek if dayOfMonth does not fall on dayOfWeek, advance to + * dayOfWeek when true, retreat when false. + * @param millisOfDay additional precision for specifying time of day of transitions + */ + public DateTimeZoneBuilder addRecurringSavings(int saveMillis, + int fromYear, int toYear, + char mode, + int monthOfYear, + int dayOfMonth, + int dayOfWeek, + boolean advanceDayOfWeek, + int millisOfDay) + { + if (fromYear <= toYear) { + OfYear ofYear = new OfYear + (mode, monthOfYear, dayOfMonth, dayOfWeek, advanceDayOfWeek, millisOfDay); + Recurrence recurrence = new Recurrence(ofYear, saveMillis); + Rule rule = new Rule(recurrence, fromYear, toYear); + getLastRuleSet().addRule(rule); + } + return this; + } + + private RuleSet getLastRuleSet() { + if (iRuleSets.size() == 0) { + addCutover(Integer.MIN_VALUE, 'w', 1, 1, 0, false, 0); + } + return iRuleSets.get(iRuleSets.size() - 1); + } + + /** + * Processes all the rules and builds a DateTimeZone. + * + * @param id time zone id to assign + * @param outputID true if the zone id should be output + */ + public StorableDateTimeZone toDateTimeZone(String id, boolean outputID) { + if (id == null) { + throw new IllegalArgumentException(); + } + + // Discover where all the transitions occur and store the results in + // these lists. + ArrayList transitions = new ArrayList<>(); + + // Tail zone picks up remaining transitions in the form of an endless + // DST cycle. + DSTZone tailZone = null; + + long millis = Long.MIN_VALUE; + int saveMillis = 0; + + int ruleSetCount = iRuleSets.size(); + for (int i=0; i transitions, Transition tr) { + int size = transitions.size(); + if (size == 0) { + transitions.add(tr); + return true; + } + + Transition last = transitions.get(size - 1); + if (!tr.isTransitionFrom(last)) { + return false; + } + + // If local time of new transition is same as last local time, just + // replace last transition with new one. + int offsetForLast = 0; + if (size >= 2) { + offsetForLast = transitions.get(size - 2).getWallOffset(); + } + int offsetForNew = last.getWallOffset(); + + long lastLocal = last.getMillis() + offsetForLast; + long newLocal = tr.getMillis() + offsetForNew; + + if (newLocal != lastLocal) { + transitions.add(tr); + return true; + } + + transitions.remove(size - 1); + return addTransition(transitions, tr); + } + + /** + * Supports setting fields of year and moving between transitions. + */ + private static final class OfYear { + // Is 'u', 'w', or 's'. + final char iMode; + + final int iMonthOfYear; + final int iDayOfMonth; + final int iDayOfWeek; + final boolean iAdvance; + final int iMillisOfDay; + + OfYear(char mode, + int monthOfYear, + int dayOfMonth, + int dayOfWeek, boolean advanceDayOfWeek, + int millisOfDay) + { + if (mode != 'u' && mode != 'w' && mode != 's') { + throw new IllegalArgumentException("Unknown mode: " + mode); + } + + iMode = mode; + iMonthOfYear = monthOfYear; + iDayOfMonth = dayOfMonth; + iDayOfWeek = dayOfWeek; + iAdvance = advanceDayOfWeek; + iMillisOfDay = millisOfDay; + } + + public void write(StringBuilder sb) { + sb.append(iMode); + Base46.encodeUnsigned(sb, iDayOfMonth); + Base46.encodeUnsigned(sb, iMonthOfYear); + Base46.encode(sb, iDayOfMonth); + Base46.encode(sb, iDayOfWeek); + sb.append(iAdvance ? 'y' : 'n'); + StorableDateTimeZone.writeUnsignedTime(sb, iMillisOfDay); + } + + /** + * @param standardOffset standard offset just before instant + */ + public long setInstant(int year, int standardOffset, int saveMillis) { + int offset; + if (iMode == 'w') { + offset = standardOffset + saveMillis; + } else if (iMode == 's') { + offset = standardOffset; + } else { + offset = 0; + } + + Calendar calendar = Calendar.getInstance(); + calendar.setTimeInMillis(0); + calendar.set(Calendar.YEAR, year); + calendar.set(Calendar.MONTH, iMonthOfYear); + calendar.add(Calendar.MILLISECOND, iMillisOfDay); + setDayOfMonth(calendar); + + if (iDayOfWeek != 0) { + setDayOfWeek(calendar); + } + + // Convert from local time to UTC. + return calendar.getTimeInMillis() - offset; + } + + /** + * @param standardOffset standard offset just before next recurrence + */ + public long next(long instant, int standardOffset, int saveMillis) { + int offset; + if (iMode == 'w') { + offset = standardOffset + saveMillis; + } else if (iMode == 's') { + offset = standardOffset; + } else { + offset = 0; + } + + // Convert from UTC to local time. + instant += offset; + + GregorianCalendar calendar = new GregorianCalendar(); + calendar.setTimeInMillis(instant); + calendar.set(Calendar.MONTH, iMonthOfYear); + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + calendar.add(Calendar.MILLISECOND, iMillisOfDay); + setDayOfMonthNext(calendar); + + if (iDayOfWeek == 0) { + if (calendar.getTimeInMillis() <= instant) { + calendar.add(Calendar.YEAR, 1); + setDayOfMonthNext(calendar); + } + } else { + setDayOfWeek(calendar); + if (calendar.getTimeInMillis() <= instant) { + calendar.add(Calendar.YEAR, 1); + calendar.set(Calendar.MONTH, iMonthOfYear); + setDayOfMonthNext(calendar); + setDayOfWeek(calendar); + } + } + + // Convert from local time to UTC. + return calendar.getTimeInMillis() - offset; + } + + /** + * @param standardOffset standard offset just before previous recurrence + */ + public long previous(long instant, int standardOffset, int saveMillis) { + int offset; + if (iMode == 'w') { + offset = standardOffset + saveMillis; + } else if (iMode == 's') { + offset = standardOffset; + } else { + offset = 0; + } + + // Convert from UTC to local time. + instant += offset; + + GregorianCalendar calendar = new GregorianCalendar(); + calendar.setTimeInMillis(instant); + calendar.set(Calendar.MONTH, iMonthOfYear); + // Be lenient with millisOfDay. + calendar.set(Calendar.HOUR_OF_DAY, 0); + calendar.set(Calendar.MINUTE, 0); + calendar.set(Calendar.SECOND, 0); + calendar.set(Calendar.MILLISECOND, 0); + calendar.add(Calendar.MILLISECOND, iMillisOfDay); + setDayOfMonthPrevious(calendar); + + if (iDayOfWeek == 0) { + if (calendar.getTimeInMillis() >= instant) { + calendar.add(Calendar.YEAR, -1); + setDayOfMonthPrevious(calendar); + } + } else { + setDayOfWeek(calendar); + if (calendar.getTimeInMillis() >= instant) { + calendar.add(Calendar.YEAR, -1); + calendar.set(Calendar.MONTH, iMonthOfYear); + setDayOfMonthPrevious(calendar); + setDayOfWeek(calendar); + } + } + + // Convert from local time to UTC. + return calendar.getTimeInMillis() - offset; + } + + /** + * If month-day is 02-29 and year isn't leap, advances to next leap year. + */ + private void setDayOfMonthNext(GregorianCalendar calendar) { + if (calendar.get(Calendar.MONTH) == Calendar.FEBRUARY && calendar.get(Calendar.DATE) == 29) { + while (!calendar.isLeapYear(calendar.get(Calendar.YEAR))) { + calendar.add(Calendar.YEAR, 1); + } + } + } + + /** + * If month-day is 02-29 and year isn't leap, retreats to previous leap year. + */ + private void setDayOfMonthPrevious(GregorianCalendar calendar) { + if (calendar.get(Calendar.MONTH) == Calendar.FEBRUARY && calendar.get(Calendar.DATE) == 29) { + while (!calendar.isLeapYear(calendar.get(Calendar.YEAR))) { + calendar.add(Calendar.YEAR, -1); + } + } + } + + private void setDayOfMonth(Calendar calendar) { + if (iDayOfMonth >= 0) { + calendar.set(Calendar.DAY_OF_MONTH, iDayOfMonth); + } else { + calendar.set(Calendar.DAY_OF_MONTH, 1); + calendar.add(Calendar.MONTH, 1); + calendar.add(Calendar.DAY_OF_MONTH, iDayOfMonth); + } + } + + private void setDayOfWeek(Calendar calendar) { + int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK); + int daysToAdd = iDayOfWeek - dayOfWeek; + if (daysToAdd != 0) { + if (iAdvance) { + if (daysToAdd < 0) { + daysToAdd += 7; + } + } else { + if (daysToAdd > 0) { + daysToAdd -= 7; + } + } + calendar.add(Calendar.DAY_OF_WEEK, daysToAdd); + } + } + } + + /** + * Extends OfYear with a nameKey and savings. + */ + private static final class Recurrence { + final OfYear iOfYear; + final int iSaveMillis; + + Recurrence(OfYear ofYear, int saveMillis) { + iOfYear = ofYear; + iSaveMillis = saveMillis; + } + + public OfYear getOfYear() { + return iOfYear; + } + + /** + * @param standardOffset standard offset just before next recurrence + */ + public long next(long instant, int standardOffset, int saveMillis) { + return iOfYear.next(instant, standardOffset, saveMillis); + } + + /** + * @param standardOffset standard offset just before previous recurrence + */ + public long previous(long instant, int standardOffset, int saveMillis) { + return iOfYear.previous(instant, standardOffset, saveMillis); + } + + public int getSaveMillis() { + return iSaveMillis; + } + + public void write(StringBuilder sb) { + iOfYear.write(sb); + StorableDateTimeZone.writeTime(sb, iSaveMillis); + } + } + + /** + * Extends Recurrence with inclusive year limits. + */ + private static final class Rule { + final Recurrence iRecurrence; + final int iFromYear; // inclusive + final int iToYear; // inclusive + + Rule(Recurrence recurrence, int fromYear, int toYear) { + iRecurrence = recurrence; + iFromYear = fromYear; + iToYear = toYear; + } + + @SuppressWarnings("unused") + public int getFromYear() { + return iFromYear; + } + + public int getToYear() { + return iToYear; + } + + @SuppressWarnings("unused") + public OfYear getOfYear() { + return iRecurrence.getOfYear(); + } + + public int getSaveMillis() { + return iRecurrence.getSaveMillis(); + } + + public long next(final long instant, int standardOffset, int saveMillis) { + Calendar calendar = Calendar.getInstance(); + final int wallOffset = standardOffset + saveMillis; + long testInstant = instant; + + int year; + if (instant == Long.MIN_VALUE) { + year = Integer.MIN_VALUE; + } else { + calendar.setTimeInMillis(instant + wallOffset); + year = calendar.get(Calendar.YEAR); + } + + if (year < iFromYear) { + calendar.setTimeInMillis(0); + calendar.set(Calendar.YEAR, iFromYear); + // First advance instant to start of from year. + testInstant = calendar.getTimeInMillis() - wallOffset; + // Back off one millisecond to account for next recurrence + // being exactly at the beginning of the year. + testInstant -= 1; + } + + long next = iRecurrence.next(testInstant, standardOffset, saveMillis); + + if (next > instant) { + calendar.setTimeInMillis(next + wallOffset); + year = calendar.get(Calendar.YEAR); + if (year > iToYear) { + // Out of range, return original value. + next = instant; + } + } + + return next; + } + } + + private static final class Transition { + private final long iMillis; + private final int iWallOffset; + private final int iStandardOffset; + + Transition(long millis, Transition tr) { + iMillis = millis; + iWallOffset = tr.iWallOffset; + iStandardOffset = tr.iStandardOffset; + } + + Transition(long millis, Rule rule, int standardOffset) { + iMillis = millis; + iWallOffset = standardOffset + rule.getSaveMillis(); + iStandardOffset = standardOffset; + } + + Transition(long millis, int wallOffset, int standardOffset) { + iMillis = millis; + iWallOffset = wallOffset; + iStandardOffset = standardOffset; + } + + public long getMillis() { + return iMillis; + } + + public int getWallOffset() { + return iWallOffset; + } + + public int getStandardOffset() { + return iStandardOffset; + } + + public int getSaveMillis() { + return iWallOffset - iStandardOffset; + } + + /** + * There must be a change in the millis, wall offsets or name keys. + */ + public boolean isTransitionFrom(Transition other) { + if (other == null) { + return true; + } + return iMillis > other.iMillis && + iWallOffset != other.iWallOffset; + } + } + + private static final class RuleSet { + private static final int YEAR_LIMIT; + + static { + // Don't pre-calculate more than 100 years into the future. Almost + // all zones will stop pre-calculating far sooner anyhow. Either a + // simple DST cycle is detected or the last rule is a fixed + // offset. If a zone has a fixed offset set more than 100 years + // into the future, then it won't be observed. + Calendar calendar = Calendar.getInstance(); + YEAR_LIMIT = calendar.get(Calendar.YEAR) + 100; + } + + private int iStandardOffset; + private ArrayList iRules; + + // Optional. + private String iInitialNameKey; + private int iInitialSaveMillis; + + // Upper limit is exclusive. + private int iUpperYear; + private OfYear iUpperOfYear; + + RuleSet() { + iRules = new ArrayList<>(10); + iUpperYear = Integer.MAX_VALUE; + } + + /** + * Copy constructor. + */ + RuleSet(RuleSet rs) { + iStandardOffset = rs.iStandardOffset; + iRules = new ArrayList<>(rs.iRules); + iInitialSaveMillis = rs.iInitialSaveMillis; + iUpperYear = rs.iUpperYear; + iUpperOfYear = rs.iUpperOfYear; + } + + @SuppressWarnings("unused") + public int getStandardOffset() { + return iStandardOffset; + } + + public void setStandardOffset(int standardOffset) { + iStandardOffset = standardOffset; + } + + public void setFixedSavings(String nameKey, int saveMillis) { + iInitialNameKey = nameKey; + iInitialSaveMillis = saveMillis; + } + + public void addRule(Rule rule) { + if (!iRules.contains(rule)) { + iRules.add(rule); + } + } + + public void setUpperLimit(int year, OfYear ofYear) { + iUpperYear = year; + iUpperOfYear = ofYear; + } + + /** + * Returns a transition at firstMillis with the first name key and + * offsets for this rule set. This method may return null. + * + * @param firstMillis millis of first transition + */ + public Transition firstTransition(final long firstMillis) { + if (iInitialNameKey != null) { + // Initial zone info explicitly set, so don't search the rules. + return new Transition(firstMillis, iStandardOffset + iInitialSaveMillis, iStandardOffset); + } + + // Make a copy before we destroy the rules. + ArrayList copy = new ArrayList<>(iRules); + + // Iterate through all the transitions until firstMillis is + // reached. Use the name key and savings for whatever rule reaches + // the limit. + + long millis = Long.MIN_VALUE; + int saveMillis = 0; + Transition first = null; + + Transition next; + while ((next = nextTransition(millis, saveMillis)) != null) { + millis = next.getMillis(); + + if (millis == firstMillis) { + first = new Transition(firstMillis, next); + break; + } + + if (millis > firstMillis) { + if (first == null) { + // Find first rule without savings. This way a more + // accurate nameKey is found even though no rule + // extends to the RuleSet's lower limit. + for (Rule rule : copy) { + if (rule.getSaveMillis() == 0) { + first = new Transition(firstMillis, rule, iStandardOffset); + break; + } + } + } + if (first == null) { + // Found no rule without savings. Create a transition + // with no savings anyhow, and use the best available + // name key. + first = new Transition(firstMillis, iStandardOffset, iStandardOffset); + } + break; + } + + // Set first to the best transition found so far, but next + // iteration may find something closer to lower limit. + first = new Transition(firstMillis, next); + + saveMillis = next.getSaveMillis(); + } + + iRules = copy; + return first; + } + + /** + * Returns null if RuleSet is exhausted or upper limit reached. Calling + * this method will throw away rules as they each become + * exhausted. Copy the RuleSet before using it to compute transitions. + * + * Returned transition may be a duplicate from previous + * transition. Caller must call isTransitionFrom to filter out + * duplicates. + * + * @param saveMillis savings before next transition + */ + public Transition nextTransition(final long instant, final int saveMillis) { + // Find next matching rule. + Rule nextRule = null; + long nextMillis = Long.MAX_VALUE; + + Iterator it = iRules.iterator(); + while (it.hasNext()) { + Rule rule = it.next(); + long next = rule.next(instant, iStandardOffset, saveMillis); + if (next <= instant) { + it.remove(); + continue; + } + // Even if next is same as previous next, choose the rule + // in order for more recently added rules to override. + if (next <= nextMillis) { + // Found a better match. + nextRule = rule; + nextMillis = next; + } + } + + if (nextRule == null) { + return null; + } + + // Stop precalculating if year reaches some arbitrary limit. + Calendar c = Calendar.getInstance(); + c.setTimeInMillis(nextMillis); + if (c.get(Calendar.YEAR) >= YEAR_LIMIT) { + return null; + } + + // Check if upper limit reached or passed. + if (iUpperYear < Integer.MAX_VALUE) { + long upperMillis = + iUpperOfYear.setInstant(iUpperYear, iStandardOffset, saveMillis); + if (nextMillis >= upperMillis) { + // At or after upper limit. + return null; + } + } + + return new Transition(nextMillis, nextRule, iStandardOffset); + } + + /** + * @param saveMillis savings before upper limit + */ + public long getUpperLimit(int saveMillis) { + if (iUpperYear == Integer.MAX_VALUE) { + return Long.MAX_VALUE; + } + return iUpperOfYear.setInstant(iUpperYear, iStandardOffset, saveMillis); + } + + /** + * Returns null if none can be built. + */ + public DSTZone buildTailZone(String id) { + if (iRules.size() == 2) { + Rule startRule = iRules.get(0); + Rule endRule = iRules.get(1); + if (startRule.getToYear() == Integer.MAX_VALUE && + endRule.getToYear() == Integer.MAX_VALUE) { + + // With exactly two infinitely recurring rules left, a + // simple DSTZone can be formed. + + // The order of rules can come in any order, and it doesn't + // really matter which rule was chosen the 'start' and + // which is chosen the 'end'. DSTZone works properly either + // way. + return new DSTZone(id, iStandardOffset, + startRule.iRecurrence, endRule.iRecurrence); + } + } + return null; + } + } + + private static final class DSTZone extends StorableDateTimeZone { + final int iStandardOffset; + final Recurrence iStartRecurrence; + final Recurrence iEndRecurrence; + + DSTZone(String id, int standardOffset, + Recurrence startRecurrence, Recurrence endRecurrence) { + super(id); + iStandardOffset = standardOffset; + iStartRecurrence = startRecurrence; + iEndRecurrence = endRecurrence; + } + + @Override + public int getOffset(long instant) { + return iStandardOffset + findMatchingRecurrence(instant).getSaveMillis(); + } + + @Override + public int getStandardOffset(long instant) { + return iStandardOffset; + } + + @Override + public boolean isFixed() { + return false; + } + + @Override + public long nextTransition(long instant) { + int standardOffset = iStandardOffset; + Recurrence startRecurrence = iStartRecurrence; + Recurrence endRecurrence = iEndRecurrence; + + long start, end; + + try { + start = startRecurrence.next + (instant, standardOffset, endRecurrence.getSaveMillis()); + if (instant > 0 && start < 0) { + // Overflowed. + start = instant; + } + } catch (IllegalArgumentException e) { + // Overflowed. + start = instant; + } catch (ArithmeticException e) { + // Overflowed. + start = instant; + } + + try { + end = endRecurrence.next + (instant, standardOffset, startRecurrence.getSaveMillis()); + if (instant > 0 && end < 0) { + // Overflowed. + end = instant; + } + } catch (IllegalArgumentException e) { + // Overflowed. + end = instant; + } catch (ArithmeticException e) { + // Overflowed. + end = instant; + } + + return (start > end) ? end : start; + } + + @Override + public long previousTransition(long instant) { + // Increment in order to handle the case where instant is exactly at + // a transition. + instant++; + + int standardOffset = iStandardOffset; + Recurrence startRecurrence = iStartRecurrence; + Recurrence endRecurrence = iEndRecurrence; + + long start, end; + + try { + start = startRecurrence.previous + (instant, standardOffset, endRecurrence.getSaveMillis()); + if (instant < 0 && start > 0) { + // Overflowed. + start = instant; + } + } catch (IllegalArgumentException e) { + // Overflowed. + start = instant; + } catch (ArithmeticException e) { + // Overflowed. + start = instant; + } + + try { + end = endRecurrence.previous + (instant, standardOffset, startRecurrence.getSaveMillis()); + if (instant < 0 && end > 0) { + // Overflowed. + end = instant; + } + } catch (IllegalArgumentException e) { + // Overflowed. + end = instant; + } catch (ArithmeticException e) { + // Overflowed. + end = instant; + } + + return ((start > end) ? start : end) - 1; + } + + private Recurrence findMatchingRecurrence(long instant) { + int standardOffset = iStandardOffset; + Recurrence startRecurrence = iStartRecurrence; + Recurrence endRecurrence = iEndRecurrence; + + long start, end; + + try { + start = startRecurrence.next + (instant, standardOffset, endRecurrence.getSaveMillis()); + } catch (IllegalArgumentException e) { + // Overflowed. + start = instant; + } catch (ArithmeticException e) { + // Overflowed. + start = instant; + } + + try { + end = endRecurrence.next + (instant, standardOffset, startRecurrence.getSaveMillis()); + } catch (IllegalArgumentException e) { + // Overflowed. + end = instant; + } catch (ArithmeticException e) { + // Overflowed. + end = instant; + } + + return (start > end) ? startRecurrence : endRecurrence; + } + + @Override + public void write(StringBuilder sb) { + Base46.encodeUnsigned(sb, DST); + writeTime(sb, iStandardOffset); + iStartRecurrence.write(sb); + iEndRecurrence.write(sb); + } + } + + private static final class PrecalculatedZone extends StorableDateTimeZone { + /** + * Factory to create instance from builder. + * + * @param id the zone id + * @param outputID true if the zone id should be output + * @param transitions the list of Transition objects + * @param tailZone optional zone for getting info beyond precalculated tables + */ + static PrecalculatedZone create(String id, boolean outputID, ArrayList transitions, + DSTZone tailZone) { + int size = transitions.size(); + if (size == 0) { + throw new IllegalArgumentException(); + } + + long[] trans = new long[size]; + int[] wallOffsets = new int[size]; + int[] standardOffsets = new int[size]; + + Transition last = null; + for (int i=0; i= 0) { + return iWallOffsets[i]; + } + i = ~i; + if (i < transitions.length) { + if (i > 0) { + return iWallOffsets[i - 1]; + } + return 0; + } + if (iTailZone == null) { + return iWallOffsets[i - 1]; + } + return iTailZone.getOffset(instant); + } + + @Override + public int getStandardOffset(long instant) { + long[] transitions = iTransitions; + int i = Arrays.binarySearch(transitions, instant); + if (i >= 0) { + return iStandardOffsets[i]; + } + i = ~i; + if (i < transitions.length) { + if (i > 0) { + return iStandardOffsets[i - 1]; + } + return 0; + } + if (iTailZone == null) { + return iStandardOffsets[i - 1]; + } + return iTailZone.getStandardOffset(instant); + } + + @Override + public boolean isFixed() { + return false; + } + + @Override + public long nextTransition(long instant) { + long[] transitions = iTransitions; + int i = Arrays.binarySearch(transitions, instant); + i = (i >= 0) ? (i + 1) : ~i; + if (i < transitions.length) { + return transitions[i]; + } + if (iTailZone == null) { + return instant; + } + long end = transitions[transitions.length - 1]; + if (instant < end) { + instant = end; + } + return iTailZone.nextTransition(instant); + } + + @Override + public long previousTransition(long instant) { + long[] transitions = iTransitions; + int i = Arrays.binarySearch(transitions, instant); + if (i >= 0) { + if (instant > Long.MIN_VALUE) { + return instant - 1; + } + return instant; + } + i = ~i; + if (i < transitions.length) { + if (i > 0) { + long prev = transitions[i - 1]; + if (prev > Long.MIN_VALUE) { + return prev - 1; + } + } + return instant; + } + if (iTailZone != null) { + long prev = iTailZone.previousTransition(instant); + if (prev < instant) { + return prev; + } + } + long prev = transitions[i - 1]; + if (prev > Long.MIN_VALUE) { + return prev - 1; + } + return instant; + } + + public boolean isCachable() { + if (iTailZone != null) { + return true; + } + long[] transitions = iTransitions; + if (transitions.length <= 1) { + return false; + } + + // Add up all the distances between transitions that are less than + // about two years. + double distances = 0; + int count = 0; + + for (int i=1; i 0) { + double avg = distances / count; + avg /= 24 * 60 * 60 * 1000; + if (avg >= 25) { + // Only bother caching if average distance between + // transitions is at least 25 days. Why 25? + // CachedDateTimeZone is more efficient if the distance + // between transitions is large. With an average of 25, it + // will on average perform about 2 tests per cache + // hit. (49.7 / 25) is approximately 2. + return true; + } + } + + return false; + } + } +} diff --git a/teavm-classlib/src/main/java/org/teavm/classlib/impl/tz/FixedDateTimeZone.java b/teavm-classlib/src/main/java/org/teavm/classlib/impl/tz/FixedDateTimeZone.java new file mode 100644 index 000000000..754f7328a --- /dev/null +++ b/teavm-classlib/src/main/java/org/teavm/classlib/impl/tz/FixedDateTimeZone.java @@ -0,0 +1,74 @@ +/* + * Copyright 2001-2005 Stephen Colebourne + * + * 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 org.teavm.classlib.impl.tz; + +import org.teavm.classlib.impl.Base46; + +/** + * Basic DateTimeZone implementation that has a fixed name key and offsets. + *

+ * FixedDateTimeZone is thread-safe and immutable. + * + * @author Brian S O'Neill + * @since 1.0 + */ +public final class FixedDateTimeZone extends StorableDateTimeZone { + private final int iWallOffset; + private final int iStandardOffset; + + public FixedDateTimeZone(String id, int wallOffset, int standardOffset) { + super(id); + iWallOffset = wallOffset; + iStandardOffset = standardOffset; + } + + @Override + public int getOffset(long instant) { + return iWallOffset; + } + + @Override + public int getStandardOffset(long instant) { + return iStandardOffset; + } + + @Override + public int getOffsetFromLocal(long instantLocal) { + return iWallOffset; + } + + @Override + public boolean isFixed() { + return true; + } + + @Override + public long nextTransition(long instant) { + return instant; + } + + @Override + public long previousTransition(long instant) { + return instant; + } + + @Override + public void write(StringBuilder sb) { + Base46.encodeUnsigned(sb, FIXED); + writeTime(sb, iWallOffset); + writeTime(sb, iStandardOffset); + } +} diff --git a/teavm-classlib/src/main/java/org/teavm/classlib/impl/tz/StorableDateTimeZone.java b/teavm-classlib/src/main/java/org/teavm/classlib/impl/tz/StorableDateTimeZone.java new file mode 100644 index 000000000..f6cf22df5 --- /dev/null +++ b/teavm-classlib/src/main/java/org/teavm/classlib/impl/tz/StorableDateTimeZone.java @@ -0,0 +1,51 @@ +/* + * Copyright 2015 Alexey Andreev. + * + * 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 org.teavm.classlib.impl.tz; + +import org.teavm.classlib.impl.Base46; + +/** + * + * @author Alexey Andreev + */ +public abstract class StorableDateTimeZone extends DateTimeZone { + public static int PRECALCULATED = 0; + public static int FIXED = 1; + public static int CACHED = 2; + public static int DST = 3; + + public StorableDateTimeZone(String id) { + super(id); + } + + public abstract void write(StringBuilder sb); + + public static void writeTime(StringBuilder sb, long time) { + if (time % 1800_000 == 0) { + Base46.encode(sb, (int)((time / 1800_000) << 1)); + } else { + Base46.encode(sb, (int)(((time / 60_000) << 1) | 1)); + } + } + + public static void writeUnsignedTime(StringBuilder sb, long time) { + if (time % 1800_000 == 0) { + Base46.encodeUnsigned(sb, (int)((time / 1800_000) << 1)); + } else { + Base46.encodeUnsigned(sb, (int)(((time / 60_000) << 1) | 1)); + } + } +} diff --git a/teavm-classlib/src/main/java/org/teavm/classlib/impl/tz/TimeZoneWriter.java b/teavm-classlib/src/main/java/org/teavm/classlib/impl/tz/TimeZoneWriter.java new file mode 100644 index 000000000..3ec964250 --- /dev/null +++ b/teavm-classlib/src/main/java/org/teavm/classlib/impl/tz/TimeZoneWriter.java @@ -0,0 +1,24 @@ +/* + * Copyright 2015 Alexey Andreev. + * + * 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 org.teavm.classlib.impl.tz; + +/** + * + * @author Alexey Andreev + */ +public class TimeZoneWriter { + +} diff --git a/teavm-classlib/src/main/java/org/teavm/classlib/impl/tz/ZoneInfoCompiler.java b/teavm-classlib/src/main/java/org/teavm/classlib/impl/tz/ZoneInfoCompiler.java new file mode 100644 index 000000000..3ff358357 --- /dev/null +++ b/teavm-classlib/src/main/java/org/teavm/classlib/impl/tz/ZoneInfoCompiler.java @@ -0,0 +1,650 @@ +/* + * Copyright 2001-2013 Stephen Colebourne + * + * 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 org.teavm.classlib.impl.tz; + +import java.io.BufferedReader; +import java.io.IOException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.StringTokenizer; +import java.util.TreeMap; +import org.joda.time.Chronology; +import org.joda.time.DateTime; +import org.joda.time.DateTimeField; +import org.joda.time.LocalDate; +import org.joda.time.MutableDateTime; +import org.joda.time.chrono.ISOChronology; +import org.joda.time.chrono.LenientChronology; +import org.joda.time.format.DateTimeFormatter; +import org.joda.time.format.ISODateTimeFormat; + +/** + * Compiles IANA ZoneInfo database files into binary files for each time zone + * in the database. {@link DateTimeZoneBuilder} is used to construct and encode + * compiled data files. {@link ZoneInfoProvider} loads the encoded files and + * converts them back into {@link DateTimeZone} objects. + *

+ * Although this tool is similar to zic, the binary formats are not + * compatible. The latest IANA time zone database files may be obtained + * here. + *

+ * ZoneInfoCompiler is mutable and not thread-safe, although the main method + * may be safely invoked by multiple threads. + * + * @author Brian S O'Neill + * @since 1.0 + */ +public class ZoneInfoCompiler { + static DateTimeOfYear cStartOfYear; + + static Chronology cLenientISO; + + static DateTimeOfYear getStartOfYear() { + if (cStartOfYear == null) { + cStartOfYear = new DateTimeOfYear(); + } + return cStartOfYear; + } + + static Chronology getLenientISOChronology() { + if (cLenientISO == null) { + cLenientISO = LenientChronology.getInstance(ISOChronology.getInstanceUTC()); + } + return cLenientISO; + } + + static int parseYear(String str, int def) { + str = str.toLowerCase(); + if (str.equals("minimum") || str.equals("min")) { + return Integer.MIN_VALUE; + } else if (str.equals("maximum") || str.equals("max")) { + return Integer.MAX_VALUE; + } else if (str.equals("only")) { + return def; + } + return Integer.parseInt(str); + } + + static int parseMonth(String str) { + DateTimeField field = ISOChronology.getInstanceUTC().monthOfYear(); + return field.get(field.set(0, str, Locale.ENGLISH)); + } + + static int parseDayOfWeek(String str) { + DateTimeField field = ISOChronology.getInstanceUTC().dayOfWeek(); + return field.get(field.set(0, str, Locale.ENGLISH)); + } + + static String parseOptional(String str) { + return (str.equals("-")) ? null : str; + } + + static int parseTime(String str) { + DateTimeFormatter p = ISODateTimeFormat.hourMinuteSecondFraction(); + MutableDateTime mdt = new MutableDateTime(0, getLenientISOChronology()); + int pos = 0; + if (str.startsWith("-")) { + pos = 1; + } + int newPos = p.parseInto(mdt, str, pos); + if (newPos == ~pos) { + throw new IllegalArgumentException(str); + } + int millis = (int)mdt.getMillis(); + if (pos == 1) { + millis = -millis; + } + return millis; + } + + static char parseZoneChar(char c) { + switch (c) { + case 's': case 'S': + // Standard time + return 's'; + case 'u': case 'U': case 'g': case 'G': case 'z': case 'Z': + // UTC + return 'u'; + case 'w': case 'W': default: + // Wall time + return 'w'; + } + } + + /** + * @return false if error. + */ + static boolean test(String id, DateTimeZone tz) { + if (!id.equals(tz.getID())) { + return true; + } + + // Test to ensure that reported transitions are not duplicated. + + long millis = ISOChronology.getInstanceUTC().year().set(0, 1850); + long end = ISOChronology.getInstanceUTC().year().set(0, 2050); + + int offset = tz.getOffset(millis); + + List transitions = new ArrayList<>(); + + while (true) { + long next = tz.nextTransition(millis); + if (next == millis || next > end) { + break; + } + + millis = next; + + int nextOffset = tz.getOffset(millis); + + if (offset == nextOffset) { + System.out.println("*d* Error in " + tz.getID() + " " + + new DateTime(millis, + ISOChronology.getInstanceUTC())); + return false; + } + + + transitions.add(Long.valueOf(millis)); + + offset = nextOffset; + } + + // Now verify that reverse transitions match up. + + millis = ISOChronology.getInstanceUTC().year().set(0, 2050); + end = ISOChronology.getInstanceUTC().year().set(0, 1850); + + for (int i=transitions.size(); --i>= 0; ) { + long prev = tz.previousTransition(millis); + if (prev == millis || prev < end) { + break; + } + + millis = prev; + + long trans = transitions.get(i).longValue(); + + if (trans - 1 != millis) { + System.out.println("*r* Error in " + tz.getID() + " " + + new DateTime(millis, + ISOChronology.getInstanceUTC()) + " != " + + new DateTime(trans - 1, + ISOChronology.getInstanceUTC())); + + return false; + } + } + + return true; + } + + // Maps names to RuleSets. + private Map iRuleSets; + + // List of Zone objects. + private List iZones; + + // List String pairs to link. + private List iGoodLinks; + + // List String pairs to link. + private List iBackLinks; + + public ZoneInfoCompiler() { + iRuleSets = new HashMap<>(); + iZones = new ArrayList<>(); + iGoodLinks = new ArrayList<>(); + iBackLinks = new ArrayList<>(); + } + + public Map compile() { + Map map = new TreeMap<>(); + Map sourceMap = new TreeMap<>(); + + // write out the standard entries + for (int i = 0; i < iZones.size(); i++) { + Zone zone = iZones.get(i); + DateTimeZoneBuilder builder = new DateTimeZoneBuilder(); + zone.addToBuilder(builder, iRuleSets); + StorableDateTimeZone tz = builder.toDateTimeZone(zone.iName, true); + if (test(tz.getID(), tz)) { + map.put(tz.getID(), tz); + sourceMap.put(tz.getID(), zone); + } + } + + // revive zones from "good" links + for (int i = 0; i < iGoodLinks.size(); i += 2) { + String baseId = iGoodLinks.get(i); + String alias = iGoodLinks.get(i + 1); + Zone sourceZone = sourceMap.get(baseId); + if (sourceZone == null) { + throw new RuntimeException("Cannot find source zone '" + baseId + "' to link alias '" + + alias + "' to"); + } else { + DateTimeZoneBuilder builder = new DateTimeZoneBuilder(); + sourceZone.addToBuilder(builder, iRuleSets); + StorableDateTimeZone revived = builder.toDateTimeZone(alias, true); + if (test(revived.getID(), revived)) { + map.put(revived.getID(), revived); + } + map.put(revived.getID(), revived); + } + } + + // store "back" links as aliases (where name is permanently mapped + for (int pass = 0; pass < 2; pass++) { + for (int i = 0; i < iBackLinks.size(); i += 2) { + String id = iBackLinks.get(i); + String alias = iBackLinks.get(i + 1); + StorableDateTimeZone tz = map.get(id); + if (tz == null) { + if (pass > 0) { + throw new RuntimeException("Cannot find time zone '" + id + "' to link alias '" + + alias + "' to"); + } + } else { + map.put(alias, tz); + } + } + } + + return map; + } + + public void parseDataFile(BufferedReader in, boolean backward) throws IOException { + Zone zone = null; + String line; + while ((line = in.readLine()) != null) { + String trimmed = line.trim(); + if (trimmed.length() == 0 || trimmed.charAt(0) == '#') { + continue; + } + + int index = line.indexOf('#'); + if (index >= 0) { + line = line.substring(0, index); + } + + //System.out.println(line); + + StringTokenizer st = new StringTokenizer(line, " \t"); + + if (Character.isWhitespace(line.charAt(0)) && st.hasMoreTokens()) { + if (zone != null) { + // Zone continuation + zone.chain(st); + } + continue; + } else { + if (zone != null) { + iZones.add(zone); + } + zone = null; + } + + if (st.hasMoreTokens()) { + String token = st.nextToken(); + if (token.equalsIgnoreCase("Rule")) { + Rule r = new Rule(st); + RuleSet rs = iRuleSets.get(r.iName); + if (rs == null) { + rs = new RuleSet(r); + iRuleSets.put(r.iName, rs); + } else { + rs.addRule(r); + } + } else if (token.equalsIgnoreCase("Zone")) { + zone = new Zone(st); + } else if (token.equalsIgnoreCase("Link")) { + String real = st.nextToken(); + String alias = st.nextToken(); + // links in "backward" are deprecated names + // links in other files should be kept + // special case a few to try to repair terrible damage to tzdb + if (backward || alias.equals("US/Pacific-New") || alias.startsWith("Etc/") || alias.equals("GMT")) { + iBackLinks.add(real); + iBackLinks.add(alias); + } else { + iGoodLinks.add(real); + iGoodLinks.add(alias); + } + } else { + System.out.println("Unknown line: " + line); + } + } + } + + if (zone != null) { + iZones.add(zone); + } + } + + static class DateTimeOfYear { + public final int iMonthOfYear; + public final int iDayOfMonth; + public final int iDayOfWeek; + public final boolean iAdvanceDayOfWeek; + public final int iMillisOfDay; + public final char iZoneChar; + + DateTimeOfYear() { + iMonthOfYear = 1; + iDayOfMonth = 1; + iDayOfWeek = 0; + iAdvanceDayOfWeek = false; + iMillisOfDay = 0; + iZoneChar = 'w'; + } + + DateTimeOfYear(StringTokenizer st) { + int month = 1; + int day = 1; + int dayOfWeek = 0; + int millis = 0; + boolean advance = false; + char zoneChar = 'w'; + + if (st.hasMoreTokens()) { + month = parseMonth(st.nextToken()); + + if (st.hasMoreTokens()) { + String str = st.nextToken(); + if (str.startsWith("last")) { + day = -1; + dayOfWeek = parseDayOfWeek(str.substring(4)); + advance = false; + } else { + try { + day = Integer.parseInt(str); + dayOfWeek = 0; + advance = false; + } catch (NumberFormatException e) { + int index = str.indexOf(">="); + if (index > 0) { + day = Integer.parseInt(str.substring(index + 2)); + dayOfWeek = parseDayOfWeek(str.substring(0, index)); + advance = true; + } else { + index = str.indexOf("<="); + if (index > 0) { + day = Integer.parseInt(str.substring(index + 2)); + dayOfWeek = parseDayOfWeek(str.substring(0, index)); + advance = false; + } else { + throw new IllegalArgumentException(str); + } + } + } + } + + if (st.hasMoreTokens()) { + str = st.nextToken(); + zoneChar = parseZoneChar(str.charAt(str.length() - 1)); + if (str.equals("24:00")) { + // handle end of year + if (month == 12 && day == 31) { + millis = parseTime("23:59:59.999"); + } else { + LocalDate date = (day == -1 ? + new LocalDate(2001, month, 1).plusMonths(1) : + new LocalDate(2001, month, day).plusDays(1)); + advance = (day != -1 && dayOfWeek != 0); + month = date.getMonthOfYear(); + day = date.getDayOfMonth(); + if (dayOfWeek != 0) { + dayOfWeek = ((dayOfWeek - 1 + 1) % 7) + 1; + } + } + } else { + millis = parseTime(str); + } + } + } + } + + iMonthOfYear = month; + iDayOfMonth = day; + iDayOfWeek = dayOfWeek; + iAdvanceDayOfWeek = advance; + iMillisOfDay = millis; + iZoneChar = zoneChar; + } + + /** + * Adds a recurring savings rule to the builder. + */ + public void addRecurring(DateTimeZoneBuilder builder, int saveMillis, int fromYear, int toYear) + { + builder.addRecurringSavings(saveMillis, + fromYear, toYear, + iZoneChar, + iMonthOfYear, + iDayOfMonth, + iDayOfWeek, + iAdvanceDayOfWeek, + iMillisOfDay); + } + + /** + * Adds a cutover to the builder. + */ + public void addCutover(DateTimeZoneBuilder builder, int year) { + builder.addCutover(year, + iZoneChar, + iMonthOfYear, + iDayOfMonth, + iDayOfWeek, + iAdvanceDayOfWeek, + iMillisOfDay); + } + + @Override + public String toString() { + return + "MonthOfYear: " + iMonthOfYear + "\n" + + "DayOfMonth: " + iDayOfMonth + "\n" + + "DayOfWeek: " + iDayOfWeek + "\n" + + "AdvanceDayOfWeek: " + iAdvanceDayOfWeek + "\n" + + "MillisOfDay: " + iMillisOfDay + "\n" + + "ZoneChar: " + iZoneChar + "\n"; + } + } + + private static class Rule { + public final String iName; + public final int iFromYear; + public final int iToYear; + public final String iType; + public final DateTimeOfYear iDateTimeOfYear; + public final int iSaveMillis; + public final String iLetterS; + + Rule(StringTokenizer st) { + iName = st.nextToken().intern(); + iFromYear = parseYear(st.nextToken(), 0); + iToYear = parseYear(st.nextToken(), iFromYear); + if (iToYear < iFromYear) { + throw new IllegalArgumentException(); + } + iType = parseOptional(st.nextToken()); + iDateTimeOfYear = new DateTimeOfYear(st); + iSaveMillis = parseTime(st.nextToken()); + iLetterS = parseOptional(st.nextToken()); + } + + /** + * Adds a recurring savings rule to the builder. + */ + public void addRecurring(DateTimeZoneBuilder builder) { + iDateTimeOfYear.addRecurring(builder, iSaveMillis, iFromYear, iToYear); + } + + @Override + public String toString() { + return + "[Rule]\n" + + "Name: " + iName + "\n" + + "FromYear: " + iFromYear + "\n" + + "ToYear: " + iToYear + "\n" + + "Type: " + iType + "\n" + + iDateTimeOfYear + + "SaveMillis: " + iSaveMillis + "\n" + + "LetterS: " + iLetterS + "\n"; + } + } + + private static class RuleSet { + private List iRules; + + RuleSet(Rule rule) { + iRules = new ArrayList<>(); + iRules.add(rule); + } + + void addRule(Rule rule) { + if (!(rule.iName.equals(iRules.get(0).iName))) { + throw new IllegalArgumentException("Rule name mismatch"); + } + iRules.add(rule); + } + + /** + * Adds recurring savings rules to the builder. + */ + public void addRecurring(DateTimeZoneBuilder builder) { + for (int i=0; i ruleSets) { + addToBuilder(this, builder, ruleSets); + } + + private static void addToBuilder(Zone zone, + DateTimeZoneBuilder builder, + Map ruleSets) + { + for (; zone != null; zone = zone.iNext) { + builder.setStandardOffset(zone.iOffsetMillis); + + if (zone.iRules == null) { + builder.setFixedSavings(zone.iFormat, 0); + } else { + try { + // Check if iRules actually just refers to a savings. + int saveMillis = parseTime(zone.iRules); + builder.setFixedSavings(zone.iFormat, saveMillis); + } + catch (Exception e) { + RuleSet rs = ruleSets.get(zone.iRules); + if (rs == null) { + throw new IllegalArgumentException + ("Rules not found: " + zone.iRules); + } + rs.addRecurring(builder); + } + } + + if (zone.iUntilYear == Integer.MAX_VALUE) { + break; + } + + zone.iUntilDateTimeOfYear.addCutover(builder, zone.iUntilYear); + } + } + + @Override + public String toString() { + String str = + "[Zone]\n" + + "Name: " + iName + "\n" + + "OffsetMillis: " + iOffsetMillis + "\n" + + "Rules: " + iRules + "\n" + + "Format: " + iFormat + "\n" + + "UntilYear: " + iUntilYear + "\n" + + iUntilDateTimeOfYear; + + if (iNext == null) { + return str; + } + + return str + "...\n" + iNext.toString(); + } + } +} + diff --git a/teavm-classlib/src/main/resources/org/teavm/classlib/impl/tz/tzdata2015d.zip b/teavm-classlib/src/main/resources/org/teavm/classlib/impl/tz/tzdata2015d.zip new file mode 100644 index 000000000..ef9455803 Binary files /dev/null and b/teavm-classlib/src/main/resources/org/teavm/classlib/impl/tz/tzdata2015d.zip differ