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 }