/*
* Copyright 2001-2006 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.goda.time;
import com.google.gwt.core.client.GWT;
import java.io.Serializable;
import java.util.Date;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import org.goda.time.chrono.BaseChronology;
import org.goda.time.chrono.ISOChronology;
import org.goda.time.field.FieldUtils;
import org.goda.time.format.DateTimeFormat;
import org.goda.time.format.DateTimeFormatter;
import org.goda.time.format.DateTimeFormatterBuilder;
import org.goda.time.format.FormatUtils;
import org.goda.time.tz.DefaultNameProvider;
import org.goda.time.tz.FixedDateTimeZone;
import org.goda.time.tz.NameProvider;
import org.goda.time.tz.Provider;
import org.goda.time.tz.UTCProvider;
/**
* 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 folowing 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>
* 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 implements Serializable {
/** Serialization version. */
private static final long serialVersionUID = 5546345482340108586L;
/** The time zone for Universal Coordinated Time */
public static final DateTimeZone UTC = new FixedDateTimeZone("UTC", "UTC", 0, 0);
/** The instance that is providing time zones. */
private static Provider cProvider;
/** The instance that is providing time zone names. */
private static NameProvider cNameProvider;
/** The set of ID strings. */
private static Set cAvailableIDs;
/** The default time zone. */
private static DateTimeZone cDefault;
/** A formatter for printing and parsing zones. */
private static DateTimeFormatter cOffsetFormatter;
/** Cache that maps fixed offset strings to softly referenced DateTimeZones */
private static Map<String, DateTimeZone> iFixedOffsetCache;
/** Cache of old zone IDs to new zone IDs */
private static Map<String, String> cZoneIdConversion;
static {
setProvider0(null);
setNameProvider0(null);
try {
// try {
// cDefault = forID(System.getProperty("user.timezone"));
// } catch (RuntimeException ex) {
// // ignored
// }
if (cDefault == null) {
///
if(GWT.isClient()){
cDefault = forTimeZone(
com.google.gwt.i18n.client.DateTimeFormat
.getFormat( "zzz" )
.format(new Date()));
} else {
cDefault = forTimeZone("EDT") ;//TODO change this
}
}
} catch (IllegalArgumentException ex) {
// ignored
}
if (cDefault == null) {
cDefault = UTC;
}
}
//-----------------------------------------------------------------------
/**
* Gets the default time zone.
*
* @return the default datetime zone object
*/
public static DateTimeZone getDefault() {
return cDefault;
}
/**
* Sets the default time zone.
*
* @param zone the default datetime zone object, must not be null
* @throws IllegalArgumentException if the zone is null
* @throws SecurityException if the application has insufficient security rights
*/
public static void setDefault(DateTimeZone zone) { //throws SecurityException {
// SecurityManager sm = System.getSecurityManager();
// if (sm != null) {
// sm.checkPermission(new JodaTimePermission("DateTimeZone.setDefault"));
// }
// if (zone == null) {
// throw new IllegalArgumentException("The datetime zone must not be null");
// }
cDefault = zone;
}
//-----------------------------------------------------------------------
/**
* Gets a time zone instance for the specified time zone id.
* <p>
* The time zone id may be one of those returned by getAvailableIDs.
* Short ids, as accepted by {@link java.util.TimeZone}, are not accepted.
* All IDs must be specified in the long format.
* The exception is UTC, which is an acceptable id.
* <p>
* Alternatively a locale independent, fixed offset, datetime zone can
* be specified. The form <code>[+-]hh:mm</code> can be used.
*
* @param id the ID of the datetime zone, null means default
* @return the DateTimeZone object for the ID
* @throws IllegalArgumentException if the ID is not recognised
*/
public static DateTimeZone forID(String id) {
if (id == null) {
return getDefault();
}
if (id.equals("UTC")) {
return DateTimeZone.UTC;
}
DateTimeZone zone = cProvider.getZone(id);
if (zone != null) {
return zone;
}
if (id.startsWith("+") || id.startsWith("-")) {
int offset = parseOffset(id);
if (offset == 0L) {
return DateTimeZone.UTC;
} else {
id = printOffset(offset);
return fixedOffsetZone(id, offset);
}
}
throw new IllegalArgumentException("The datetime zone id is not recognised: " + id);
}
/**
* Gets a time zone instance for the specified offset to UTC in hours.
* This method assumes standard length hours.
* <p>
* This factory is a convenient way of constructing zones with a fixed offset.
*
* @param hoursOffset the offset in hours from UTC
* @return the DateTimeZone object for the offset
* @throws IllegalArgumentException if the offset is too large or too small
*/
public static DateTimeZone forOffsetHours(int hoursOffset) throws IllegalArgumentException {
return forOffsetHoursMinutes(hoursOffset, 0);
}
/**
* Gets a time zone instance for the specified offset to UTC in hours and minutes.
* This method assumes 60 minutes in an hour, and standard length minutes.
* <p>
* This factory is a convenient way of constructing zones with a fixed offset.
* The minutes value is always positive and in the range 0 to 59.
* If constructed with the values (-2, 30), the resultiong zone is '-02:30'.
*
* @param hoursOffset the offset in hours from UTC
* @param minutesOffset the offset in minutes from UTC, must be between 0 and 59 inclusive
* @return the DateTimeZone object for the offset
* @throws IllegalArgumentException if the offset or minute is too large or too small
*/
public static DateTimeZone forOffsetHoursMinutes(int hoursOffset, int minutesOffset) throws IllegalArgumentException {
if (hoursOffset == 0 && minutesOffset == 0) {
return DateTimeZone.UTC;
}
if (minutesOffset < 0 || minutesOffset > 59) {
throw new IllegalArgumentException("Minutes out of range: " + minutesOffset);
}
int offset = 0;
try {
int hoursInMinutes = FieldUtils.safeMultiply(hoursOffset, 60);
if (hoursInMinutes < 0) {
minutesOffset = FieldUtils.safeAdd(hoursInMinutes, -minutesOffset);
} else {
minutesOffset = FieldUtils.safeAdd(hoursInMinutes, minutesOffset);
}
offset = FieldUtils.safeMultiply(minutesOffset, DateTimeConstants.MILLIS_PER_MINUTE);
} catch (ArithmeticException ex) {
throw new IllegalArgumentException("Offset is too large");
}
return forOffsetMillis(offset);
}
/**
* Gets a time zone instance for the specified offset to UTC in milliseconds.
*
* @param millisOffset the offset in millis from UTC
* @return the DateTimeZone object for the offset
*/
public static DateTimeZone forOffsetMillis(int millisOffset) {
String id = printOffset(millisOffset);
return fixedOffsetZone(id, millisOffset);
}
/**
* Gets a time zone instance for a JDK TimeZone.
* <p>
* DateTimeZone only accepts a subset of the IDs from TimeZone. The
* excluded IDs are the short three letter form (except UTC). This
* method will attempt to convert between time zones created using the
* short IDs and the full version.
* <p>
* This method is not designed to parse time zones with rules created by
* applications using <code>SimpleTimeZone</code> directly.
*
* @param zone the zone to convert, null means default
* @return the DateTimeZone object for the zone
* @throws IllegalArgumentException if the zone is not recognised
*/
public static DateTimeZone forTimeZone(final String id) {
// if (zone == null) {
// return getDefault();
// }
// final String id = zone.getID();
if (id.equals("UTC")) {
return DateTimeZone.UTC;
}
// Convert from old alias before consulting provider since they may differ.
DateTimeZone dtz = null;
String convId = getConvertedId(id);
if (convId != null) {
dtz = cProvider.getZone(convId);
}
if (dtz == null) {
dtz = cProvider.getZone(id);
}
if (dtz != null) {
return dtz;
}
// Support GMT+/-hh:mm formats
if (convId == null) {
convId = ""; // TODO zone.getDisplayName();
if (convId.startsWith("GMT+") || convId.startsWith("GMT-")) {
convId = convId.substring(3);
int offset = parseOffset(convId);
if (offset == 0L) {
return DateTimeZone.UTC;
} else {
convId = printOffset(offset);
return fixedOffsetZone(convId, offset);
}
}
}
throw new IllegalArgumentException("The datetime zone id is not recognised: " + id);
}
//-----------------------------------------------------------------------
/**
* Gets the zone using a fixed offset amount.
*
* @param id the zone id
* @param offset the offset in millis
* @return the zone
*/
private static synchronized DateTimeZone fixedOffsetZone(String id, int offset) {
if (offset == 0) {
return DateTimeZone.UTC;
}
if (iFixedOffsetCache == null) {
iFixedOffsetCache = new HashMap<String, DateTimeZone>();
}
DateTimeZone zone = (DateTimeZone) iFixedOffsetCache.get(id);
if (zone == null) {
zone = new FixedDateTimeZone(id, null, offset, offset);
}
iFixedOffsetCache.put(id, zone);
return zone;
}
/**
* Gets all the available IDs supported.
*
* @return an unmodifiable Set of String IDs
*/
public static Set getAvailableIDs() {
return cAvailableIDs;
}
//-----------------------------------------------------------------------
/**
* Gets the zone provider factory.
* <p>
* The zone provider is a pluggable instance factory that supplies the
* actual instances of DateTimeZone.
*
* @return the provider
*/
public static Provider getProvider() {
return cProvider;
}
/**
* Sets the zone provider factory.
* <p>
* The zone provider is a pluggable instance factory that supplies the
* actual instances of DateTimeZone.
*
* @param provider provider to use, or null for default
* @throws SecurityException if you do not have the permission DateTimeZone.setProvider
* @throws IllegalArgumentException if the provider is invalid
*/
public static void setProvider(Provider provider) { //throws SecurityException {
// SecurityManager sm = System.getSecurityManager();
// if (sm != null) {
// sm.checkPermission(new JodaTimePermission("DateTimeZone.setProvider"));
// }
setProvider0(provider);
}
/**
* Sets the zone provider factory without performing the security check.
*
* @param provider provider to use, or null for default
* @throws IllegalArgumentException if the provider is invalid
*/
private static void setProvider0(Provider provider) {
if (provider == null) {
provider = getDefaultProvider();
}
Set ids = provider.getAvailableIDs();
if (ids == null || ids.size() == 0) {
throw new IllegalArgumentException
("The provider doesn't have any available ids");
}
if (!ids.contains("UTC")) {
throw new IllegalArgumentException("The provider doesn't support UTC");
}
if (!UTC.equals(provider.getZone("UTC"))) {
throw new IllegalArgumentException("Invalid UTC zone provided");
}
cProvider = provider;
cAvailableIDs = ids;
}
/**
* Gets the default zone provider.
* <p>
* Tries the system property <code>org.joda.time.DateTimeZone.Provider</code>.
* Then tries a <code>ZoneInfoProvider</code> using the data in <code>org/joda/time/tz/data</code>.
* Then uses <code>UTCProvider</code>.
*
* @return the default name provider
*/
private static Provider getDefaultProvider() {
Provider provider = null;
// try {
// String providerClass =
// System.getProperty("org.joda.time.DateTimeZone.Provider");
// if (providerClass != null) {
// try {
// provider = (Provider) Class.forName(providerClass).newInstance();
// } catch (Exception ex) {
// Thread thread = Thread.currentThread();
// thread.getThreadGroup().uncaughtException(thread, ex);
// }
// }
// } catch (SecurityException ex) {
// // ignored
// }
// if (provider == null) {
// try {
// provider = new ZoneInfoProvider("org/joda/time/tz/data");
// } catch (Exception ex) {
// //Thread thread = Thread.currentThread();
// // thread.getThreadGroup().uncaughtException(thread, ex);
// throw new RuntimeException(ex);
// }
// }
if (provider == null) {
provider = new UTCProvider();
}
return provider;
}
//-----------------------------------------------------------------------
/**
* Gets the name provider factory.
* <p>
* The name provider is a pluggable instance factory that supplies the
* names of each DateTimeZone.
*
* @return the provider
*/
public static NameProvider getNameProvider() {
return cNameProvider;
}
/**
* Sets the name provider factory.
* <p>
* The name provider is a pluggable instance factory that supplies the
* names of each DateTimeZone.
*
* @param nameProvider provider to use, or null for default
* @throws SecurityException if you do not have the permission DateTimeZone.setNameProvider
* @throws IllegalArgumentException if the provider is invalid
*/
public static void setNameProvider(NameProvider nameProvider) { // throws SecurityException {
// SecurityManager sm = System.getSecurityManager();
// if (sm != null) {
// sm.checkPermission(new JodaTimePermission("DateTimeZone.setNameProvider"));
// }
setNameProvider0(nameProvider);
}
/**
* Sets the name provider factory without performing the security check.
*
* @param nameProvider provider to use, or null for default
* @throws IllegalArgumentException if the provider is invalid
*/
private static void setNameProvider0(NameProvider nameProvider) {
if (nameProvider == null) {
nameProvider = getDefaultNameProvider();
}
cNameProvider = nameProvider;
}
/**
* Gets the default name provider.
* <p>
* Tries the system property <code>org.joda.time.DateTimeZone.NameProvider</code>.
* Then uses <code>DefaultNameProvider</code>.
*
* @return the default name provider
*/
private static NameProvider getDefaultNameProvider() {
NameProvider nameProvider = null;
// try {
// String providerClass = System.getProperty("org.joda.time.DateTimeZone.NameProvider");
// if (providerClass != null) {
// try {
// nameProvider = (NameProvider) Class.forName(providerClass).newInstance();
// } catch (Exception ex) {
// Thread thread = Thread.currentThread();
// thread.getThreadGroup().uncaughtException(thread, ex);
// }
// }
// } catch (SecurityException ex) {
// // ignore
// }
if (nameProvider == null) {
nameProvider = new DefaultNameProvider();
}
return nameProvider;
}
//-----------------------------------------------------------------------
/**
* Converts an old style id to a new style id.
*
* @param id the old style id
* @return the new style id, null if not found
*/
private static synchronized String getConvertedId(String id) {
Map<String, String> map = cZoneIdConversion;
if (map == null) {
// Backwards compatibility with TimeZone.
map = new HashMap<String, String>();
map.put("GMT", "UTC");
map.put("MIT", "Pacific/Apia");
map.put("HST", "Pacific/Honolulu");
map.put("AST", "America/Anchorage");
map.put("PST", "America/Los_Angeles");
map.put("MST", "America/Denver");
map.put("PNT", "America/Phoenix");
map.put("CST", "America/Chicago");
map.put("EST", "America/New_York");
map.put("IET", "America/Indianapolis");
map.put("PRT", "America/Puerto_Rico");
map.put("CNT", "America/St_Johns");
map.put("AGT", "America/Buenos_Aires");
map.put("BET", "America/Sao_Paulo");
map.put("WET", "Europe/London");
map.put("ECT", "Europe/Paris");
map.put("ART", "Africa/Cairo");
map.put("CAT", "Africa/Harare");
map.put("EET", "Europe/Bucharest");
map.put("EAT", "Africa/Addis_Ababa");
map.put("MET", "Asia/Tehran");
map.put("NET", "Asia/Yerevan");
map.put("PLT", "Asia/Karachi");
map.put("IST", "Asia/Calcutta");
map.put("BST", "Asia/Dhaka");
map.put("VST", "Asia/Saigon");
map.put("CTT", "Asia/Shanghai");
map.put("JST", "Asia/Tokyo");
map.put("ACT", "Australia/Darwin");
map.put("AET", "Australia/Sydney");
map.put("SST", "Pacific/Guadalcanal");
map.put("NST", "Pacific/Auckland");
cZoneIdConversion = map;
}
return (String) map.get(id);
}
private static int parseOffset(String str) {
Chronology chrono;
if (cDefault != null) {
chrono = ISOChronology.getInstanceUTC();
} else {
// Can't use a real chronology if called during class
// initialization. Offset parser doesn't need it anyhow.
chrono = new BaseChronology() {
public DateTimeZone getZone() {
return null;
}
public Chronology withUTC() {
return this;
}
public Chronology withZone(DateTimeZone zone) {
return this;
}
public String toString() {
return getClass().getName();
}
};
}
return -(int) offsetFormatter().withChronology(chrono).parseMillis(str);
}
/**
* Formats a timezone offset string.
* <p>
* This method is kept separate from the formatting classes to speed and
* simplify startup and classloading.
*
* @param offset the offset in milliseconds
* @return the time zone string
*/
private static String printOffset(int offset) {
StringBuffer buf = new StringBuffer();
if (offset >= 0) {
buf.append('+');
} else {
buf.append('-');
offset = -offset;
}
int hours = offset / DateTimeConstants.MILLIS_PER_HOUR;
FormatUtils.appendPaddedInteger(buf, hours, 2);
offset -= hours * (int) DateTimeConstants.MILLIS_PER_HOUR;
int minutes = offset / DateTimeConstants.MILLIS_PER_MINUTE;
buf.append(':');
FormatUtils.appendPaddedInteger(buf, minutes, 2);
offset -= minutes * DateTimeConstants.MILLIS_PER_MINUTE;
if (offset == 0) {
return buf.toString();
}
int seconds = offset / DateTimeConstants.MILLIS_PER_SECOND;
buf.append(':');
FormatUtils.appendPaddedInteger(buf, seconds, 2);
offset -= seconds * DateTimeConstants.MILLIS_PER_SECOND;
if (offset == 0) {
return buf.toString();
}
buf.append('.');
FormatUtils.appendPaddedInteger(buf, offset, 3);
return buf.toString();
}
/**
* Gets a printer/parser for managing the offset id formatting.
*
* @return the formatter
*/
private static synchronized DateTimeFormatter offsetFormatter() {
if (cOffsetFormatter == null) {
cOffsetFormatter = new DateTimeFormatterBuilder()
.appendTimeZoneOffset(null, true, 2, 4)
.toFormatter();
}
return cOffsetFormatter;
}
// 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;
}
/**
* Returns a non-localized name that is unique to this time zone. It can be
* combined with id to form a unique key for fetching localized names.
*
* @param instant milliseconds from 1970-01-01T00:00:00Z to get the name for
* @return name key or null if id should be used for names
*/
public abstract String getNameKey(long instant);
/**
* Gets the short name of this datetime zone suitable for display using
* the default locale.
* <p>
* If the name is not available for the locale, then this method returns a
* string in the format <code>[+-]hh:mm</code>.
*
* @param instant milliseconds from 1970-01-01T00:00:00Z to get the name for
* @return the human-readable short name in the default locale
*/
public final String getShortName(long instant) {
return getShortName(instant, null);
}
/**
* Gets the short name of this datetime zone suitable for display using
* the specified locale.
* <p>
* If the name is not available for the locale, then this method returns a
* string in the format <code>[+-]hh:mm</code>.
*
* @param instant milliseconds from 1970-01-01T00:00:00Z to get the name for
* @param locale the locale to get the name for
* @return the human-readable short name in the specified locale
*/
public String getShortName(long instant, Locale locale) {
if (locale == null) {
locale = Locale.getDefault();
}
String nameKey = getNameKey(instant);
if (nameKey == null) {
return iID;
}
String name = cNameProvider.getShortName(locale, iID, nameKey);
if (name != null) {
return name;
}
return printOffset(getOffset(instant));
}
/**
* Gets the long name of this datetime zone suitable for display using
* the default locale.
* <p>
* If the name is not available for the locale, then this method returns a
* string in the format <code>[+-]hh:mm</code>.
*
* @param instant milliseconds from 1970-01-01T00:00:00Z to get the name for
* @return the human-readable long name in the default locale
*/
public final String getName(long instant) {
return getName(instant, null);
}
/**
* Gets the long name of this datetime zone suitable for display using
* the specified locale.
* <p>
* If the name is not available for the locale, then this method returns a
* string in the format <code>[+-]hh:mm</code>.
*
* @param instant milliseconds from 1970-01-01T00:00:00Z to get the name for
* @param locale the locale to get the name for
* @return the human-readable long name in the specified locale
*/
public String getName(long instant, Locale locale) {
if (locale == null) {
locale = Locale.getDefault();
}
String nameKey = getNameKey(instant);
if (nameKey == null) {
return iID;
}
String name = cNameProvider.getName(locale, iID, nameKey);
if (name != null) {
return name;
}
return printOffset(getOffset(instant));
}
/**
* 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 millisecond offset to add to UTC to get local time.
*
* @param instant instant to get the offset for, null means now
* @return the millisecond offset to add to UTC to get local time
*/
public final int getOffset(ReadableInstant instant) {
if (instant == null) {
return getOffset(DateTimeUtils.currentTimeMillis());
}
return getOffset(instant.getMillis());
}
/**
* 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>
* This method will return an offset suitable for calculating an instant
* after any DST gap. For example, consider a zone with a cutover
* from 01:00 to 01:59:<br />
* Input: 00:00 Output: 00:00<br />
* Input: 00:30 Output: 00:30<br />
* Input: 01:00 Output: 02:00<br />
* Input: 01:30 Output: 02:30<br />
* Input: 02:00 Output: 02:00<br />
* Input: 02:30 Output: 02:30<br />
* <p>
* NOTE: The behaviour of this method changed in v1.5, with the emphasis
* on returning a consistent result later along the time-line (shown above).
*
* @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)
int offsetLocal = getOffset(instantLocal);
// adjust instantLocal using the estimate and recalc the offset
int offsetAdjusted = getOffset(instantLocal - offsetLocal);
// 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(instantLocal - offsetLocal);
long nextAdjusted = nextTransition(instantLocal - offsetAdjusted);
if (nextLocal != nextAdjusted) {
return offsetLocal;
}
}
}
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. 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 IllegalArgumentException if the zone has no eqivalent 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);
long nextAdjusted = nextTransition(instantLocal - offset);
if (nextLocal != nextAdjusted) {
// yes we are in the DST gap
if (strict) {
// DST gap is not acceptable
throw new IllegalArgumentException("Illegal instant due to time zone offset transition: " +
DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss.SSS").print(new Instant(instantLocal)) +
" (" + 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 == null) {
newZone = DateTimeZone.getDefault();
}
if (newZone == this) {
return oldInstant;
}
long instantLocal = oldInstant + getOffset(oldInstant);
return instantLocal - newZone.getOffsetFromLocal(instantLocal);
}
/**
* 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);
// Basic methods
//--------------------------------------------------------------------
/**
* Get the datetime zone as a {@link java.util.TimeZone}.
*
* @return the closest matching TimeZone object
*/
// public java.util.TimeZone toTimeZone() {
// return java.util.TimeZone.getTimeZone(iID);
// }
/**
* Compare this datetime zone with another.
*
* @param object the object to compare with
* @return true if equal, based on the ID and all internal rules
*/
public abstract boolean equals(Object object);
/**
* Gets a hash code compatable with equals.
*
* @return suitable hashcode
*/
public int hashCode() {
return 57 + getID().hashCode();
}
/**
* Gets the datetime zone as a string, which is simply its ID.
* @return the id of the zone
*/
public String toString() {
return getID();
}
/**
* By default, when DateTimeZones are serialized, only a "stub" object
* referring to the id is written out. When the stub is read in, it
* replaces itself with a DateTimeZone object.
* @return a stub object to go in the stream
*/
// protected Object writeReplace() throws ObjectStreamException {
// return new Stub(iID);
// }
/**
* Used to serialize DateTimeZones by id.
*/
private static final class Stub implements Serializable {
/** Serialization lock. */
private static final long serialVersionUID = -6471952376487863581L;
/** The ID of the zone. */
private transient String iID;
/**
* Constructor.
* @param id the id of the zone
*/
Stub(String id) {
iID = id;
}
// private void writeObject(ObjectOutputStream out) throws IOException {
// out.writeUTF(iID);
// }
//
// private void readObject(ObjectInputStream in) throws IOException {
// iID = in.readUTF();
// }
//
// private Object readResolve() throws ObjectStreamException {
// return forID(iID);
// }
}
}