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