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 }