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     */
016    package org.joda.time;
017    
018    import java.io.IOException;
019    import java.io.ObjectInputStream;
020    import java.io.ObjectOutputStream;
021    import java.io.ObjectStreamException;
022    import java.io.Serializable;
023    import java.lang.ref.Reference;
024    import java.lang.ref.SoftReference;
025    import java.util.HashMap;
026    import java.util.Locale;
027    import java.util.Map;
028    import java.util.Set;
029    import java.util.TimeZone;
030    
031    import org.joda.time.chrono.BaseChronology;
032    import org.joda.time.field.FieldUtils;
033    import org.joda.time.format.DateTimeFormat;
034    import org.joda.time.format.DateTimeFormatter;
035    import org.joda.time.format.DateTimeFormatterBuilder;
036    import org.joda.time.format.FormatUtils;
037    import org.joda.time.tz.DefaultNameProvider;
038    import org.joda.time.tz.FixedDateTimeZone;
039    import org.joda.time.tz.NameProvider;
040    import org.joda.time.tz.Provider;
041    import org.joda.time.tz.UTCProvider;
042    import 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     */
087    public 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    }