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 }