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 }