1 | /* |
2 | * Copyright 2001-2008 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.chrono; |
17 | |
18 | import java.util.HashMap; |
19 | import java.util.Locale; |
20 | |
21 | import org.joda.time.Chronology; |
22 | import org.joda.time.DateTimeConstants; |
23 | import org.joda.time.DateTimeField; |
24 | import org.joda.time.DateTimeZone; |
25 | import org.joda.time.DurationField; |
26 | import org.joda.time.IllegalFieldValueException; |
27 | import org.joda.time.Instant; |
28 | import org.joda.time.ReadablePartial; |
29 | import org.joda.time.field.BaseDateTimeField; |
30 | import org.joda.time.field.BaseDurationField; |
31 | import org.joda.time.format.DateTimeFormat; |
32 | |
33 | /** |
34 | * Wraps another Chronology to add support for time zones. |
35 | * <p> |
36 | * ZonedChronology is thread-safe and immutable. |
37 | * |
38 | * @author Brian S O'Neill |
39 | * @author Stephen Colebourne |
40 | * @since 1.0 |
41 | */ |
42 | public final class ZonedChronology extends AssembledChronology { |
43 | |
44 | /** Serialization lock */ |
45 | private static final long serialVersionUID = -1079258847191166848L; |
46 | |
47 | /** |
48 | * Create a ZonedChronology for any chronology, overriding any time zone it |
49 | * may already have. |
50 | * |
51 | * @param base base chronology to wrap |
52 | * @param zone the time zone |
53 | * @throws IllegalArgumentException if chronology or time zone is null |
54 | */ |
55 | public static ZonedChronology getInstance(Chronology base, DateTimeZone zone) { |
56 | if (base == null) { |
57 | throw new IllegalArgumentException("Must supply a chronology"); |
58 | } |
59 | base = base.withUTC(); |
60 | if (base == null) { |
61 | throw new IllegalArgumentException("UTC chronology must not be null"); |
62 | } |
63 | if (zone == null) { |
64 | throw new IllegalArgumentException("DateTimeZone must not be null"); |
65 | } |
66 | return new ZonedChronology(base, zone); |
67 | } |
68 | |
69 | static boolean useTimeArithmetic(DurationField field) { |
70 | // Use time of day arithmetic rules for unit durations less than |
71 | // typical time zone offsets. |
72 | return field != null && field.getUnitMillis() < DateTimeConstants.MILLIS_PER_HOUR * 12; |
73 | } |
74 | |
75 | /** |
76 | * Restricted constructor |
77 | * |
78 | * @param base base chronology to wrap |
79 | * @param zone the time zone |
80 | */ |
81 | private ZonedChronology(Chronology base, DateTimeZone zone) { |
82 | super(base, zone); |
83 | } |
84 | |
85 | public DateTimeZone getZone() { |
86 | return (DateTimeZone)getParam(); |
87 | } |
88 | |
89 | public Chronology withUTC() { |
90 | return getBase(); |
91 | } |
92 | |
93 | public Chronology withZone(DateTimeZone zone) { |
94 | if (zone == null) { |
95 | zone = DateTimeZone.getDefault(); |
96 | } |
97 | if (zone == getParam()) { |
98 | return this; |
99 | } |
100 | if (zone == DateTimeZone.UTC) { |
101 | return getBase(); |
102 | } |
103 | return new ZonedChronology(getBase(), zone); |
104 | } |
105 | |
106 | public long getDateTimeMillis(int year, int monthOfYear, int dayOfMonth, |
107 | int millisOfDay) |
108 | throws IllegalArgumentException |
109 | { |
110 | return localToUTC(getBase().getDateTimeMillis |
111 | (year, monthOfYear, dayOfMonth, millisOfDay)); |
112 | } |
113 | |
114 | public long getDateTimeMillis(int year, int monthOfYear, int dayOfMonth, |
115 | int hourOfDay, int minuteOfHour, |
116 | int secondOfMinute, int millisOfSecond) |
117 | throws IllegalArgumentException |
118 | { |
119 | return localToUTC(getBase().getDateTimeMillis |
120 | (year, monthOfYear, dayOfMonth, |
121 | hourOfDay, minuteOfHour, secondOfMinute, millisOfSecond)); |
122 | } |
123 | |
124 | public long getDateTimeMillis(long instant, |
125 | int hourOfDay, int minuteOfHour, |
126 | int secondOfMinute, int millisOfSecond) |
127 | throws IllegalArgumentException |
128 | { |
129 | return localToUTC(getBase().getDateTimeMillis |
130 | (instant + getZone().getOffset(instant), |
131 | hourOfDay, minuteOfHour, secondOfMinute, millisOfSecond)); |
132 | } |
133 | |
134 | /** |
135 | * @param instant instant from 1970-01-01T00:00:00 local time |
136 | * @return instant from 1970-01-01T00:00:00Z |
137 | */ |
138 | private long localToUTC(long instant) { |
139 | DateTimeZone zone = getZone(); |
140 | int offset = zone.getOffsetFromLocal(instant); |
141 | instant -= offset; |
142 | if (offset != zone.getOffset(instant)) { |
143 | throw new IllegalArgumentException |
144 | ("Illegal instant due to time zone offset transition: " + |
145 | DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss.SSS").print(new Instant(instant))); |
146 | } |
147 | return instant; |
148 | } |
149 | |
150 | protected void assemble(Fields fields) { |
151 | // Keep a local cache of converted fields so as not to create redundant |
152 | // objects. |
153 | HashMap converted = new HashMap(); |
154 | |
155 | // Convert duration fields... |
156 | |
157 | fields.eras = convertField(fields.eras, converted); |
158 | fields.centuries = convertField(fields.centuries, converted); |
159 | fields.years = convertField(fields.years, converted); |
160 | fields.months = convertField(fields.months, converted); |
161 | fields.weekyears = convertField(fields.weekyears, converted); |
162 | fields.weeks = convertField(fields.weeks, converted); |
163 | fields.days = convertField(fields.days, converted); |
164 | |
165 | fields.halfdays = convertField(fields.halfdays, converted); |
166 | fields.hours = convertField(fields.hours, converted); |
167 | fields.minutes = convertField(fields.minutes, converted); |
168 | fields.seconds = convertField(fields.seconds, converted); |
169 | fields.millis = convertField(fields.millis, converted); |
170 | |
171 | // Convert datetime fields... |
172 | |
173 | fields.year = convertField(fields.year, converted); |
174 | fields.yearOfEra = convertField(fields.yearOfEra, converted); |
175 | fields.yearOfCentury = convertField(fields.yearOfCentury, converted); |
176 | fields.centuryOfEra = convertField(fields.centuryOfEra, converted); |
177 | fields.era = convertField(fields.era, converted); |
178 | fields.dayOfWeek = convertField(fields.dayOfWeek, converted); |
179 | fields.dayOfMonth = convertField(fields.dayOfMonth, converted); |
180 | fields.dayOfYear = convertField(fields.dayOfYear, converted); |
181 | fields.monthOfYear = convertField(fields.monthOfYear, converted); |
182 | fields.weekOfWeekyear = convertField(fields.weekOfWeekyear, converted); |
183 | fields.weekyear = convertField(fields.weekyear, converted); |
184 | fields.weekyearOfCentury = convertField(fields.weekyearOfCentury, converted); |
185 | |
186 | fields.millisOfSecond = convertField(fields.millisOfSecond, converted); |
187 | fields.millisOfDay = convertField(fields.millisOfDay, converted); |
188 | fields.secondOfMinute = convertField(fields.secondOfMinute, converted); |
189 | fields.secondOfDay = convertField(fields.secondOfDay, converted); |
190 | fields.minuteOfHour = convertField(fields.minuteOfHour, converted); |
191 | fields.minuteOfDay = convertField(fields.minuteOfDay, converted); |
192 | fields.hourOfDay = convertField(fields.hourOfDay, converted); |
193 | fields.hourOfHalfday = convertField(fields.hourOfHalfday, converted); |
194 | fields.clockhourOfDay = convertField(fields.clockhourOfDay, converted); |
195 | fields.clockhourOfHalfday = convertField(fields.clockhourOfHalfday, converted); |
196 | fields.halfdayOfDay = convertField(fields.halfdayOfDay, converted); |
197 | } |
198 | |
199 | private DurationField convertField(DurationField field, HashMap converted) { |
200 | if (field == null || !field.isSupported()) { |
201 | return field; |
202 | } |
203 | if (converted.containsKey(field)) { |
204 | return (DurationField)converted.get(field); |
205 | } |
206 | ZonedDurationField zonedField = new ZonedDurationField(field, getZone()); |
207 | converted.put(field, zonedField); |
208 | return zonedField; |
209 | } |
210 | |
211 | private DateTimeField convertField(DateTimeField field, HashMap converted) { |
212 | if (field == null || !field.isSupported()) { |
213 | return field; |
214 | } |
215 | if (converted.containsKey(field)) { |
216 | return (DateTimeField)converted.get(field); |
217 | } |
218 | ZonedDateTimeField zonedField = |
219 | new ZonedDateTimeField(field, getZone(), |
220 | convertField(field.getDurationField(), converted), |
221 | convertField(field.getRangeDurationField(), converted), |
222 | convertField(field.getLeapDurationField(), converted)); |
223 | converted.put(field, zonedField); |
224 | return zonedField; |
225 | } |
226 | |
227 | //----------------------------------------------------------------------- |
228 | /** |
229 | * A zoned chronology is only equal to a zoned chronology with the |
230 | * same base chronology and zone. |
231 | * |
232 | * @param obj the object to compare to |
233 | * @return true if equal |
234 | * @since 1.4 |
235 | */ |
236 | public boolean equals(Object obj) { |
237 | if (this == obj) { |
238 | return true; |
239 | } |
240 | if (obj instanceof ZonedChronology == false) { |
241 | return false; |
242 | } |
243 | ZonedChronology chrono = (ZonedChronology) obj; |
244 | return |
245 | getBase().equals(chrono.getBase()) && |
246 | getZone().equals(chrono.getZone()); |
247 | } |
248 | |
249 | /** |
250 | * A suitable hashcode for the chronology. |
251 | * |
252 | * @return the hashcode |
253 | * @since 1.4 |
254 | */ |
255 | public int hashCode() { |
256 | return 326565 + getZone().hashCode() * 11 + getBase().hashCode() * 7; |
257 | } |
258 | |
259 | /** |
260 | * A debugging string for the chronology. |
261 | * |
262 | * @return the debugging string |
263 | */ |
264 | public String toString() { |
265 | return "ZonedChronology[" + getBase() + ", " + getZone().getID() + ']'; |
266 | } |
267 | |
268 | //----------------------------------------------------------------------- |
269 | /* |
270 | * Because time durations are typically smaller than time zone offsets, the |
271 | * arithmetic methods subtract the original offset. This produces a more |
272 | * expected behavior when crossing time zone offset transitions. For dates, |
273 | * the new offset is subtracted off. This behavior, if applied to time |
274 | * fields, can nullify or reverse an add when crossing a transition. |
275 | */ |
276 | static class ZonedDurationField extends BaseDurationField { |
277 | private static final long serialVersionUID = -485345310999208286L; |
278 | |
279 | final DurationField iField; |
280 | final boolean iTimeField; |
281 | final DateTimeZone iZone; |
282 | |
283 | ZonedDurationField(DurationField field, DateTimeZone zone) { |
284 | super(field.getType()); |
285 | if (!field.isSupported()) { |
286 | throw new IllegalArgumentException(); |
287 | } |
288 | iField = field; |
289 | iTimeField = useTimeArithmetic(field); |
290 | iZone = zone; |
291 | } |
292 | |
293 | public boolean isPrecise() { |
294 | return iTimeField ? iField.isPrecise() : iField.isPrecise() && this.iZone.isFixed(); |
295 | } |
296 | |
297 | public long getUnitMillis() { |
298 | return iField.getUnitMillis(); |
299 | } |
300 | |
301 | public int getValue(long duration, long instant) { |
302 | return iField.getValue(duration, addOffset(instant)); |
303 | } |
304 | |
305 | public long getValueAsLong(long duration, long instant) { |
306 | return iField.getValueAsLong(duration, addOffset(instant)); |
307 | } |
308 | |
309 | public long getMillis(int value, long instant) { |
310 | return iField.getMillis(value, addOffset(instant)); |
311 | } |
312 | |
313 | public long getMillis(long value, long instant) { |
314 | return iField.getMillis(value, addOffset(instant)); |
315 | } |
316 | |
317 | public long add(long instant, int value) { |
318 | int offset = getOffsetToAdd(instant); |
319 | instant = iField.add(instant + offset, value); |
320 | return instant - (iTimeField ? offset : getOffsetFromLocalToSubtract(instant)); |
321 | } |
322 | |
323 | public long add(long instant, long value) { |
324 | int offset = getOffsetToAdd(instant); |
325 | instant = iField.add(instant + offset, value); |
326 | return instant - (iTimeField ? offset : getOffsetFromLocalToSubtract(instant)); |
327 | } |
328 | |
329 | public int getDifference(long minuendInstant, long subtrahendInstant) { |
330 | int offset = getOffsetToAdd(subtrahendInstant); |
331 | return iField.getDifference |
332 | (minuendInstant + (iTimeField ? offset : getOffsetToAdd(minuendInstant)), |
333 | subtrahendInstant + offset); |
334 | } |
335 | |
336 | public long getDifferenceAsLong(long minuendInstant, long subtrahendInstant) { |
337 | int offset = getOffsetToAdd(subtrahendInstant); |
338 | return iField.getDifferenceAsLong |
339 | (minuendInstant + (iTimeField ? offset : getOffsetToAdd(minuendInstant)), |
340 | subtrahendInstant + offset); |
341 | } |
342 | |
343 | private int getOffsetToAdd(long instant) { |
344 | int offset = this.iZone.getOffset(instant); |
345 | long sum = instant + offset; |
346 | // If there is a sign change, but the two values have the same sign... |
347 | if ((instant ^ sum) < 0 && (instant ^ offset) >= 0) { |
348 | throw new ArithmeticException("Adding time zone offset caused overflow"); |
349 | } |
350 | return offset; |
351 | } |
352 | |
353 | private int getOffsetFromLocalToSubtract(long instant) { |
354 | int offset = this.iZone.getOffsetFromLocal(instant); |
355 | long diff = instant - offset; |
356 | // If there is a sign change, but the two values have different signs... |
357 | if ((instant ^ diff) < 0 && (instant ^ offset) < 0) { |
358 | throw new ArithmeticException("Subtracting time zone offset caused overflow"); |
359 | } |
360 | return offset; |
361 | } |
362 | |
363 | private long addOffset(long instant) { |
364 | return iZone.convertUTCToLocal(instant); |
365 | } |
366 | } |
367 | |
368 | /** |
369 | * A DateTimeField that decorates another to add timezone behaviour. |
370 | * <p> |
371 | * This class converts passed in instants to local wall time, and vice |
372 | * versa on output. |
373 | */ |
374 | static final class ZonedDateTimeField extends BaseDateTimeField { |
375 | private static final long serialVersionUID = -3968986277775529794L; |
376 | |
377 | final DateTimeField iField; |
378 | final DateTimeZone iZone; |
379 | final DurationField iDurationField; |
380 | final boolean iTimeField; |
381 | final DurationField iRangeDurationField; |
382 | final DurationField iLeapDurationField; |
383 | |
384 | ZonedDateTimeField(DateTimeField field, |
385 | DateTimeZone zone, |
386 | DurationField durationField, |
387 | DurationField rangeDurationField, |
388 | DurationField leapDurationField) { |
389 | super(field.getType()); |
390 | if (!field.isSupported()) { |
391 | throw new IllegalArgumentException(); |
392 | } |
393 | iField = field; |
394 | iZone = zone; |
395 | iDurationField = durationField; |
396 | iTimeField = useTimeArithmetic(durationField); |
397 | iRangeDurationField = rangeDurationField; |
398 | iLeapDurationField = leapDurationField; |
399 | } |
400 | |
401 | public boolean isLenient() { |
402 | return iField.isLenient(); |
403 | } |
404 | |
405 | public int get(long instant) { |
406 | long localInstant = iZone.convertUTCToLocal(instant); |
407 | return iField.get(localInstant); |
408 | } |
409 | |
410 | public String getAsText(long instant, Locale locale) { |
411 | long localInstant = iZone.convertUTCToLocal(instant); |
412 | return iField.getAsText(localInstant, locale); |
413 | } |
414 | |
415 | public String getAsShortText(long instant, Locale locale) { |
416 | long localInstant = iZone.convertUTCToLocal(instant); |
417 | return iField.getAsShortText(localInstant, locale); |
418 | } |
419 | |
420 | public String getAsText(int fieldValue, Locale locale) { |
421 | return iField.getAsText(fieldValue, locale); |
422 | } |
423 | |
424 | public String getAsShortText(int fieldValue, Locale locale) { |
425 | return iField.getAsShortText(fieldValue, locale); |
426 | } |
427 | |
428 | public long add(long instant, int value) { |
429 | if (iTimeField) { |
430 | int offset = getOffsetToAdd(instant); |
431 | long localInstant = iField.add(instant + offset, value); |
432 | return localInstant - offset; |
433 | } else { |
434 | long localInstant = iZone.convertUTCToLocal(instant); |
435 | localInstant = iField.add(localInstant, value); |
436 | return iZone.convertLocalToUTC(localInstant, false); |
437 | } |
438 | } |
439 | |
440 | public long add(long instant, long value) { |
441 | if (iTimeField) { |
442 | int offset = getOffsetToAdd(instant); |
443 | long localInstant = iField.add(instant + offset, value); |
444 | return localInstant - offset; |
445 | } else { |
446 | long localInstant = iZone.convertUTCToLocal(instant); |
447 | localInstant = iField.add(localInstant, value); |
448 | return iZone.convertLocalToUTC(localInstant, false); |
449 | } |
450 | } |
451 | |
452 | public long addWrapField(long instant, int value) { |
453 | if (iTimeField) { |
454 | int offset = getOffsetToAdd(instant); |
455 | long localInstant = iField.addWrapField(instant + offset, value); |
456 | return localInstant - offset; |
457 | } else { |
458 | long localInstant = iZone.convertUTCToLocal(instant); |
459 | localInstant = iField.addWrapField(localInstant, value); |
460 | return iZone.convertLocalToUTC(localInstant, false); |
461 | } |
462 | } |
463 | |
464 | public long set(long instant, int value) { |
465 | long localInstant = iZone.convertUTCToLocal(instant); |
466 | localInstant = iField.set(localInstant, value); |
467 | long result = iZone.convertLocalToUTC(localInstant, false); |
468 | if (get(result) != value) { |
469 | throw new IllegalFieldValueException(iField.getType(), new Integer(value), |
470 | "Illegal instant due to time zone offset transition: " + |
471 | DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss.SSS").print(new Instant(localInstant)) + |
472 | " (" + iZone.getID() + ")"); |
473 | } |
474 | return result; |
475 | } |
476 | |
477 | public long set(long instant, String text, Locale locale) { |
478 | // cannot verify that new value stuck because set may be lenient |
479 | long localInstant = iZone.convertUTCToLocal(instant); |
480 | localInstant = iField.set(localInstant, text, locale); |
481 | return iZone.convertLocalToUTC(localInstant, false); |
482 | } |
483 | |
484 | public int getDifference(long minuendInstant, long subtrahendInstant) { |
485 | int offset = getOffsetToAdd(subtrahendInstant); |
486 | return iField.getDifference |
487 | (minuendInstant + (iTimeField ? offset : getOffsetToAdd(minuendInstant)), |
488 | subtrahendInstant + offset); |
489 | } |
490 | |
491 | public long getDifferenceAsLong(long minuendInstant, long subtrahendInstant) { |
492 | int offset = getOffsetToAdd(subtrahendInstant); |
493 | return iField.getDifferenceAsLong |
494 | (minuendInstant + (iTimeField ? offset : getOffsetToAdd(minuendInstant)), |
495 | subtrahendInstant + offset); |
496 | } |
497 | |
498 | public final DurationField getDurationField() { |
499 | return iDurationField; |
500 | } |
501 | |
502 | public final DurationField getRangeDurationField() { |
503 | return iRangeDurationField; |
504 | } |
505 | |
506 | public boolean isLeap(long instant) { |
507 | long localInstant = iZone.convertUTCToLocal(instant); |
508 | return iField.isLeap(localInstant); |
509 | } |
510 | |
511 | public int getLeapAmount(long instant) { |
512 | long localInstant = iZone.convertUTCToLocal(instant); |
513 | return iField.getLeapAmount(localInstant); |
514 | } |
515 | |
516 | public final DurationField getLeapDurationField() { |
517 | return iLeapDurationField; |
518 | } |
519 | |
520 | public long roundFloor(long instant) { |
521 | if (iTimeField) { |
522 | int offset = getOffsetToAdd(instant); |
523 | instant = iField.roundFloor(instant + offset); |
524 | return instant - offset; |
525 | } else { |
526 | long localInstant = iZone.convertUTCToLocal(instant); |
527 | localInstant = iField.roundFloor(localInstant); |
528 | return iZone.convertLocalToUTC(localInstant, false); |
529 | } |
530 | } |
531 | |
532 | public long roundCeiling(long instant) { |
533 | if (iTimeField) { |
534 | int offset = getOffsetToAdd(instant); |
535 | instant = iField.roundCeiling(instant + offset); |
536 | return instant - offset; |
537 | } else { |
538 | long localInstant = iZone.convertUTCToLocal(instant); |
539 | localInstant = iField.roundCeiling(localInstant); |
540 | return iZone.convertLocalToUTC(localInstant, false); |
541 | } |
542 | } |
543 | |
544 | public long remainder(long instant) { |
545 | long localInstant = iZone.convertUTCToLocal(instant); |
546 | return iField.remainder(localInstant); |
547 | } |
548 | |
549 | public int getMinimumValue() { |
550 | return iField.getMinimumValue(); |
551 | } |
552 | |
553 | public int getMinimumValue(long instant) { |
554 | long localInstant = iZone.convertUTCToLocal(instant); |
555 | return iField.getMinimumValue(localInstant); |
556 | } |
557 | |
558 | public int getMinimumValue(ReadablePartial instant) { |
559 | return iField.getMinimumValue(instant); |
560 | } |
561 | |
562 | public int getMinimumValue(ReadablePartial instant, int[] values) { |
563 | return iField.getMinimumValue(instant, values); |
564 | } |
565 | |
566 | public int getMaximumValue() { |
567 | return iField.getMaximumValue(); |
568 | } |
569 | |
570 | public int getMaximumValue(long instant) { |
571 | long localInstant = iZone.convertUTCToLocal(instant); |
572 | return iField.getMaximumValue(localInstant); |
573 | } |
574 | |
575 | public int getMaximumValue(ReadablePartial instant) { |
576 | return iField.getMaximumValue(instant); |
577 | } |
578 | |
579 | public int getMaximumValue(ReadablePartial instant, int[] values) { |
580 | return iField.getMaximumValue(instant, values); |
581 | } |
582 | |
583 | public int getMaximumTextLength(Locale locale) { |
584 | return iField.getMaximumTextLength(locale); |
585 | } |
586 | |
587 | public int getMaximumShortTextLength(Locale locale) { |
588 | return iField.getMaximumShortTextLength(locale); |
589 | } |
590 | |
591 | private int getOffsetToAdd(long instant) { |
592 | int offset = this.iZone.getOffset(instant); |
593 | long sum = instant + offset; |
594 | // If there is a sign change, but the two values have the same sign... |
595 | if ((instant ^ sum) < 0 && (instant ^ offset) >= 0) { |
596 | throw new ArithmeticException("Adding time zone offset caused overflow"); |
597 | } |
598 | return offset; |
599 | } |
600 | } |
601 | |
602 | } |