001/*
002 *  Copyright 2001-2006 Stephen Colebourne
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.joda.time;
017
018import java.io.IOException;
019import java.io.ObjectInputStream;
020import java.io.ObjectOutputStream;
021import java.io.ObjectStreamException;
022import java.io.Serializable;
023import java.lang.ref.Reference;
024import java.lang.ref.SoftReference;
025import java.util.HashMap;
026import java.util.Locale;
027import java.util.Map;
028import java.util.Set;
029import java.util.TimeZone;
030
031import org.joda.time.chrono.BaseChronology;
032import org.joda.time.field.FieldUtils;
033import org.joda.time.format.DateTimeFormat;
034import org.joda.time.format.DateTimeFormatter;
035import org.joda.time.format.DateTimeFormatterBuilder;
036import org.joda.time.format.FormatUtils;
037import org.joda.time.tz.DefaultNameProvider;
038import org.joda.time.tz.FixedDateTimeZone;
039import org.joda.time.tz.NameProvider;
040import org.joda.time.tz.Provider;
041import org.joda.time.tz.UTCProvider;
042import org.joda.time.tz.ZoneInfoProvider;
043
044/**
045 * DateTimeZone represents a time zone.
046 * <p>
047 * A time zone is a system of rules to convert time from one geographic 
048 * location to another. For example, Paris, France is one hour ahead of
049 * London, England. Thus when it is 10:00 in London, it is 11:00 in Paris.
050 * <p>
051 * All time zone rules are expressed, for historical reasons, relative to
052 * Greenwich, London. Local time in Greenwich is referred to as Greenwich Mean
053 * Time (GMT).  This is similar, but not precisely identical, to Universal 
054 * Coordinated Time, or UTC. This library only uses the term UTC.
055 * <p>
056 * Using this system, America/Los_Angeles is expressed as UTC-08:00, or UTC-07:00
057 * in the summer. The offset -08:00 indicates that America/Los_Angeles time is
058 * obtained from UTC by adding -08:00, that is, by subtracting 8 hours.
059 * <p>
060 * The offset differs in the summer because of daylight saving time, or DST.
061 * The folowing definitions of time are generally used:
062 * <ul>
063 * <li>UTC - The reference time.
064 * <li>Standard Time - The local time without a daylight saving time offset.
065 * For example, in Paris, standard time is UTC+01:00.
066 * <li>Daylight Saving Time - The local time with a daylight saving time 
067 * offset. This offset is typically one hour, but not always. It is typically
068 * used in most countries away from the equator.  In Paris, daylight saving 
069 * time is UTC+02:00.
070 * <li>Wall Time - This is what a local clock on the wall reads. This will be
071 * either Standard Time or Daylight Saving Time depending on the time of year
072 * and whether the location uses Daylight Saving Time.
073 * </ul>
074 * <p>
075 * Unlike the Java TimeZone class, DateTimeZone is immutable. It also only
076 * supports long format time zone ids. Thus EST and ECT are not accepted.
077 * However, the factory that accepts a TimeZone will attempt to convert from
078 * the old short id to a suitable long id.
079 * <p>
080 * DateTimeZone is thread-safe and immutable, and all subclasses must be as
081 * well.
082 * 
083 * @author Brian S O'Neill
084 * @author Stephen Colebourne
085 * @since 1.0
086 */
087public abstract class DateTimeZone implements Serializable {
088    
089    /** Serialization version. */
090    private static final long serialVersionUID = 5546345482340108586L;
091
092    /** The time zone for Universal Coordinated Time */
093    public static final DateTimeZone UTC = new FixedDateTimeZone("UTC", "UTC", 0, 0);
094
095    /** The instance that is providing time zones. */
096    private static Provider cProvider;
097    /** The instance that is providing time zone names. */
098    private static NameProvider cNameProvider;
099    /** The set of ID strings. */
100    private static Set cAvailableIDs;
101    /** The default time zone. */
102    private static volatile DateTimeZone cDefault;
103    /** A formatter for printing and parsing zones. */
104    private static DateTimeFormatter cOffsetFormatter;
105
106    /** Cache that maps fixed offset strings to softly referenced DateTimeZones */
107    private static Map iFixedOffsetCache;
108
109    /** Cache of old zone IDs to new zone IDs */
110    private static Map cZoneIdConversion;
111
112    static {
113        setProvider0(null);
114        setNameProvider0(null);
115    }
116
117    //-----------------------------------------------------------------------
118    /**
119     * Gets the default time zone.
120     * <p>
121     * The default time zone is derived from the system property {@code user.timezone}.
122     * If that is {@code null} or is not a valid identifier, then the value of the
123     * JDK {@code TimeZone} default is converted. If that fails, {@code UTC} is used.
124     * 
125     * @return the default datetime zone object
126     */
127    public static DateTimeZone getDefault() {
128        DateTimeZone zone = cDefault;
129        if (zone == null) {
130            synchronized(DateTimeZone.class) {
131                zone = cDefault;
132                if (zone == null) {
133                    DateTimeZone temp = null;
134                    try {
135                        try {
136                            String id = System.getProperty("user.timezone");
137                            if (id != null) {  // null check avoids stack overflow
138                                temp = forID(id);
139                            }
140                        } catch (RuntimeException ex) {
141                            // ignored
142                        }
143                        if (temp == null) {
144                            temp = forTimeZone(TimeZone.getDefault());
145                        }
146                    } catch (IllegalArgumentException ex) {
147                        // ignored
148                    }
149                    if (temp == null) {
150                        temp = UTC;
151                    }
152                    cDefault = zone = temp;
153                }
154            }
155        }
156        return zone;
157    }
158
159    /**
160     * Sets the default time zone.
161     * 
162     * @param zone  the default datetime zone object, must not be null
163     * @throws IllegalArgumentException if the zone is null
164     * @throws SecurityException if the application has insufficient security rights
165     */
166    public static void setDefault(DateTimeZone zone) throws SecurityException {
167        SecurityManager sm = System.getSecurityManager();
168        if (sm != null) {
169            sm.checkPermission(new JodaTimePermission("DateTimeZone.setDefault"));
170        }
171        if (zone == null) {
172            throw new IllegalArgumentException("The datetime zone must not be null");
173        }
174        synchronized(DateTimeZone.class) {
175            cDefault = zone;
176        }
177    }
178
179    //-----------------------------------------------------------------------
180    /**
181     * Gets a time zone instance for the specified time zone id.
182     * <p>
183     * The time zone id may be one of those returned by getAvailableIDs.
184     * Short ids, as accepted by {@link java.util.TimeZone}, are not accepted.
185     * All IDs must be specified in the long format.
186     * The exception is UTC, which is an acceptable id.
187     * <p>
188     * Alternatively a locale independent, fixed offset, datetime zone can
189     * be specified. The form <code>[+-]hh:mm</code> can be used.
190     * 
191     * @param id  the ID of the datetime zone, null means default
192     * @return the DateTimeZone object for the ID
193     * @throws IllegalArgumentException if the ID is not recognised
194     */
195    public static DateTimeZone forID(String id) {
196        if (id == null) {
197            return getDefault();
198        }
199        if (id.equals("UTC")) {
200            return DateTimeZone.UTC;
201        }
202        DateTimeZone zone = cProvider.getZone(id);
203        if (zone != null) {
204            return zone;
205        }
206        if (id.startsWith("+") || id.startsWith("-")) {
207            int offset = parseOffset(id);
208            if (offset == 0L) {
209                return DateTimeZone.UTC;
210            } else {
211                id = printOffset(offset);
212                return fixedOffsetZone(id, offset);
213            }
214        }
215        throw new IllegalArgumentException("The datetime zone id '" + id + "' is not recognised");
216    }
217
218    /**
219     * Gets a time zone instance for the specified offset to UTC in hours.
220     * This method assumes standard length hours.
221     * <p>
222     * This factory is a convenient way of constructing zones with a fixed offset.
223     * 
224     * @param hoursOffset  the offset in hours from UTC
225     * @return the DateTimeZone object for the offset
226     * @throws IllegalArgumentException if the offset is too large or too small
227     */
228    public static DateTimeZone forOffsetHours(int hoursOffset) throws IllegalArgumentException {
229        return forOffsetHoursMinutes(hoursOffset, 0);
230    }
231
232    /**
233     * Gets a time zone instance for the specified offset to UTC in hours and minutes.
234     * This method assumes 60 minutes in an hour, and standard length minutes.
235     * <p>
236     * This factory is a convenient way of constructing zones with a fixed offset.
237     * The minutes value is always positive and in the range 0 to 59.
238     * If constructed with the values (-2, 30), the resulting zone is '-02:30'.
239     * 
240     * @param hoursOffset  the offset in hours from UTC
241     * @param minutesOffset  the offset in minutes from UTC, must be between 0 and 59 inclusive
242     * @return the DateTimeZone object for the offset
243     * @throws IllegalArgumentException if the offset or minute is too large or too small
244     */
245    public static DateTimeZone forOffsetHoursMinutes(int hoursOffset, int minutesOffset) throws IllegalArgumentException {
246        if (hoursOffset == 0 && minutesOffset == 0) {
247            return DateTimeZone.UTC;
248        }
249        if (minutesOffset < 0 || minutesOffset > 59) {
250            throw new IllegalArgumentException("Minutes out of range: " + minutesOffset);
251        }
252        int offset = 0;
253        try {
254            int hoursInMinutes = FieldUtils.safeMultiply(hoursOffset, 60);
255            if (hoursInMinutes < 0) {
256                minutesOffset = FieldUtils.safeAdd(hoursInMinutes, -minutesOffset);
257            } else {
258                minutesOffset = FieldUtils.safeAdd(hoursInMinutes, minutesOffset);
259            }
260            offset = FieldUtils.safeMultiply(minutesOffset, DateTimeConstants.MILLIS_PER_MINUTE);
261        } catch (ArithmeticException ex) {
262            throw new IllegalArgumentException("Offset is too large");
263        }
264        return forOffsetMillis(offset);
265    }
266
267    /**
268     * Gets a time zone instance for the specified offset to UTC in milliseconds.
269     *
270     * @param millisOffset  the offset in millis from UTC
271     * @return the DateTimeZone object for the offset
272     */
273    public static DateTimeZone forOffsetMillis(int millisOffset) {
274        String id = printOffset(millisOffset);
275        return fixedOffsetZone(id, millisOffset);
276    }
277
278    /**
279     * Gets a time zone instance for a JDK TimeZone.
280     * <p>
281     * DateTimeZone only accepts a subset of the IDs from TimeZone. The
282     * excluded IDs are the short three letter form (except UTC). This 
283     * method will attempt to convert between time zones created using the
284     * short IDs and the full version.
285     * <p>
286     * This method is not designed to parse time zones with rules created by
287     * applications using <code>SimpleTimeZone</code> directly.
288     * 
289     * @param zone  the zone to convert, null means default
290     * @return the DateTimeZone object for the zone
291     * @throws IllegalArgumentException if the zone is not recognised
292     */
293    public static DateTimeZone forTimeZone(TimeZone zone) {
294        if (zone == null) {
295            return getDefault();
296        }
297        final String id = zone.getID();
298        if (id.equals("UTC")) {
299            return DateTimeZone.UTC;
300        }
301
302        // Convert from old alias before consulting provider since they may differ.
303        DateTimeZone dtz = null;
304        String convId = getConvertedId(id);
305        if (convId != null) {
306            dtz = cProvider.getZone(convId);
307        }
308        if (dtz == null) {
309            dtz = cProvider.getZone(id);
310        }
311        if (dtz != null) {
312            return dtz;
313        }
314
315        // Support GMT+/-hh:mm formats
316        if (convId == null) {
317            convId = zone.getDisplayName();
318            if (convId.startsWith("GMT+") || convId.startsWith("GMT-")) {
319                convId = convId.substring(3);
320                int offset = parseOffset(convId);
321                if (offset == 0L) {
322                    return DateTimeZone.UTC;
323                } else {
324                    convId = printOffset(offset);
325                    return fixedOffsetZone(convId, offset);
326                }
327            }
328        }
329        throw new IllegalArgumentException("The datetime zone id '" + id + "' is not recognised");
330    }
331
332    //-----------------------------------------------------------------------
333    /**
334     * Gets the zone using a fixed offset amount.
335     * 
336     * @param id  the zone id
337     * @param offset  the offset in millis
338     * @return the zone
339     */
340    private static synchronized DateTimeZone fixedOffsetZone(String id, int offset) {
341        if (offset == 0) {
342            return DateTimeZone.UTC;
343        }
344        if (iFixedOffsetCache == null) {
345            iFixedOffsetCache = new HashMap();
346        }
347        DateTimeZone zone;
348        Reference ref = (Reference) iFixedOffsetCache.get(id);
349        if (ref != null) {
350            zone = (DateTimeZone) ref.get();
351            if (zone != null) {
352                return zone;
353            }
354        }
355        zone = new FixedDateTimeZone(id, null, offset, offset);
356        iFixedOffsetCache.put(id, new SoftReference(zone));
357        return zone;
358    }
359
360    /**
361     * Gets all the available IDs supported.
362     * 
363     * @return an unmodifiable Set of String IDs
364     */
365    public static Set getAvailableIDs() {
366        return cAvailableIDs;
367    }
368
369    //-----------------------------------------------------------------------
370    /**
371     * Gets the zone provider factory.
372     * <p>
373     * The zone provider is a pluggable instance factory that supplies the
374     * actual instances of DateTimeZone.
375     * 
376     * @return the provider
377     */
378    public static Provider getProvider() {
379        return cProvider;
380    }
381
382    /**
383     * Sets the zone provider factory.
384     * <p>
385     * The zone provider is a pluggable instance factory that supplies the
386     * actual instances of DateTimeZone.
387     * 
388     * @param provider  provider to use, or null for default
389     * @throws SecurityException if you do not have the permission DateTimeZone.setProvider
390     * @throws IllegalArgumentException if the provider is invalid
391     */
392    public static void setProvider(Provider provider) throws SecurityException {
393        SecurityManager sm = System.getSecurityManager();
394        if (sm != null) {
395            sm.checkPermission(new JodaTimePermission("DateTimeZone.setProvider"));
396        }
397        setProvider0(provider);
398    }
399
400    /**
401     * Sets the zone provider factory without performing the security check.
402     * 
403     * @param provider  provider to use, or null for default
404     * @throws IllegalArgumentException if the provider is invalid
405     */
406    private static void setProvider0(Provider provider) {
407        if (provider == null) {
408            provider = getDefaultProvider();
409        }
410        Set ids = provider.getAvailableIDs();
411        if (ids == null || ids.size() == 0) {
412            throw new IllegalArgumentException
413                ("The provider doesn't have any available ids");
414        }
415        if (!ids.contains("UTC")) {
416            throw new IllegalArgumentException("The provider doesn't support UTC");
417        }
418        if (!UTC.equals(provider.getZone("UTC"))) {
419            throw new IllegalArgumentException("Invalid UTC zone provided");
420        }
421        cProvider = provider;
422        cAvailableIDs = ids;
423    }
424
425    /**
426     * Gets the default zone provider.
427     * <p>
428     * Tries the system property <code>org.joda.time.DateTimeZone.Provider</code>.
429     * Then tries a <code>ZoneInfoProvider</code> using the data in <code>org/joda/time/tz/data</code>.
430     * Then uses <code>UTCProvider</code>.
431     * 
432     * @return the default name provider
433     */
434    private static Provider getDefaultProvider() {
435        Provider provider = null;
436
437        try {
438            String providerClass =
439                System.getProperty("org.joda.time.DateTimeZone.Provider");
440            if (providerClass != null) {
441                try {
442                    provider = (Provider) Class.forName(providerClass).newInstance();
443                } catch (Exception ex) {
444                    Thread thread = Thread.currentThread();
445                    thread.getThreadGroup().uncaughtException(thread, ex);
446                }
447            }
448        } catch (SecurityException ex) {
449            // ignored
450        }
451
452        if (provider == null) {
453            try {
454                provider = new ZoneInfoProvider("org/joda/time/tz/data");
455            } catch (Exception ex) {
456                Thread thread = Thread.currentThread();
457                thread.getThreadGroup().uncaughtException(thread, ex);
458            }
459        }
460
461        if (provider == null) {
462            provider = new UTCProvider();
463        }
464
465        return provider;
466    }
467
468    //-----------------------------------------------------------------------
469    /**
470     * Gets the name provider factory.
471     * <p>
472     * The name provider is a pluggable instance factory that supplies the
473     * names of each DateTimeZone.
474     * 
475     * @return the provider
476     */
477    public static NameProvider getNameProvider() {
478        return cNameProvider;
479    }
480
481    /**
482     * Sets the name provider factory.
483     * <p>
484     * The name provider is a pluggable instance factory that supplies the
485     * names of each DateTimeZone.
486     * 
487     * @param nameProvider  provider to use, or null for default
488     * @throws SecurityException if you do not have the permission DateTimeZone.setNameProvider
489     * @throws IllegalArgumentException if the provider is invalid
490     */
491    public static void setNameProvider(NameProvider nameProvider) throws SecurityException {
492        SecurityManager sm = System.getSecurityManager();
493        if (sm != null) {
494            sm.checkPermission(new JodaTimePermission("DateTimeZone.setNameProvider"));
495        }
496        setNameProvider0(nameProvider);
497    }
498
499    /**
500     * Sets the name provider factory without performing the security check.
501     * 
502     * @param nameProvider  provider to use, or null for default
503     * @throws IllegalArgumentException if the provider is invalid
504     */
505    private static void setNameProvider0(NameProvider nameProvider) {
506        if (nameProvider == null) {
507            nameProvider = getDefaultNameProvider();
508        }
509        cNameProvider = nameProvider;
510    }
511
512    /**
513     * Gets the default name provider.
514     * <p>
515     * Tries the system property <code>org.joda.time.DateTimeZone.NameProvider</code>.
516     * Then uses <code>DefaultNameProvider</code>.
517     * 
518     * @return the default name provider
519     */
520    private static NameProvider getDefaultNameProvider() {
521        NameProvider nameProvider = null;
522        try {
523            String providerClass = System.getProperty("org.joda.time.DateTimeZone.NameProvider");
524            if (providerClass != null) {
525                try {
526                    nameProvider = (NameProvider) Class.forName(providerClass).newInstance();
527                } catch (Exception ex) {
528                    Thread thread = Thread.currentThread();
529                    thread.getThreadGroup().uncaughtException(thread, ex);
530                }
531            }
532        } catch (SecurityException ex) {
533            // ignore
534        }
535
536        if (nameProvider == null) {
537            nameProvider = new DefaultNameProvider();
538        }
539
540        return nameProvider;
541    }
542
543    //-----------------------------------------------------------------------
544    /**
545     * Converts an old style id to a new style id.
546     * 
547     * @param id  the old style id
548     * @return the new style id, null if not found
549     */
550    private static synchronized String getConvertedId(String id) {
551        Map map = cZoneIdConversion;
552        if (map == null) {
553            // Backwards compatibility with TimeZone.
554            map = new HashMap();
555            map.put("GMT", "UTC");
556            map.put("MIT", "Pacific/Apia");
557            map.put("HST", "Pacific/Honolulu");
558            map.put("AST", "America/Anchorage");
559            map.put("PST", "America/Los_Angeles");
560            map.put("MST", "America/Denver");
561            map.put("PNT", "America/Phoenix");
562            map.put("CST", "America/Chicago");
563            map.put("EST", "America/New_York");
564            map.put("IET", "America/Indianapolis");
565            map.put("PRT", "America/Puerto_Rico");
566            map.put("CNT", "America/St_Johns");
567            map.put("AGT", "America/Buenos_Aires");
568            map.put("BET", "America/Sao_Paulo");
569            map.put("WET", "Europe/London");
570            map.put("ECT", "Europe/Paris");
571            map.put("ART", "Africa/Cairo");
572            map.put("CAT", "Africa/Harare");
573            map.put("EET", "Europe/Bucharest");
574            map.put("EAT", "Africa/Addis_Ababa");
575            map.put("MET", "Asia/Tehran");
576            map.put("NET", "Asia/Yerevan");
577            map.put("PLT", "Asia/Karachi");
578            map.put("IST", "Asia/Calcutta");
579            map.put("BST", "Asia/Dhaka");
580            map.put("VST", "Asia/Saigon");
581            map.put("CTT", "Asia/Shanghai");
582            map.put("JST", "Asia/Tokyo");
583            map.put("ACT", "Australia/Darwin");
584            map.put("AET", "Australia/Sydney");
585            map.put("SST", "Pacific/Guadalcanal");
586            map.put("NST", "Pacific/Auckland");
587            cZoneIdConversion = map;
588        }
589        return (String) map.get(id);
590    }
591
592    private static int parseOffset(String str) {
593        // Can't use a real chronology if called during class
594        // initialization. Offset parser doesn't need it anyhow.
595        Chronology chrono = new BaseChronology() {
596            public DateTimeZone getZone() {
597                return null;
598            }
599            public Chronology withUTC() {
600                return this;
601            }
602            public Chronology withZone(DateTimeZone zone) {
603                return this;
604            }
605            public String toString() {
606                return getClass().getName();
607            }
608        };
609        return -(int) offsetFormatter().withChronology(chrono).parseMillis(str);
610    }
611
612    /**
613     * Formats a timezone offset string.
614     * <p>
615     * This method is kept separate from the formatting classes to speed and
616     * simplify startup and classloading.
617     * 
618     * @param offset  the offset in milliseconds
619     * @return the time zone string
620     */
621    private static String printOffset(int offset) {
622        StringBuffer buf = new StringBuffer();
623        if (offset >= 0) {
624            buf.append('+');
625        } else {
626            buf.append('-');
627            offset = -offset;
628        }
629
630        int hours = offset / DateTimeConstants.MILLIS_PER_HOUR;
631        FormatUtils.appendPaddedInteger(buf, hours, 2);
632        offset -= hours * (int) DateTimeConstants.MILLIS_PER_HOUR;
633
634        int minutes = offset / DateTimeConstants.MILLIS_PER_MINUTE;
635        buf.append(':');
636        FormatUtils.appendPaddedInteger(buf, minutes, 2);
637        offset -= minutes * DateTimeConstants.MILLIS_PER_MINUTE;
638        if (offset == 0) {
639            return buf.toString();
640        }
641
642        int seconds = offset / DateTimeConstants.MILLIS_PER_SECOND;
643        buf.append(':');
644        FormatUtils.appendPaddedInteger(buf, seconds, 2);
645        offset -= seconds * DateTimeConstants.MILLIS_PER_SECOND;
646        if (offset == 0) {
647            return buf.toString();
648        }
649
650        buf.append('.');
651        FormatUtils.appendPaddedInteger(buf, offset, 3);
652        return buf.toString();
653    }
654
655    /**
656     * Gets a printer/parser for managing the offset id formatting.
657     * 
658     * @return the formatter
659     */
660    private static synchronized DateTimeFormatter offsetFormatter() {
661        if (cOffsetFormatter == null) {
662            cOffsetFormatter = new DateTimeFormatterBuilder()
663                .appendTimeZoneOffset(null, true, 2, 4)
664                .toFormatter();
665        }
666        return cOffsetFormatter;
667    }
668
669    // Instance fields and methods
670    //--------------------------------------------------------------------
671
672    private final String iID;
673
674    /**
675     * Constructor.
676     * 
677     * @param id  the id to use
678     * @throws IllegalArgumentException if the id is null
679     */
680    protected DateTimeZone(String id) {
681        if (id == null) {
682            throw new IllegalArgumentException("Id must not be null");
683        }
684        iID = id;
685    }
686
687    // Principal methods
688    //--------------------------------------------------------------------
689
690    /**
691     * Gets the ID of this datetime zone.
692     * 
693     * @return the ID of this datetime zone
694     */
695    public final String getID() {
696        return iID;
697    }
698
699    /**
700     * Returns a non-localized name that is unique to this time zone. It can be
701     * combined with id to form a unique key for fetching localized names.
702     *
703     * @param instant  milliseconds from 1970-01-01T00:00:00Z to get the name for
704     * @return name key or null if id should be used for names
705     */
706    public abstract String getNameKey(long instant);
707
708    /**
709     * Gets the short name of this datetime zone suitable for display using
710     * the default locale.
711     * <p>
712     * If the name is not available for the locale, then this method returns a
713     * string in the format <code>[+-]hh:mm</code>.
714     * 
715     * @param instant  milliseconds from 1970-01-01T00:00:00Z to get the name for
716     * @return the human-readable short name in the default locale
717     */
718    public final String getShortName(long instant) {
719        return getShortName(instant, null);
720    }
721
722    /**
723     * Gets the short name of this datetime zone suitable for display using
724     * the specified locale.
725     * <p>
726     * If the name is not available for the locale, then this method returns a
727     * string in the format <code>[+-]hh:mm</code>.
728     * 
729     * @param instant  milliseconds from 1970-01-01T00:00:00Z to get the name for
730     * @param locale  the locale to get the name for
731     * @return the human-readable short name in the specified locale
732     */
733    public String getShortName(long instant, Locale locale) {
734        if (locale == null) {
735            locale = Locale.getDefault();
736        }
737        String nameKey = getNameKey(instant);
738        if (nameKey == null) {
739            return iID;
740        }
741        String name = cNameProvider.getShortName(locale, iID, nameKey);
742        if (name != null) {
743            return name;
744        }
745        return printOffset(getOffset(instant));
746    }
747
748    /**
749     * Gets the long name of this datetime zone suitable for display using
750     * the default locale.
751     * <p>
752     * If the name is not available for the locale, then this method returns a
753     * string in the format <code>[+-]hh:mm</code>.
754     * 
755     * @param instant  milliseconds from 1970-01-01T00:00:00Z to get the name for
756     * @return the human-readable long name in the default locale
757     */
758    public final String getName(long instant) {
759        return getName(instant, null);
760    }
761
762    /**
763     * Gets the long name of this datetime zone suitable for display using
764     * the specified locale.
765     * <p>
766     * If the name is not available for the locale, then this method returns a
767     * string in the format <code>[+-]hh:mm</code>.
768     * 
769     * @param instant  milliseconds from 1970-01-01T00:00:00Z to get the name for
770     * @param locale  the locale to get the name for
771     * @return the human-readable long name in the specified locale
772     */
773    public String getName(long instant, Locale locale) {
774        if (locale == null) {
775            locale = Locale.getDefault();
776        }
777        String nameKey = getNameKey(instant);
778        if (nameKey == null) {
779            return iID;
780        }
781        String name = cNameProvider.getName(locale, iID, nameKey);
782        if (name != null) {
783            return name;
784        }
785        return printOffset(getOffset(instant));
786    }
787
788    /**
789     * Gets the millisecond offset to add to UTC to get local time.
790     * 
791     * @param instant  milliseconds from 1970-01-01T00:00:00Z to get the offset for
792     * @return the millisecond offset to add to UTC to get local time
793     */
794    public abstract int getOffset(long instant);
795
796    /**
797     * Gets the millisecond offset to add to UTC to get local time.
798     * 
799     * @param instant  instant to get the offset for, null means now
800     * @return the millisecond offset to add to UTC to get local time
801     */
802    public final int getOffset(ReadableInstant instant) {
803        if (instant == null) {
804            return getOffset(DateTimeUtils.currentTimeMillis());
805        }
806        return getOffset(instant.getMillis());
807    }
808
809    /**
810     * Gets the standard millisecond offset to add to UTC to get local time,
811     * when standard time is in effect.
812     * 
813     * @param instant  milliseconds from 1970-01-01T00:00:00Z to get the offset for
814     * @return the millisecond offset to add to UTC to get local time
815     */
816    public abstract int getStandardOffset(long instant);
817
818    /**
819     * Checks whether, at a particular instant, the offset is standard or not.
820     * <p>
821     * This method can be used to determine whether Summer Time (DST) applies.
822     * As a general rule, if the offset at the specified instant is standard,
823     * then either Winter time applies, or there is no Summer Time. If the
824     * instant is not standard, then Summer Time applies.
825     * <p>
826     * The implementation of the method is simply whether {@link #getOffset(long)}
827     * equals {@link #getStandardOffset(long)} at the specified instant.
828     * 
829     * @param instant  milliseconds from 1970-01-01T00:00:00Z to get the offset for
830     * @return true if the offset at the given instant is the standard offset
831     * @since 1.5
832     */
833    public boolean isStandardOffset(long instant) {
834        return getOffset(instant) == getStandardOffset(instant);
835    }
836
837    /**
838     * Gets the millisecond offset to subtract from local time to get UTC time.
839     * This offset can be used to undo adding the offset obtained by getOffset.
840     *
841     * <pre>
842     * millisLocal == millisUTC   + getOffset(millisUTC)
843     * millisUTC   == millisLocal - getOffsetFromLocal(millisLocal)
844     * </pre>
845     *
846     * NOTE: After calculating millisLocal, some error may be introduced. At
847     * offset transitions (due to DST or other historical changes), ranges of
848     * local times may map to different UTC times.
849     * <p>
850     * This method will return an offset suitable for calculating an instant
851     * after any DST gap. For example, consider a zone with a cutover
852     * from 01:00 to 01:59:<br />
853     * Input: 00:00  Output: 00:00<br />
854     * Input: 00:30  Output: 00:30<br />
855     * Input: 01:00  Output: 02:00<br />
856     * Input: 01:30  Output: 02:30<br />
857     * Input: 02:00  Output: 02:00<br />
858     * Input: 02:30  Output: 02:30<br />
859     * <p>
860     * NOTE: The behaviour of this method changed in v1.5, with the emphasis
861     * on returning a consistent result later along the time-line (shown above).
862     *
863     * @param instantLocal  the millisecond instant, relative to this time zone, to
864     * get the offset for
865     * @return the millisecond offset to subtract from local time to get UTC time
866     */
867    public int getOffsetFromLocal(long instantLocal) {
868        // get the offset at instantLocal (first estimate)
869        int offsetLocal = getOffset(instantLocal);
870        // adjust instantLocal using the estimate and recalc the offset
871        int offsetAdjusted = getOffset(instantLocal - offsetLocal);
872        // if the offsets differ, we must be near a DST boundary
873        if (offsetLocal != offsetAdjusted) {
874            // we need to ensure that time is always after the DST gap
875            // this happens naturally for positive offsets, but not for negative
876            if ((offsetLocal - offsetAdjusted) < 0) {
877                // if we just return offsetAdjusted then the time is pushed
878                // back before the transition, whereas it should be
879                // on or after the transition
880                long nextLocal = nextTransition(instantLocal - offsetLocal);
881                long nextAdjusted = nextTransition(instantLocal - offsetAdjusted);
882                if (nextLocal != nextAdjusted) {
883                    return offsetLocal;
884                }
885            }
886        }
887        return offsetAdjusted;
888    }
889
890    /**
891     * Converts a standard UTC instant to a local instant with the same
892     * local time. This conversion is used before performing a calculation
893     * so that the calculation can be done using a simple local zone.
894     *
895     * @param instantUTC  the UTC instant to convert to local
896     * @return the local instant with the same local time
897     * @throws ArithmeticException if the result overflows a long
898     * @since 1.5
899     */
900    public long convertUTCToLocal(long instantUTC) {
901        int offset = getOffset(instantUTC);
902        long instantLocal = instantUTC + offset;
903        // If there is a sign change, but the two values have the same sign...
904        if ((instantUTC ^ instantLocal) < 0 && (instantUTC ^ offset) >= 0) {
905            throw new ArithmeticException("Adding time zone offset caused overflow");
906        }
907        return instantLocal;
908    }
909
910    /**
911     * Converts a local instant to a standard UTC instant with the same
912     * local time. This conversion is used after performing a calculation
913     * where the calculation was done using a simple local zone.
914     *
915     * @param instantLocal  the local instant to convert to UTC
916     * @param strict  whether the conversion should reject non-existent local times
917     * @return the UTC instant with the same local time, 
918     * @throws ArithmeticException if the result overflows a long
919     * @throws IllegalArgumentException if the zone has no eqivalent local time
920     * @since 1.5
921     */
922    public long convertLocalToUTC(long instantLocal, boolean strict) {
923        // get the offset at instantLocal (first estimate)
924        int offsetLocal = getOffset(instantLocal);
925        // adjust instantLocal using the estimate and recalc the offset
926        int offset = getOffset(instantLocal - offsetLocal);
927        // if the offsets differ, we must be near a DST boundary
928        if (offsetLocal != offset) {
929            // if strict then always check if in DST gap
930            // otherwise only check if zone in Western hemisphere (as the
931            // value of offset is already correct for Eastern hemisphere)
932            if (strict || offsetLocal < 0) {
933                // determine if we are in the DST gap
934                long nextLocal = nextTransition(instantLocal - offsetLocal);
935                if (nextLocal == (instantLocal - offsetLocal)) {
936                    nextLocal = Long.MAX_VALUE;
937                }
938                long nextAdjusted = nextTransition(instantLocal - offset);
939                if (nextAdjusted == (instantLocal - offset)) {
940                    nextAdjusted = Long.MAX_VALUE;
941                }
942                if (nextLocal != nextAdjusted) {
943                    // yes we are in the DST gap
944                    if (strict) {
945                        // DST gap is not acceptable
946                        throw new IllegalArgumentException("Illegal instant due to time zone offset transition: " +
947                                DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss.SSS").print(new Instant(instantLocal)) +
948                                " (" + getID() + ")");
949                    } else {
950                        // DST gap is acceptable, but for the Western hemisphere
951                        // the offset is wrong and will result in local times
952                        // before the cutover so use the offsetLocal instead
953                        offset = offsetLocal;
954                    }
955                }
956            }
957        }
958        // check for overflow
959        long instantUTC = instantLocal - offset;
960        // If there is a sign change, but the two values have different signs...
961        if ((instantLocal ^ instantUTC) < 0 && (instantLocal ^ offset) < 0) {
962            throw new ArithmeticException("Subtracting time zone offset caused overflow");
963        }
964        return instantUTC;
965    }
966
967    /**
968     * Gets the millisecond instant in another zone keeping the same local time.
969     * <p>
970     * The conversion is performed by converting the specified UTC millis to local
971     * millis in this zone, then converting back to UTC millis in the new zone.
972     *
973     * @param newZone  the new zone, null means default
974     * @param oldInstant  the UTC millisecond instant to convert
975     * @return the UTC millisecond instant with the same local time in the new zone
976     */
977    public long getMillisKeepLocal(DateTimeZone newZone, long oldInstant) {
978        if (newZone == null) {
979            newZone = DateTimeZone.getDefault();
980        }
981        if (newZone == this) {
982            return oldInstant;
983        }
984        long instantLocal = oldInstant + getOffset(oldInstant);
985        return instantLocal - newZone.getOffsetFromLocal(instantLocal);
986    }
987
988//    //-----------------------------------------------------------------------
989//    /**
990//     * Checks if the given {@link LocalDateTime} is within an overlap.
991//     * <p>
992//     * When switching from Daylight Savings Time to standard time there is
993//     * typically an overlap where the same clock hour occurs twice. This
994//     * method identifies whether the local datetime refers to such an overlap.
995//     * 
996//     * @param localDateTime  the time to check, not null
997//     * @return true if the given datetime refers to an overlap
998//     */
999//    public boolean isLocalDateTimeOverlap(LocalDateTime localDateTime) {
1000//        if (isFixed()) {
1001//            return false;
1002//        }
1003//        long instantLocal = localDateTime.toDateTime(DateTimeZone.UTC).getMillis();
1004//        // get the offset at instantLocal (first estimate)
1005//        int offsetLocal = getOffset(instantLocal);
1006//        // adjust instantLocal using the estimate and recalc the offset
1007//        int offset = getOffset(instantLocal - offsetLocal);
1008//        // if the offsets differ, we must be near a DST boundary
1009//        if (offsetLocal != offset) {
1010//            long nextLocal = nextTransition(instantLocal - offsetLocal);
1011//            long nextAdjusted = nextTransition(instantLocal - offset);
1012//            if (nextLocal != nextAdjusted) {
1013//                // in DST gap
1014//                return false;
1015//            }
1016//            long diff = Math.abs(offset - offsetLocal);
1017//            DateTime dateTime = localDateTime.toDateTime(this);
1018//            DateTime adjusted = dateTime.plus(diff);
1019//            if (dateTime.getHourOfDay() == adjusted.getHourOfDay() &&
1020//                    dateTime.getMinuteOfHour() == adjusted.getMinuteOfHour() &&
1021//                    dateTime.getSecondOfMinute() == adjusted.getSecondOfMinute()) {
1022//                return true;
1023//            }
1024//            adjusted = dateTime.minus(diff);
1025//            if (dateTime.getHourOfDay() == adjusted.getHourOfDay() &&
1026//                    dateTime.getMinuteOfHour() == adjusted.getMinuteOfHour() &&
1027//                    dateTime.getSecondOfMinute() == adjusted.getSecondOfMinute()) {
1028//                return true;
1029//            }
1030//            return false;
1031//        }
1032//        return false;
1033//    }
1034//        
1035//        
1036//        DateTime dateTime = null;
1037//        try {
1038//            dateTime = localDateTime.toDateTime(this);
1039//        } catch (IllegalArgumentException ex) {
1040//            return false;  // it is a gap, not an overlap
1041//        }
1042//        long offset1 = Math.abs(getOffset(dateTime.getMillis() + 1) - getStandardOffset(dateTime.getMillis() + 1));
1043//        long offset2 = Math.abs(getOffset(dateTime.getMillis() - 1) - getStandardOffset(dateTime.getMillis() - 1));
1044//        long offset = Math.max(offset1, offset2);
1045//        if (offset == 0) {
1046//            return false;
1047//        }
1048//        DateTime adjusted = dateTime.plus(offset);
1049//        if (dateTime.getHourOfDay() == adjusted.getHourOfDay() &&
1050//                dateTime.getMinuteOfHour() == adjusted.getMinuteOfHour() &&
1051//                dateTime.getSecondOfMinute() == adjusted.getSecondOfMinute()) {
1052//            return true;
1053//        }
1054//        adjusted = dateTime.minus(offset);
1055//        if (dateTime.getHourOfDay() == adjusted.getHourOfDay() &&
1056//                dateTime.getMinuteOfHour() == adjusted.getMinuteOfHour() &&
1057//                dateTime.getSecondOfMinute() == adjusted.getSecondOfMinute()) {
1058//            return true;
1059//        }
1060//        return false;
1061        
1062//        long millis = dateTime.getMillis();
1063//        long nextTransition = nextTransition(millis);
1064//        long previousTransition = previousTransition(millis);
1065//        long deltaToPreviousTransition = millis - previousTransition;
1066//        long deltaToNextTransition = nextTransition - millis;
1067//        if (deltaToNextTransition < deltaToPreviousTransition) {
1068//            int offset = getOffset(nextTransition);
1069//            int standardOffset = getStandardOffset(nextTransition);
1070//            if (Math.abs(offset - standardOffset) >= deltaToNextTransition) {
1071//                return true;
1072//            }
1073//        } else  {
1074//            int offset = getOffset(previousTransition);
1075//            int standardOffset = getStandardOffset(previousTransition);
1076//            if (Math.abs(offset - standardOffset) >= deltaToPreviousTransition) {
1077//                return true;
1078//            }
1079//        }
1080//        return false;
1081//    }
1082
1083    /**
1084     * Checks if the given {@link LocalDateTime} is within a gap.
1085     * <p>
1086     * When switching from standard time to Daylight Savings Time there is
1087     * typically a gap where a clock hour is missing. This method identifies
1088     * whether the local datetime refers to such a gap.
1089     * 
1090     * @param localDateTime  the time to check, not null
1091     * @return true if the given datetime refers to a gap
1092     * @since 1.6
1093     */
1094    public boolean isLocalDateTimeGap(LocalDateTime localDateTime) {
1095        if (isFixed()) {
1096            return false;
1097        }
1098        try {
1099            localDateTime.toDateTime(this);
1100            return false;
1101        } catch (IllegalArgumentException ex) {
1102            return true;
1103        }
1104    }
1105
1106    //-----------------------------------------------------------------------
1107    /**
1108     * Returns true if this time zone has no transitions.
1109     *
1110     * @return true if no transitions
1111     */
1112    public abstract boolean isFixed();
1113
1114    /**
1115     * Advances the given instant to where the time zone offset or name changes.
1116     * If the instant returned is exactly the same as passed in, then
1117     * no changes occur after the given instant.
1118     *
1119     * @param instant  milliseconds from 1970-01-01T00:00:00Z
1120     * @return milliseconds from 1970-01-01T00:00:00Z
1121     */
1122    public abstract long nextTransition(long instant);
1123
1124    /**
1125     * Retreats the given instant to where the time zone offset or name changes.
1126     * If the instant returned is exactly the same as passed in, then
1127     * no changes occur before the given instant.
1128     *
1129     * @param instant  milliseconds from 1970-01-01T00:00:00Z
1130     * @return milliseconds from 1970-01-01T00:00:00Z
1131     */
1132    public abstract long previousTransition(long instant);
1133
1134    // Basic methods
1135    //--------------------------------------------------------------------
1136
1137    /**
1138     * Get the datetime zone as a {@link java.util.TimeZone}.
1139     * 
1140     * @return the closest matching TimeZone object
1141     */
1142    public java.util.TimeZone toTimeZone() {
1143        return java.util.TimeZone.getTimeZone(iID);
1144    }
1145
1146    /**
1147     * Compare this datetime zone with another.
1148     * 
1149     * @param object the object to compare with
1150     * @return true if equal, based on the ID and all internal rules
1151     */
1152    public abstract boolean equals(Object object);
1153
1154    /**
1155     * Gets a hash code compatable with equals.
1156     * 
1157     * @return suitable hashcode
1158     */
1159    public int hashCode() {
1160        return 57 + getID().hashCode();
1161    }
1162
1163    /**
1164     * Gets the datetime zone as a string, which is simply its ID.
1165     * @return the id of the zone
1166     */
1167    public String toString() {
1168        return getID();
1169    }
1170
1171    /**
1172     * By default, when DateTimeZones are serialized, only a "stub" object
1173     * referring to the id is written out. When the stub is read in, it
1174     * replaces itself with a DateTimeZone object.
1175     * @return a stub object to go in the stream
1176     */
1177    protected Object writeReplace() throws ObjectStreamException {
1178        return new Stub(iID);
1179    }
1180
1181    /**
1182     * Used to serialize DateTimeZones by id.
1183     */
1184    private static final class Stub implements Serializable {
1185        /** Serialization lock. */
1186        private static final long serialVersionUID = -6471952376487863581L;
1187        /** The ID of the zone. */
1188        private transient String iID;
1189
1190        /**
1191         * Constructor.
1192         * @param id  the id of the zone
1193         */
1194        Stub(String id) {
1195            iID = id;
1196        }
1197
1198        private void writeObject(ObjectOutputStream out) throws IOException {
1199            out.writeUTF(iID);
1200        }
1201
1202        private void readObject(ObjectInputStream in) throws IOException {
1203            iID = in.readUTF();
1204        }
1205
1206        private Object readResolve() throws ObjectStreamException {
1207            return forID(iID);
1208        }
1209    }
1210}