Borrow some code from Joda Time to parse tz data and expose offsets. Add

tz data archive
This commit is contained in:
Alexey Andreev 2015-05-13 22:57:00 +03:00
parent b264e34ef8
commit 13fbf7c544
9 changed files with 2740 additions and 1 deletions

12
NOTICE
View File

@ -1,2 +1,12 @@
This product includes software developed by Alexey Andreev
(http://teavm.org).
This product includes software developed by The Apache Software This product includes software developed by The Apache Software
Foundation (http://www.apache.org/). 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/).

View File

@ -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.
* <p>
* 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);
}
}
}

View File

@ -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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* The offset differs in the summer because of daylight saving time, or DST.
* The following definitions of time are generally used:
* <ul>
* <li>UTC - The reference time.
* <li>Standard Time - The local time without a daylight saving time offset.
* For example, in Paris, standard time is UTC+01:00.
* <li>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.
* <li>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.
* </ul>
* <p>
* 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.
* <p>
* There are four approaches to loading time-zone data, which are tried in this order:
* <ol>
* <li>load the specific {@link Provider} specified by the system property
* {@code org.joda.time.DateTimeZone.Provider}.
* <li>load {@link ZoneInfoProvider} using the data in the filing system folder
* pointed to by system property {@code org.joda.time.DateTimeZone.Folder}.
* <li>load {@link ZoneInfoProvider} using the data in the classpath location
* {@code org/joda/time/tz/data}.
* <li>load {@link UTCProvider}
* </ol>
* <p>
* Unless you override the standard behaviour, the default if the third approach.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
*
* <pre>
* millisLocal == millisUTC + getOffset(millisUTC)
* millisUTC == millisLocal - getOffsetFromLocal(millisLocal)
* </pre>
*
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* For example, consider a zone with a gap from 01:00 to 01:59:<br />
* Input: 00:00 (before gap) Output: Offset applicable before gap DateTime: 00:00<br />
* Input: 00:30 (before gap) Output: Offset applicable before gap DateTime: 00:30<br />
* Input: 01:00 (in gap) Output: Offset applicable before gap DateTime: 02:00<br />
* Input: 01:30 (in gap) Output: Offset applicable before gap DateTime: 02:30<br />
* Input: 02:00 (after gap) Output: Offset applicable after gap DateTime: 02:00<br />
* Input: 02:30 (after gap) Output: Offset applicable after gap DateTime: 02:30<br />
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
// * <p>
// * 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);
}

File diff suppressed because it is too large Load Diff

View File

@ -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.
* <p>
* 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);
}
}

View File

@ -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));
}
}
}

View File

@ -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 {
}

View File

@ -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.
* <p>
* Although this tool is similar to zic, the binary formats are not
* compatible. The latest IANA time zone database files may be obtained
* <a href="http://www.iana.org/time-zones">here</a>.
* <p>
* 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<Long> 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<String, RuleSet> iRuleSets;
// List of Zone objects.
private List<Zone> iZones;
// List String pairs to link.
private List<String> iGoodLinks;
// List String pairs to link.
private List<String> iBackLinks;
public ZoneInfoCompiler() {
iRuleSets = new HashMap<>();
iZones = new ArrayList<>();
iGoodLinks = new ArrayList<>();
iBackLinks = new ArrayList<>();
}
public Map<String, StorableDateTimeZone> compile() {
Map<String, StorableDateTimeZone> map = new TreeMap<>();
Map<String, Zone> 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<Rule> 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<iRules.size(); i++) {
Rule rule = iRules.get(i);
rule.addRecurring(builder);
}
}
}
private static class Zone {
public final String iName;
public final int iOffsetMillis;
public final String iRules;
public final String iFormat;
public final int iUntilYear;
public final DateTimeOfYear iUntilDateTimeOfYear;
private Zone iNext;
Zone(StringTokenizer st) {
this(st.nextToken(), st);
}
private Zone(String name, StringTokenizer st) {
iName = name.intern();
iOffsetMillis = parseTime(st.nextToken());
iRules = parseOptional(st.nextToken());
iFormat = st.nextToken().intern();
int year = Integer.MAX_VALUE;
DateTimeOfYear dtOfYear = getStartOfYear();
if (st.hasMoreTokens()) {
year = Integer.parseInt(st.nextToken());
if (st.hasMoreTokens()) {
dtOfYear = new DateTimeOfYear(st);
}
}
iUntilYear = year;
iUntilDateTimeOfYear = dtOfYear;
}
void chain(StringTokenizer st) {
if (iNext != null) {
iNext.chain(st);
} else {
iNext = new Zone(iName, st);
}
}
/*
public DateTimeZone buildDateTimeZone(Map ruleSets) {
DateTimeZoneBuilder builder = new DateTimeZoneBuilder();
addToBuilder(builder, ruleSets);
return builder.toDateTimeZone(iName);
}
*/
/**
* Adds zone info to the builder.
*/
public void addToBuilder(DateTimeZoneBuilder builder, Map<String, RuleSet> ruleSets) {
addToBuilder(this, builder, ruleSets);
}
private static void addToBuilder(Zone zone,
DateTimeZoneBuilder builder,
Map<String, RuleSet> 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();
}
}
}