/** * Copyright (c) 2012-2016 André Bargull * Alle Rechte vorbehalten / All Rights Reserved. Use is subject to license terms. * * <https://github.com/anba/es6draft> */ package com.github.anba.es6draft.runtime.objects.date; import java.lang.ref.SoftReference; import java.util.Date; import java.util.Locale; import java.util.TimeZone; import org.joda.time.DateTimeZone; import com.ibm.icu.text.TimeZoneNames; import com.ibm.icu.util.ULocale; /** * Historic timezone names are not supported in Java. * * @see https://bugs.openjdk.java.net/browse/JDK-4255109 */ abstract class TimeZoneInfo { private static final long TODAY = System.currentTimeMillis(); private static final TimeZoneInfo INSTANCE = new JodaTimeZoneInfo(); /** * Returns the default {@link TimeZoneInfo} instance. * * @return the default instance */ public static final TimeZoneInfo getDefault() { return INSTANCE; } /** * Returns the local time. * * @param tz * the timezone * @param date * the date in milli-seconds since the start of the epoch * @return the date in local time */ public abstract long localTime(TimeZone tz, long date); /** * Returns the UTC time. * * @param tz * the timezone * @param localTime * the date in local time * @return the date in milli-seconds since the start of the epoch */ public abstract long utc(TimeZone tz, long localTime); /** * Returns {@code true} if the specified date is in daylight savings time. * * @param tz * the timezone * @param date * the date in milli-seconds since the start of the epoch * @return {@code true} if the date is in DST * * @see TimeZone#inDaylightTime(Date) */ public abstract boolean inDaylightTime(TimeZone tz, long date); /** * Returns the amount of saved daylight savings time, typically one hour. If the given date is * not in DST, this method returns {@code 0}. * * @param tz * the timezone * @param date * the date in milli-seconds since the start of the epoch * @return the amount of saved daylight savings time * * @see TimeZone#getDSTSavings() */ public abstract int getDSTSavings(TimeZone tz, long date); /** * Returns the offset from UTC in milli-seconds, including DST savings. * * @param tz * the timezone * @param date * the date in milli-seconds since the start of the epoch * @return the local offset adjusted with DST * * @see TimeZone#getOffset(long) */ public abstract int getOffset(TimeZone tz, long date); /** * Returns the current offset from UTC in milli-seconds. * * @param tz * the timezone * @return the current local offset * * @see TimeZone#getRawOffset() */ public abstract int getRawOffset(TimeZone tz); /** * Returns the offset from UTC in milli-seconds. * * @param tz * the timezone * @param date * the date in milli-seconds since the start of the epoch * @return the local offset * * @see TimeZone#getRawOffset() */ public abstract int getRawOffset(TimeZone tz, long date); /** * Returns the display name for the given time zone and date. * * @param tz * the timezone * @param date * the date in milli-seconds since the start of the epoch * @return the display name * * @see TimeZone#getDisplayName() */ public abstract String getDisplayName(TimeZone tz, long date); private static final class TimeZoneRef<T> extends SoftReference<T> { private final String id; public TimeZoneRef(String id, T referent) { super(referent); this.id = id; } public T get(String id) { if (id.equals(this.id)) { return get(); } return null; } } static final class JDKTimeZoneInfo extends TimeZoneInfo { @Override public long localTime(TimeZone tz, long date) { return date + tz.getOffset(date); } @Override public long utc(TimeZone tz, long localTime) { long date = localTime - tz.getRawOffset(); return localTime - tz.getOffset(date); } @Override public boolean inDaylightTime(TimeZone tz, long date) { return tz.inDaylightTime(new Date(date)); } @Override public int getDSTSavings(TimeZone tz, long date) { if (tz.inDaylightTime(new Date(date))) { return tz.getDSTSavings(); } return 0; } @Override public int getOffset(TimeZone tz, long date) { return tz.getOffset(date); } @Override public int getRawOffset(TimeZone tz) { return getRawOffset(tz, TODAY); } @Override public int getRawOffset(TimeZone tz, long date) { return tz.getRawOffset(); } @Override public String getDisplayName(TimeZone tz, long date) { boolean daylightSavings = tz.inDaylightTime(new Date(date)); if (!daylightSavings && tz.getOffset(date) != tz.getRawOffset()) { daylightSavings = tz.useDaylightTime(); } return tz.getDisplayName(daylightSavings, TimeZone.SHORT, Locale.US); } } static final class ICUTimeZoneInfo extends TimeZoneInfo { private static TimeZoneRef<com.ibm.icu.util.TimeZone> lastTimeZone = new TimeZoneRef<>( null, null); private static com.ibm.icu.util.TimeZone toICUTimeZone(TimeZone tz) { TimeZoneRef<com.ibm.icu.util.TimeZone> ref = lastTimeZone; String id = tz.getID(); com.ibm.icu.util.TimeZone timeZone = ref.get(id); if (timeZone != null) { return timeZone; } timeZone = com.ibm.icu.util.TimeZone.getTimeZone(id).freeze(); lastTimeZone = new TimeZoneRef<>(id, timeZone); return timeZone; } @Override public long localTime(TimeZone tz, long date) { return date + toICUTimeZone(tz).getOffset(date); } @Override public long utc(TimeZone tz, long localTime) { com.ibm.icu.util.TimeZone timeZone = toICUTimeZone(tz); long date = localTime - timeZone.getRawOffset(); return localTime - timeZone.getOffset(date); } @Override public boolean inDaylightTime(TimeZone tz, long date) { return toICUTimeZone(tz).inDaylightTime(new Date(date)); } @Override public int getDSTSavings(TimeZone tz, long date) { int[] offsets = new int[2]; toICUTimeZone(tz).getOffset(date, false, offsets); return offsets[1]; } @Override public int getOffset(TimeZone tz, long date) { return toICUTimeZone(tz).getOffset(date); } @Override public int getRawOffset(TimeZone tz) { return toICUTimeZone(tz).getRawOffset(); } @Override public int getRawOffset(TimeZone tz, long date) { int[] offsets = new int[2]; toICUTimeZone(tz).getOffset(date, false, offsets); return offsets[0]; } @Override public String getDisplayName(TimeZone tz, long date) { com.ibm.icu.util.TimeZone timeZone = toICUTimeZone(tz); int[] offsets = new int[2]; timeZone.getOffset(date, false, offsets); boolean daylightSavings = offsets[1] != 0; if (!daylightSavings && offsets[0] != timeZone.getRawOffset()) { daylightSavings = timeZone.useDaylightTime(); } TimeZoneNames.NameType nameType = daylightSavings ? TimeZoneNames.NameType.SHORT_DAYLIGHT : TimeZoneNames.NameType.SHORT_STANDARD; TimeZoneNames tzNames = TimeZoneNames.getTZDBInstance(ULocale.US); String displayName = tzNames.getDisplayName(timeZone.getID(), nameType, date); if (displayName != null) { return displayName; } return tz.getDisplayName(daylightSavings, TimeZone.SHORT, Locale.US); } } static final class JodaTimeZoneInfo extends TimeZoneInfo { // Last transition from LMT (Asia/Aden on 01 January, 1950). private static final long LAST_LMT_TRANSITION = -631162794000L; private static final long LOCAL_LAST_LMT_TRANSITION = -631152000000L; private static final boolean IGNORE_LMT = false; // Last transition from local time (Africa/Monrovia on 01 May, 1972). private static final long LAST_LOCAL_TRANSITION = 73529070000L; private static final long LOCAL_LAST_LOCAL_TRANSITION = 73526400000L; private static final boolean IGNORE_LOCAL = false; // Start of the epoch (01 January, 1970). private static final long EPOCH = 0L; private static final boolean IGNORE_LOCAL_BEFORE_EPOCH = true; static { assert !((IGNORE_LMT | IGNORE_LOCAL) & (IGNORE_LMT | IGNORE_LOCAL_BEFORE_EPOCH) & (IGNORE_LOCAL | IGNORE_LOCAL_BEFORE_EPOCH)); } private static TimeZoneRef<DateTimeZone> lastTimeZone = new TimeZoneRef<>(null, null); private static DateTimeZone toDateTimeZone(TimeZone tz) { TimeZoneRef<DateTimeZone> ref = lastTimeZone; String id = tz.getID(); DateTimeZone timeZone = ref.get(id); if (timeZone != null) { return timeZone; } timeZone = DateTimeZone.forTimeZone(tz); lastTimeZone = new TimeZoneRef<>(id, timeZone); return timeZone; } private static long toFirstDateAfterLocalMeanTime(DateTimeZone timeZone, long date) { assert date <= LAST_LMT_TRANSITION; while ("LMT".equals(timeZone.getNameKey(date))) { date = timeZone.nextTransition(date); } return date; } private static boolean isNormalizedOffset(int offset) { offset /= 1000; // Multiple of 15 or 20 minutes. return (offset % (15 * 60) == 0) || (offset % (20 * 60) == 0); } private static long toFirstDateWithNormalizedTime(DateTimeZone timeZone, long date) { assert date <= LAST_LOCAL_TRANSITION; long normalized = date; while (!isNormalizedOffset(timeZone.getOffset(normalized))) { normalized = timeZone.nextTransition(normalized); } if (normalized != date && timeZone.isStandardOffset(date) != timeZone.isStandardOffset(normalized)) { long next = timeZone.nextTransition(normalized); if (timeZone.isStandardOffset(date) == timeZone.isStandardOffset(next)) { normalized = next; } } return normalized; } @Override public long localTime(TimeZone tz, long date) { DateTimeZone timeZone = toDateTimeZone(tz); if (IGNORE_LOCAL_BEFORE_EPOCH) { if (date < EPOCH) { return date + getOffset(timeZone, date); } } else if (IGNORE_LOCAL) { if (date <= LAST_LOCAL_TRANSITION) { return date + getOffset(timeZone, date); } } else if (IGNORE_LMT) { if (date <= LAST_LMT_TRANSITION) { return date + getOffset(timeZone, date); } } return timeZone.convertUTCToLocal(date); } @Override public long utc(TimeZone tz, long localTime) { DateTimeZone timeZone = toDateTimeZone(tz); if (IGNORE_LOCAL_BEFORE_EPOCH) { if (localTime < EPOCH) { int offsetLocal = getOffset(timeZone, localTime); int offset = getOffset(timeZone, localTime - offsetLocal); return localTime - offset; } } else if (IGNORE_LOCAL) { if (localTime <= LOCAL_LAST_LOCAL_TRANSITION) { int offsetLocal = getOffset(timeZone, localTime); int offset = getOffset(timeZone, localTime - offsetLocal); return localTime - offset; } } else if (IGNORE_LMT) { if (localTime <= LOCAL_LAST_LMT_TRANSITION) { int offsetLocal = getOffset(timeZone, localTime); int offset = getOffset(timeZone, localTime - offsetLocal); return localTime - offset; } } return timeZone.convertLocalToUTC(localTime, false); } @Override public boolean inDaylightTime(TimeZone tz, long date) { DateTimeZone timeZone = toDateTimeZone(tz); if (IGNORE_LOCAL_BEFORE_EPOCH) { if (date < EPOCH) { date = toFirstDateWithNormalizedTime(timeZone, date); } } else if (IGNORE_LOCAL) { if (date <= LAST_LOCAL_TRANSITION) { date = toFirstDateWithNormalizedTime(timeZone, date); } } else if (IGNORE_LMT) { if (date <= LAST_LMT_TRANSITION) { date = toFirstDateAfterLocalMeanTime(timeZone, date); } } return !timeZone.isStandardOffset(date); } @Override public int getDSTSavings(TimeZone tz, long date) { DateTimeZone timeZone = toDateTimeZone(tz); if (IGNORE_LOCAL_BEFORE_EPOCH) { if (date < EPOCH) { date = toFirstDateWithNormalizedTime(timeZone, date); } } else if (IGNORE_LOCAL) { if (date <= LAST_LOCAL_TRANSITION) { date = toFirstDateWithNormalizedTime(timeZone, date); } } else if (IGNORE_LMT) { if (date <= LAST_LMT_TRANSITION) { date = toFirstDateAfterLocalMeanTime(timeZone, date); } } return Math.abs(timeZone.getOffset(date) - timeZone.getStandardOffset(date)); } @Override public int getOffset(TimeZone tz, long date) { return getOffset(toDateTimeZone(tz), date); } private int getOffset(DateTimeZone timeZone, long date) { if (IGNORE_LOCAL_BEFORE_EPOCH) { if (date < EPOCH) { date = toFirstDateWithNormalizedTime(timeZone, date); } } else if (IGNORE_LOCAL) { if (date <= LAST_LOCAL_TRANSITION) { date = toFirstDateWithNormalizedTime(timeZone, date); } } else if (IGNORE_LMT) { if (date <= LAST_LMT_TRANSITION) { date = toFirstDateAfterLocalMeanTime(timeZone, date); } } return timeZone.getOffset(date); } @Override public int getRawOffset(TimeZone tz) { return toDateTimeZone(tz).getStandardOffset(TODAY); } @Override public int getRawOffset(TimeZone tz, long date) { DateTimeZone timeZone = toDateTimeZone(tz); if (IGNORE_LOCAL_BEFORE_EPOCH) { if (date < EPOCH) { date = toFirstDateWithNormalizedTime(timeZone, date); } } else if (IGNORE_LOCAL) { if (date <= LAST_LOCAL_TRANSITION) { date = toFirstDateWithNormalizedTime(timeZone, date); } } else if (IGNORE_LMT) { if (date <= LAST_LMT_TRANSITION) { date = toFirstDateAfterLocalMeanTime(timeZone, date); } } return timeZone.getStandardOffset(date); } @Override public String getDisplayName(TimeZone tz, long date) { DateTimeZone timeZone = toDateTimeZone(tz); if (IGNORE_LOCAL_BEFORE_EPOCH) { if (date < EPOCH) { date = toFirstDateWithNormalizedTime(timeZone, date); } } else if (IGNORE_LOCAL) { if (date <= LAST_LOCAL_TRANSITION) { date = toFirstDateWithNormalizedTime(timeZone, date); } } else if (IGNORE_LMT) { if (date <= LAST_LMT_TRANSITION) { date = toFirstDateAfterLocalMeanTime(timeZone, date); } } String displayName = timeZone.getNameKey(date); if (displayName != null) { return displayName; } boolean daylightSavings = !timeZone.isStandardOffset(date); if (!daylightSavings && timeZone.getOffset(date) != timeZone.getStandardOffset(TODAY)) { daylightSavings = timeZone.nextTransition(date) != date; } return tz.getDisplayName(daylightSavings, TimeZone.SHORT, Locale.US); } } }