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 }