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