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:
+ *
+ * - UTC - The reference time.
+ *
- Standard Time - The local time without a daylight saving time offset.
+ * For example, in Paris, standard time is UTC+01:00.
+ *
- Daylight Saving Time - The local time with a daylight saving time
+ * offset. This offset is typically one hour, but not always. It is typically
+ * used in most countries away from the equator. In Paris, daylight saving
+ * time is UTC+02:00.
+ *
- Wall Time - This is what a local clock on the wall reads. This will be
+ * either Standard Time or Daylight Saving Time depending on the time of year
+ * and whether the location uses Daylight Saving Time.
+ *
+ *
+ * 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:
+ *
+ * - load the specific {@link Provider} specified by the system property
+ * {@code org.joda.time.DateTimeZone.Provider}.
+ *
- load {@link ZoneInfoProvider} using the data in the filing system folder
+ * pointed to by system property {@code org.joda.time.DateTimeZone.Folder}.
+ *
- load {@link ZoneInfoProvider} using the data in the classpath location
+ * {@code org/joda/time/tz/data}.
+ *
- 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