View Javadoc

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