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