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         long utcInstant = localInstant - offset;
141         int offsetBasedOnUtc = zone.getOffset(utcInstant);
142         if (offset != offsetBasedOnUtc) {
143             throw new IllegalInstantException(localInstant, zone.getID());
144         }
145         return utcInstant;
146     }
147
148     protected void assemble(Fields fields) {
149         // Keep a local cache of converted fields so as not to create redundant
150         // objects.
151         HashMap<Object, Object> converted = new HashMap<Object, Object>();
152
153         // Convert duration fields...
154
155         fields.eras = convertField(fields.eras, converted);
156         fields.centuries = convertField(fields.centuries, converted);
157         fields.years = convertField(fields.years, converted);
158         fields.months = convertField(fields.months, converted);
159         fields.weekyears = convertField(fields.weekyears, converted);
160         fields.weeks = convertField(fields.weeks, converted);
161         fields.days = convertField(fields.days, converted);
162
163         fields.halfdays = convertField(fields.halfdays, converted);
164         fields.hours = convertField(fields.hours, converted);
165         fields.minutes = convertField(fields.minutes, converted);
166         fields.seconds = convertField(fields.seconds, converted);
167         fields.millis = convertField(fields.millis, converted);
168
169         // Convert datetime fields...
170
171         fields.year = convertField(fields.year, converted);
172         fields.yearOfEra = convertField(fields.yearOfEra, converted);
173         fields.yearOfCentury = convertField(fields.yearOfCentury, converted);
174         fields.centuryOfEra = convertField(fields.centuryOfEra, converted);
175         fields.era = convertField(fields.era, converted);
176         fields.dayOfWeek = convertField(fields.dayOfWeek, converted);
177         fields.dayOfMonth = convertField(fields.dayOfMonth, converted);
178         fields.dayOfYear = convertField(fields.dayOfYear, converted);
179         fields.monthOfYear = convertField(fields.monthOfYear, converted);
180         fields.weekOfWeekyear = convertField(fields.weekOfWeekyear, converted);
181         fields.weekyear = convertField(fields.weekyear, converted);
182         fields.weekyearOfCentury = convertField(fields.weekyearOfCentury, converted);
183
184         fields.millisOfSecond = convertField(fields.millisOfSecond, converted);
185         fields.millisOfDay = convertField(fields.millisOfDay, converted);
186         fields.secondOfMinute = convertField(fields.secondOfMinute, converted);
187         fields.secondOfDay = convertField(fields.secondOfDay, converted);
188         fields.minuteOfHour = convertField(fields.minuteOfHour, converted);
189         fields.minuteOfDay = convertField(fields.minuteOfDay, converted);
190         fields.hourOfDay = convertField(fields.hourOfDay, converted);
191         fields.hourOfHalfday = convertField(fields.hourOfHalfday, converted);
192         fields.clockhourOfDay = convertField(fields.clockhourOfDay, converted);
193         fields.clockhourOfHalfday = convertField(fields.clockhourOfHalfday, converted);
194         fields.halfdayOfDay = convertField(fields.halfdayOfDay, converted);
195     }
196
197     private DurationField convertField(DurationField field, HashMap<Object, Object> converted) {
198         if (field == null || !field.isSupported()) {
199             return field;
200         }
201         if (converted.containsKey(field)) {
202             return (DurationField)converted.get(field);
203         }
204         ZonedDurationField zonedField = new ZonedDurationField(field, getZone());
205         converted.put(field, zonedField);
206         return zonedField;
207     }
208
209     private DateTimeField convertField(DateTimeField field, HashMap<Object, Object> converted) {
210         if (field == null || !field.isSupported()) {
211             return field;
212         }
213         if (converted.containsKey(field)) {
214             return (DateTimeField)converted.get(field);
215         }
216         ZonedDateTimeField zonedField =
217             new ZonedDateTimeField(field, getZone(),
218                                    convertField(field.getDurationField(), converted),
219                                    convertField(field.getRangeDurationField(), converted),
220                                    convertField(field.getLeapDurationField(), converted));
221         converted.put(field, zonedField);
222         return zonedField;
223     }
224
225     //-----------------------------------------------------------------------
226     /**
227      * A zoned chronology is only equal to a zoned chronology with the
228      * same base chronology and zone.
229      * 
230      * @param obj  the object to compare to
231      * @return true if equal
232      * @since 1.4
233      */

