001    /*
002     *  Copyright 2001-2013 Stephen Colebourne
003     *
004     *  Licensed under the Apache License, Version 2.0 (the "License");
005     *  you may not use this file except in compliance with the License.
006     *  You may obtain a copy of the License at
007     *
008     *      http://www.apache.org/licenses/LICENSE-2.0
009     *
010     *  Unless required by applicable law or agreed to in writing, software
011     *  distributed under the License is distributed on an "AS IS" BASIS,
012     *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013     *  See the License for the specific language governing permissions and
014     *  limitations under the License.
015     */
016    package org.joda.time.chrono;
017    
018    import java.util.HashMap;
019    import java.util.Locale;
020    
021    import org.joda.time.Chronology;
022    import org.joda.time.DateTimeConstants;
023    import org.joda.time.DateTimeField;
024    import org.joda.time.DateTimeZone;
025    import org.joda.time.DurationField;
026    import org.joda.time.IllegalFieldValueException;
027    import org.joda.time.IllegalInstantException;
028    import org.joda.time.ReadablePartial;
029    import org.joda.time.field.BaseDateTimeField;
030    import org.joda.time.field.BaseDurationField;
031    
032    /**
033     * Wraps another Chronology to add support for time zones.
034     * <p>
035     * ZonedChronology is thread-safe and immutable.
036     *
037     * @author Brian S O'Neill
038     * @author Stephen Colebourne
039     * @since 1.0
040     */
041    public final class ZonedChronology extends AssembledChronology {
042    
043        /** Serialization lock */
044        private static final long serialVersionUID = -1079258847191166848L;
045    
046        /**
047         * Create a ZonedChronology for any chronology, overriding any time zone it
048         * may already have.
049         *
050         * @param base base chronology to wrap
051         * @param zone the time zone
052         * @throws IllegalArgumentException if chronology or time zone is null
053         */
054        public static ZonedChronology getInstance(Chronology base, DateTimeZone zone) {
055            if (base == null) {
056                throw new IllegalArgumentException("Must supply a chronology");
057            }
058            base = base.withUTC();
059            if (base == null) {
060                throw new IllegalArgumentException("UTC chronology must not be null");
061            }
062            if (zone == null) {
063                throw new IllegalArgumentException("DateTimeZone must not be null");
064            }
065            return new ZonedChronology(base, zone);
066        }
067    
068        static boolean useTimeArithmetic(DurationField field) {
069            // Use time of day arithmetic rules for unit durations less than
070            // typical time zone offsets.
071            return field != null && field.getUnitMillis() < DateTimeConstants.MILLIS_PER_HOUR * 12;
072        }
073    
074        /**
075         * Restricted constructor
076         *
077         * @param base base chronology to wrap
078         * @param zone the time zone
079         */
080        private ZonedChronology(Chronology base, DateTimeZone zone) {
081            super(base, zone);
082        }
083    
084        public DateTimeZone getZone() {
085            return (DateTimeZone)getParam();
086        }
087    
088        public Chronology withUTC() {
089            return getBase();
090        }
091    
092        public Chronology withZone(DateTimeZone zone) {
093            if (zone == null) {
094                zone = DateTimeZone.getDefault();
095            }
096            if (zone == getParam()) {
097                return this;
098            }
099            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    }