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 */
016package org.joda.time.chrono;
017
018import java.util.HashMap;
019import java.util.Locale;
020
021import org.joda.time.Chronology;
022import org.joda.time.DateTimeConstants;
023import org.joda.time.DateTimeField;
024import org.joda.time.DateTimeZone;
025import org.joda.time.DurationField;
026import org.joda.time.IllegalFieldValueException;
027import org.joda.time.IllegalInstantException;
028import org.joda.time.ReadablePartial;
029import org.joda.time.field.BaseDateTimeField;
030import 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 */
041public 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}