234     public boolean equals(Object obj) {
235         if (this == obj) {
236             return true;
237         }
238         if (obj instanceof ZonedChronology == false) {
239             return false;
240         }
241         ZonedChronology chrono = (ZonedChronology) obj;
242         return
243             getBase().equals(chrono.getBase()) &&
244             getZone().equals(chrono.getZone());
245     }
246
247     /**
248      * A suitable hashcode for the chronology.
249      * 
250      * @return the hashcode
251      * @since 1.4
252      */

253     public int hashCode() {
254         return 326565 + getZone().hashCode() * 11 + getBase().hashCode() * 7;
255     }
256
257     /**
258      * A debugging string for the chronology.
259      * 
260      * @return the debugging string
261      */

262     public String toString() {
263         return "ZonedChronology[" + getBase() + ", " + getZone().getID() + ']';
264     }
265
266     //-----------------------------------------------------------------------
267     /*
268      * Because time durations are typically smaller than time zone offsets, the
269      * arithmetic methods subtract the original offset. This produces a more
270      * expected behavior when crossing time zone offset transitions. For dates,
271      * the new offset is subtracted off. This behavior, if applied to time
272      * fields, can nullify or reverse an add when crossing a transition.
273      */

274     static class ZonedDurationField extends BaseDurationField {
275         private static final long serialVersionUID = -485345310999208286L;
276
277         final DurationField iField;
278         final boolean iTimeField;
279         final DateTimeZone iZone;
280
281         ZonedDurationField(DurationField field, DateTimeZone zone) {
282             super(field.getType());
283             if (!field.isSupported()) {
284                 throw new IllegalArgumentException();
285             }
286             iField = field;
287             iTimeField = useTimeArithmetic(field);
288             iZone = zone;
289         }
290
291         public boolean isPrecise() {
292             return iTimeField ? iField.isPrecise() : iField.isPrecise() && this.iZone.isFixed();
293         }
294
295         public long getUnitMillis() {
296             return iField.getUnitMillis();
297         }
298
299         public int getValue(long duration, long instant) {
300             return iField.getValue(duration, addOffset(instant));
301         }
302
303         public long getValueAsLong(long duration, long instant) {
304             return iField.getValueAsLong(duration, addOffset(instant));
305         }
306
307         public long getMillis(int value, long instant) {
308             return iField.getMillis(value, addOffset(instant));
309         }
310
311         public long getMillis(long value, long instant) {
312             return iField.getMillis(value, addOffset(instant));
313         }
314
315         public long add(long instant, int value) {
316             int offset = getOffsetToAdd(instant);
317             instant = iField.add(instant + offset, value);
318             return instant - (iTimeField ? offset : getOffsetFromLocalToSubtract(instant));
319         }
320
321         public long add(long instant, long value) {
322             int offset = getOffsetToAdd(instant);
323             instant = iField.add(instant + offset, value);
324             return instant - (iTimeField ? offset : getOffsetFromLocalToSubtract(instant));
325         }
326
327         public int getDifference(long minuendInstant, long subtrahendInstant) {
328             int offset = getOffsetToAdd(subtrahendInstant);
329             return iField.getDifference
330                 (minuendInstant + (iTimeField ? offset : getOffsetToAdd(minuendInstant)),
331                  subtrahendInstant + offset);
332         }
333
334         public long getDifferenceAsLong(long minuendInstant, long subtrahendInstant) {
335             int offset = getOffsetToAdd(subtrahendInstant);
336             return iField.getDifferenceAsLong
337                 (minuendInstant + (iTimeField ? offset : getOffsetToAdd(minuendInstant)),
338                  subtrahendInstant + offset);
339         }
340
341         private int getOffsetToAdd(long instant) {
342             int offset = this.iZone.getOffset(instant);
343             long sum = instant + offset;
344             // If there is a sign change, but the two values have the same sign...
345             if ((instant ^ sum) < 0 && (instant ^ offset) >= 0) {
346                 throw new ArithmeticException("Adding time zone offset caused overflow");
347             }
348             return offset;
349         }
350
351         private int getOffsetFromLocalToSubtract(long instant) {
352             int offset = this.iZone.getOffsetFromLocal(instant);
353             long diff = instant - offset;
354             // If there is a sign change, but the two values have different signs...
355             if ((instant ^ diff) < 0 && (instant ^ offset) < 0) {
356                 throw new ArithmeticException("Subtracting time zone offset caused overflow");
357             }
358             return offset;
359         }
360
361         private long addOffset(long instant) {
362             return iZone.convertUTCToLocal(instant);
363         }
364
365         @Override
366         public boolean equals(Object obj) {
367             if (this == obj) {
368                 return true;
369             } else if (obj instanceof ZonedDurationField) {
370                 ZonedDurationField other = (ZonedDurationField) obj;
371                 return iField.equals(other.iField) &&
372                        iZone.equals(other.iZone);
373             }
374             return false;
375         }
376
377         @Override
378         public int hashCode() {
379             return iField.hashCode() ^ iZone.hashCode();
380         }
381     }
382
383     /**
384      * A DateTimeField that decorates another to add timezone behaviour.
385      * <p>
386      * This class converts passed in instants to local wall time, and vice
387      * versa on output.
388      */

