View Javadoc

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