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