389     static final class ZonedDateTimeField extends BaseDateTimeField {
390         @SuppressWarnings("unused")
391         private static final long serialVersionUID = -3968986277775529794L;
392
393         final DateTimeField iField;
394         final DateTimeZone iZone;
395         final DurationField iDurationField;
396         final boolean iTimeField;
397         final DurationField iRangeDurationField;
398         final DurationField iLeapDurationField;
399
400         ZonedDateTimeField(DateTimeField field,
401                            DateTimeZone zone,
402                            DurationField durationField,
403                            DurationField rangeDurationField,
404                            DurationField leapDurationField) {
405             super(field.getType());
406             if (!field.isSupported()) {
407                 throw new IllegalArgumentException();
408             }
409             iField = field;
410             iZone = zone;
411             iDurationField = durationField;
412             iTimeField = useTimeArithmetic(durationField);
413             iRangeDurationField = rangeDurationField;
414             iLeapDurationField = leapDurationField;
415         }
416
417         public boolean isLenient() {
418             return iField.isLenient();
419         }
420
421         public int get(long instant) {
422             long localInstant = iZone.convertUTCToLocal(instant);
423             return iField.get(localInstant);
424         }
425
426         public String getAsText(long instant, Locale locale) {
427             long localInstant = iZone.convertUTCToLocal(instant);
428             return iField.getAsText(localInstant, locale);
429         }
430
431         public String getAsShortText(long instant, Locale locale) {
432             long localInstant = iZone.convertUTCToLocal(instant);
433             return iField.getAsShortText(localInstant, locale);
434         }
435
436         public String getAsText(int fieldValue, Locale locale) {
437             return iField.getAsText(fieldValue, locale);
438         }
439
440         public String getAsShortText(int fieldValue, Locale locale) {
441             return iField.getAsShortText(fieldValue, locale);
442         }
443
444         public long add(long instant, int value) {
445             if (iTimeField) {
446                 int offset = getOffsetToAdd(instant);
447                 long localInstant = iField.add(instant + offset, value);
448                 return localInstant - offset;
449             } else {
450                long localInstant = iZone.convertUTCToLocal(instant);
451                localInstant = iField.add(localInstant, value);
452                return iZone.convertLocalToUTC(localInstant, false, instant);
453             }
454         }
455
456         public long add(long instant, long value) {
457             if (iTimeField) {
458                 int offset = getOffsetToAdd(instant);
459                 long localInstant = iField.add(instant + offset, value);
460                 return localInstant - offset;
461             } else {
462                long localInstant = iZone.convertUTCToLocal(instant);
463                localInstant = iField.add(localInstant, value);
464                return iZone.convertLocalToUTC(localInstant, false, instant);
465             }
466         }
467
468         public long addWrapField(long instant, int value) {
469             if (iTimeField) {
470                 int offset = getOffsetToAdd(instant);
471                 long localInstant = iField.addWrapField(instant + offset, value);
472                 return localInstant - offset;
473             } else {
474                 long localInstant = iZone.convertUTCToLocal(instant);
475                 localInstant = iField.addWrapField(localInstant, value);
476                 return iZone.convertLocalToUTC(localInstant, false, instant);
477             }
478         }
479
480         public long set(long instant, int value) {
481             long localInstant = iZone.convertUTCToLocal(instant);
482             localInstant = iField.set(localInstant, value);
483             long result = iZone.convertLocalToUTC(localInstant, false, instant);
484             if (get(result) != value) {
485                 IllegalInstantException cause = new IllegalInstantException(localInstant,  iZone.getID());
486                 IllegalFieldValueException ex = new IllegalFieldValueException(iField.getType(), Integer.valueOf(value), cause.getMessage());
487                 ex.initCause(cause);
488                 throw ex;
489             }
490             return result;
491         }
492
493         public long set(long instant, String text, Locale locale) {
494             // cannot verify that new value stuck because set may be lenient
495             long localInstant = iZone.convertUTCToLocal(instant);
496             localInstant = iField.set(localInstant, text, locale);
497             return iZone.convertLocalToUTC(localInstant, false, instant);
498         }
499
500         public int getDifference(long minuendInstant, long subtrahendInstant) {
501             int offset = getOffsetToAdd(subtrahendInstant);
502             return iField.getDifference
503                 (minuendInstant + (iTimeField ? offset : getOffsetToAdd(minuendInstant)),
504                  subtrahendInstant + offset);
505         }
506
507         public long getDifferenceAsLong(long minuendInstant, long subtrahendInstant) {
508             int offset = getOffsetToAdd(subtrahendInstant);
509             return iField.getDifferenceAsLong
510                 (minuendInstant + (iTimeField ? offset : getOffsetToAdd(minuendInstant)),
511                  subtrahendInstant + offset);
512         }
513
514         public final DurationField getDurationField() {
515             return iDurationField;
516         }
517
518         public final DurationField getRangeDurationField() {
519             return iRangeDurationField;
520         }
521
522         public boolean isLeap(long instant) {
523             long localInstant = iZone.convertUTCToLocal(instant);
524             return iField.isLeap(localInstant);
525         }
526
527         public int getLeapAmount(long instant) {
528             long localInstant = iZone.convertUTCToLocal(instant);
529             return iField.getLeapAmount(localInstant);
530         }
531
532         public final DurationField getLeapDurationField() {
533             return iLeapDurationField;
534         }
535
536         public long roundFloor(long instant) {
537             if (iTimeField) {
538                 int offset = getOffsetToAdd(instant);
539                 instant = iField.roundFloor(instant + offset);
540                 return instant - offset;
541             } else {
542                 long localInstant = iZone.convertUTCToLocal(instant);
543                 localInstant = iField.roundFloor(localInstant);
544                 return iZone.convertLocalToUTC(localInstant, false, instant);
545             }
546         }
547
548         public long roundCeiling(long instant) {
549             if (iTimeField) {
550                 int offset = getOffsetToAdd(instant);
551                 instant = iField.roundCeiling(instant + offset);
552                 return instant - offset;
553             } else {
554                 long localInstant = iZone.convertUTCToLocal(instant);
555                 localInstant = iField.roundCeiling(localInstant);
556                 return iZone.convertLocalToUTC(localInstant, false, instant);
557             }
558         }
559
560         public long remainder(long instant) {
561             long localInstant = iZone.convertUTCToLocal(instant);
562             return iField.remainder(localInstant);
563         }
564
565         public int getMinimumValue() {
566             return iField.getMinimumValue();
567         }
568
569         public int getMinimumValue(long instant) {
570             long localInstant = iZone.convertUTCToLocal(instant);
571             return iField.getMinimumValue(localInstant);
572         }
573
574         public int getMinimumValue(ReadablePartial instant) {
575             return iField.getMinimumValue(instant);
576         }
577
578         public int getMinimumValue(ReadablePartial instant, int[] values) {
579             return iField.getMinimumValue(instant, values);
580         }
581
582         public int getMaximumValue() {
583             return iField.getMaximumValue();
584         }
585
586         public int getMaximumValue(long instant) {
587             long localInstant = iZone.convertUTCToLocal(instant);
588             return iField.getMaximumValue(localInstant);
589         }
590
591         public int getMaximumValue(ReadablePartial instant) {
592             return iField.getMaximumValue(instant);
593         }
594
595         public int getMaximumValue(ReadablePartial instant, int[] values) {
596             return iField.getMaximumValue(instant, values);
597         }
598
599         public int getMaximumTextLength(Locale locale) {
600             return iField.getMaximumTextLength(locale);
601         }
602
603         public int getMaximumShortTextLength(Locale locale) {
604             return iField.getMaximumShortTextLength(locale);
605         }
606
607         private int getOffsetToAdd(long instant) {
608             int offset = this.iZone.getOffset(instant);
609             long sum = instant + offset;
610             // If there is a sign change, but the two values have the same sign...
611             if ((instant ^ sum) < 0 && (instant ^ offset) >= 0) {
612                 throw new ArithmeticException("Adding time zone offset caused overflow");
613             }
614             return offset;
615         }
616
617         @Override
618         public boolean equals(Object obj) {
619             if (this == obj) {
620                 return true;
621             } else if (obj instanceof ZonedDateTimeField) {
622                 ZonedDateTimeField other = (ZonedDateTimeField) obj;
623                 return iField.equals(other.iField) &&
624                        iZone.equals(other.iZone) &&
625                        iDurationField.equals(other.iDurationField) &&
626                        iRangeDurationField.equals(other.iRangeDurationField);
627             }
628             return false;
629         }
630
631         @Override
632         public int hashCode() {
633             return iField.hashCode() ^ iZone.hashCode();
634         }
635     }
636
637 }
638