001 /*
002 * Copyright 2001-2013 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.convert.FromString;
032 import org.joda.convert.ToString;
033 import org.joda.time.chrono.BaseChronology;
034 import org.joda.time.field.FieldUtils;
035 import org.joda.time.format.DateTimeFormat;
036 import org.joda.time.format.DateTimeFormatter;
037 import org.joda.time.format.DateTimeFormatterBuilder;
038 import org.joda.time.format.FormatUtils;
039 import org.joda.time.tz.DefaultNameProvider;
040 import org.joda.time.tz.FixedDateTimeZone;
041 import org.joda.time.tz.NameProvider;
042 import org.joda.time.tz.Provider;
043 import org.joda.time.tz.UTCProvider;
044 import org.joda.time.tz.ZoneInfoProvider;
045
046 /**
047 * DateTimeZone represents a time zone.
048 * <p>
049 * A time zone is a system of rules to convert time from one geographic
050 * location to another. For example, Paris, France is one hour ahead of
051 * London, England. Thus when it is 10:00 in London, it is 11:00 in Paris.
052 * <p>
053 * All time zone rules are expressed, for historical reasons, relative to
054 * Greenwich, London. Local time in Greenwich is referred to as Greenwich Mean
055 * Time (GMT). This is similar, but not precisely identical, to Universal
056 * Coordinated Time, or UTC. This library only uses the term UTC.
057 * <p>
058 * Using this system, America/Los_Angeles is expressed as UTC-08:00, or UTC-07:00
059 * in the summer. The offset -08:00 indicates that America/Los_Angeles time is
060 * obtained from UTC by adding -08:00, that is, by subtracting 8 hours.
061 * <p>
062 * The offset differs in the summer because of daylight saving time, or DST.
063 * The following definitions of time are generally used:
064 * <ul>
065 * <li>UTC - The reference time.
066 * <li>Standard Time - The local time without a daylight saving time offset.
067 * For example, in Paris, standard time is UTC+01:00.
068 * <li>Daylight Saving Time - The local time with a daylight saving time
069 * offset. This offset is typically one hour, but not always. It is typically
070 * used in most countries away from the equator. In Paris, daylight saving
071 * time is UTC+02:00.
072 * <li>Wall Time - This is what a local clock on the wall reads. This will be
073 * either Standard Time or Daylight Saving Time depending on the time of year
074 * and whether the location uses Daylight Saving Time.
075 * </ul>
076 * <p>
077 * Unlike the Java TimeZone class, DateTimeZone is immutable. It also only
078 * supports long format time zone ids. Thus EST and ECT are not accepted.
079 * However, the factory that accepts a TimeZone will attempt to convert from
080 * the old short id to a suitable long id.
081 * <p>
082 * DateTimeZone is thread-safe and immutable, and all subclasses must be as
083 * well.
084 *
085 * @author Brian S O'Neill
086 * @author Stephen Colebourne
087 * @since 1.0
088 */
089 public abstract class DateTimeZone implements Serializable {
090
091 /** Serialization version. */
092 private static final long serialVersionUID = 5546345482340108586L;
093
094 /** The time zone for Universal Coordinated Time */
095 public static final DateTimeZone UTC = new FixedDateTimeZone("UTC", "UTC", 0, 0);
096
097 /** The instance that is providing time zones. */
098 private static Provider cProvider;
099 /** 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 }