001/* 002 * Copyright (c) 2007-2013, Stephen Colebourne & Michael Nascimento Santos 003 * 004 * All rights reserved. 005 * 006 * Redistribution and use in source and binary forms, with or without 007 * modification, are permitted provided that the following conditions are met: 008 * 009 * * Redistributions of source code must retain the above copyright notice, 010 * this list of conditions and the following disclaimer. 011 * 012 * * Redistributions in binary form must reproduce the above copyright notice, 013 * this list of conditions and the following disclaimer in the documentation 014 * and/or other materials provided with the distribution. 015 * 016 * * Neither the name of JSR-310 nor the names of its contributors 017 * may be used to endorse or promote products derived from this software 018 * without specific prior written permission. 019 * 020 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 021 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 022 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 023 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR 024 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 025 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 026 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 027 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 028 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 029 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 030 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 031 */ 032package org.threeten.bp.zone; 033 034import static org.threeten.bp.temporal.TemporalAdjusters.nextOrSame; 035import static org.threeten.bp.temporal.TemporalAdjusters.previousOrSame; 036 037import java.io.DataInput; 038import java.io.DataOutput; 039import java.io.IOException; 040import java.io.Serializable; 041import java.util.Objects; 042 043import org.threeten.bp.DayOfWeek; 044import org.threeten.bp.LocalDate; 045import org.threeten.bp.LocalDateTime; 046import org.threeten.bp.LocalTime; 047import org.threeten.bp.Month; 048import org.threeten.bp.ZoneOffset; 049import org.threeten.bp.temporal.ISOChrono; 050 051/** 052 * A rule expressing how to create a transition. 053 * <p> 054 * This class allows rules for identifying future transitions to be expressed. 055 * A rule might be written in many forms: 056 * <p><ul> 057 * <li>the 16th March 058 * <li>the Sunday on or after the 16th March 059 * <li>the Sunday on or before the 16th March 060 * <li>the last Sunday in February 061 * </ul><p> 062 * These different rule types can be expressed and queried. 063 * 064 * <h3>Specification for implementors</h3> 065 * This class is immutable and thread-safe. 066 */ 067public final class ZoneOffsetTransitionRule implements Serializable { 068 069 /** 070 * Serialization version. 071 */ 072 private static final long serialVersionUID = 6889046316657758795L; 073 074 /** 075 * The month of the month-day of the first day of the cutover week. 076 * The actual date will be adjusted by the dowChange field. 077 */ 078 private final Month month; 079 /** 080 * The day-of-month of the month-day of the cutover week. 081 * If positive, it is the start of the week where the cutover can occur. 082 * If negative, it represents the end of the week where cutover can occur. 083 * The value is the number of days from the end of the month, such that 084 * {@code -1} is the last day of the month, {@code -2} is the second 085 * to last day, and so on. 086 */ 087 private final byte dom; 088 /** 089 * The cutover day-of-week, null to retain the day-of-month. 090 */ 091 private final DayOfWeek dow; 092 /** 093 * The cutover time in the 'before' offset. 094 */ 095 private final LocalTime time; 096 /** 097 * Whether the cutover time is midnight at the end of day. 098 */ 099 private final boolean timeEndOfDay; 100 /** 101 * The definition of how the local time should be interpreted. 102 */ 103 private final TimeDefinition timeDefinition; 104 /** 105 * The standard offset at the cutover. 106 */ 107 private final ZoneOffset standardOffset; 108 /** 109 * The offset before the cutover. 110 */ 111 private final ZoneOffset offsetBefore; 112 /** 113 * The offset after the cutover. 114 */ 115 private final ZoneOffset offsetAfter; 116 117 /** 118 * Obtains an instance defining the yearly rule to create transitions between two offsets. 119 * <p> 120 * Applications should normally obtain an instance from {@link ZoneRules}. 121 * This factory is only intended for use when creating {@link ZoneRules}. 122 * 123 * @param month the month of the month-day of the first day of the cutover week, not null 124 * @param dayOfMonthIndicator the day of the month-day of the cutover week, positive if the week is that 125 * day or later, negative if the week is that day or earlier, counting from the last day of the month, 126 * from -28 to 31 excluding 0 127 * @param dayOfWeek the required day-of-week, null if the month-day should not be changed 128 * @param time the cutover time in the 'before' offset, not null 129 * @param timeEndOfDay whether the time is midnight at the end of day 130 * @param timeDefnition how to interpret the cutover 131 * @param standardOffset the standard offset in force at the cutover, not null 132 * @param offsetBefore the offset before the cutover, not null 133 * @param offsetAfter the offset after the cutover, not null 134 * @return the rule, not null 135 * @throws IllegalArgumentException if the day of month indicator is invalid 136 * @throws IllegalArgumentException if the end of day flag is true when the time is not midnight 137 */ 138 public static ZoneOffsetTransitionRule of( 139 Month month, 140 int dayOfMonthIndicator, 141 DayOfWeek dayOfWeek, 142 LocalTime time, 143 boolean timeEndOfDay, 144 TimeDefinition timeDefnition, 145 ZoneOffset standardOffset, 146 ZoneOffset offsetBefore, 147 ZoneOffset offsetAfter) { 148 Objects.requireNonNull(month, "month"); 149 Objects.requireNonNull(time, "time"); 150 Objects.requireNonNull(timeDefnition, "timeDefnition"); 151 Objects.requireNonNull(standardOffset, "standardOffset"); 152 Objects.requireNonNull(offsetBefore, "offsetBefore"); 153 Objects.requireNonNull(offsetAfter, "offsetAfter"); 154 if (dayOfMonthIndicator < -28 || dayOfMonthIndicator > 31 || dayOfMonthIndicator == 0) { 155 throw new IllegalArgumentException("Day of month indicator must be between -28 and 31 inclusive excluding zero"); 156 } 157 if (timeEndOfDay && time.equals(LocalTime.MIDNIGHT) == false) { 158 throw new IllegalArgumentException("Time must be midnight when end of day flag is true"); 159 } 160 return new ZoneOffsetTransitionRule(month, dayOfMonthIndicator, dayOfWeek, time, timeEndOfDay, timeDefnition, standardOffset, offsetBefore, offsetAfter); 161 } 162 163 /** 164 * Creates an instance defining the yearly rule to create transitions between two offsets. 165 * 166 * @param month the month of the month-day of the first day of the cutover week, not null 167 * @param dayOfMonthIndicator the day of the month-day of the cutover week, positive if the week is that 168 * day or later, negative if the week is that day or earlier, counting from the last day of the month, 169 * from -28 to 31 excluding 0 170 * @param dayOfWeek the required day-of-week, null if the month-day should not be changed 171 * @param time the cutover time in the 'before' offset, not null 172 * @param timeEndOfDay whether the time is midnight at the end of day 173 * @param timeDefnition how to interpret the cutover 174 * @param standardOffset the standard offset in force at the cutover, not null 175 * @param offsetBefore the offset before the cutover, not null 176 * @param offsetAfter the offset after the cutover, not null 177 * @throws IllegalArgumentException if the day of month indicator is invalid 178 * @throws IllegalArgumentException if the end of day flag is true when the time is not midnight 179 */ 180 ZoneOffsetTransitionRule( 181 Month month, 182 int dayOfMonthIndicator, 183 DayOfWeek dayOfWeek, 184 LocalTime time, 185 boolean timeEndOfDay, 186 TimeDefinition timeDefnition, 187 ZoneOffset standardOffset, 188 ZoneOffset offsetBefore, 189 ZoneOffset offsetAfter) { 190 this.month = month; 191 this.dom = (byte) dayOfMonthIndicator; 192 this.dow = dayOfWeek; 193 this.time = time; 194 this.timeEndOfDay = timeEndOfDay; 195 this.timeDefinition = timeDefnition; 196 this.standardOffset = standardOffset; 197 this.offsetBefore = offsetBefore; 198 this.offsetAfter = offsetAfter; 199 } 200 201 //----------------------------------------------------------------------- 202 /** 203 * Uses a serialization delegate. 204 * 205 * @return the replacing object, not null 206 */ 207 private Object writeReplace() { 208 return new Ser(Ser.ZOTRULE, this); 209 } 210 211 /** 212 * Writes the state to the stream. 213 * 214 * @param out the output stream, not null 215 * @throws IOException if an error occurs 216 */ 217 void writeExternal(DataOutput out) throws IOException { 218 final int timeSecs = (timeEndOfDay ? 86400 : time.toSecondOfDay()); 219 final int stdOffset = standardOffset.getTotalSeconds(); 220 final int beforeDiff = offsetBefore.getTotalSeconds() - stdOffset; 221 final int afterDiff = offsetAfter.getTotalSeconds() - stdOffset; 222 final int timeByte = (timeSecs % 3600 == 0 ? (timeEndOfDay ? 24 : time.getHour()) : 31); 223 final int stdOffsetByte = (stdOffset % 900 == 0 ? stdOffset / 900 + 128 : 255); 224 final int beforeByte = (beforeDiff == 0 || beforeDiff == 1800 || beforeDiff == 3600 ? beforeDiff / 1800 : 3); 225 final int afterByte = (afterDiff == 0 || afterDiff == 1800 || afterDiff == 3600 ? afterDiff / 1800 : 3); 226 final int dowByte = (dow == null ? 0 : dow.getValue()); 227 int b = (month.getValue() << 28) + // 4 bits 228 ((dom + 32) << 22) + // 6 bits 229 (dowByte << 19) + // 3 bits 230 (timeByte << 14) + // 5 bits 231 (timeDefinition.ordinal() << 12) + // 2 bits 232 (stdOffsetByte << 4) + // 8 bits 233 (beforeByte << 2) + // 2 bits 234 afterByte; // 2 bits 235 out.writeInt(b); 236 if (timeByte == 31) { 237 out.writeInt(timeSecs); 238 } 239 if (stdOffsetByte == 255) { 240 out.writeInt(stdOffset); 241 } 242 if (beforeByte == 3) { 243 out.writeInt(offsetBefore.getTotalSeconds()); 244 } 245 if (afterByte == 3) { 246 out.writeInt(offsetAfter.getTotalSeconds()); 247 } 248 } 249 250 /** 251 * Reads the state from the stream. 252 * 253 * @param in the input stream, not null 254 * @return the created object, not null 255 * @throws IOException if an error occurs 256 */ 257 static ZoneOffsetTransitionRule readExternal(DataInput in) throws IOException { 258 int data = in.readInt(); 259 Month month = Month.of(data >>> 28); 260 int dom = ((data & (63 << 22)) >>> 22) - 32; 261 int dowByte = (data & (7 << 19)) >>> 19; 262 DayOfWeek dow = dowByte == 0 ? null : DayOfWeek.of(dowByte); 263 int timeByte = (data & (31 << 14)) >>> 14; 264 TimeDefinition defn = TimeDefinition.values()[(data & (3 << 12)) >>> 12]; 265 int stdByte = (data & (255 << 4)) >>> 4; 266 int beforeByte = (data & (3 << 2)) >>> 2; 267 int afterByte = (data & 3); 268 LocalTime time = (timeByte == 31 ? LocalTime.ofSecondOfDay(in.readInt()) : LocalTime.of(timeByte % 24, 0)); 269 ZoneOffset std = (stdByte == 255 ? ZoneOffset.ofTotalSeconds(in.readInt()) : ZoneOffset.ofTotalSeconds((stdByte - 128) * 900)); 270 ZoneOffset before = (beforeByte == 3 ? ZoneOffset.ofTotalSeconds(in.readInt()) : ZoneOffset.ofTotalSeconds(std.getTotalSeconds() + beforeByte * 1800)); 271 ZoneOffset after = (afterByte == 3 ? ZoneOffset.ofTotalSeconds(in.readInt()) : ZoneOffset.ofTotalSeconds(std.getTotalSeconds() + afterByte * 1800)); 272 return ZoneOffsetTransitionRule.of(month, dom, dow, time, timeByte == 24, defn, std, before, after); 273 } 274 275 //----------------------------------------------------------------------- 276 /** 277 * Gets the month of the transition. 278 * <p> 279 * If the rule defines an exact date then the month is the month of that date. 280 * <p> 281 * If the rule defines a week where the transition might occur, then the month 282 * if the month of either the earliest or latest possible date of the cutover. 283 * 284 * @return the month of the transition, not null 285 */ 286 public Month getMonth() { 287 return month; 288 } 289 290 /** 291 * Gets the indicator of the day-of-month of the transition. 292 * <p> 293 * If the rule defines an exact date then the day is the month of that date. 294 * <p> 295 * If the rule defines a week where the transition might occur, then the day 296 * defines either the start of the end of the transition week. 297 * <p> 298 * If the value is positive, then it represents a normal day-of-month, and is the 299 * earliest possible date that the transition can be. 300 * The date may refer to 29th February which should be treated as 1st March in non-leap years. 301 * <p> 302 * If the value is negative, then it represents the number of days back from the 303 * end of the month where {@code -1} is the last day of the month. 304 * In this case, the day identified is the latest possible date that the transition can be. 305 * 306 * @return the day-of-month indicator, from -28 to 31 excluding 0 307 */ 308 public int getDayOfMonthIndicator() { 309 return dom; 310 } 311 312 /** 313 * Gets the day-of-week of the transition. 314 * <p> 315 * If the rule defines an exact date then this returns null. 316 * <p> 317 * If the rule defines a week where the cutover might occur, then this method 318 * returns the day-of-week that the month-day will be adjusted to. 319 * If the day is positive then the adjustment is later. 320 * If the day is negative then the adjustment is earlier. 321 * 322 * @return the day-of-week that the transition occurs, null if the rule defines an exact date 323 */ 324 public DayOfWeek getDayOfWeek() { 325 return dow; 326 } 327 328 /** 329 * Gets the local time of day of the transition which must be checked with 330 * {@link #isMidnightEndOfDay()}. 331 * <p> 332 * The time is converted into an instant using the time definition. 333 * 334 * @return the local time of day of the transition, not null 335 */ 336 public LocalTime getLocalTime() { 337 return time; 338 } 339 340 /** 341 * Is the transition local time midnight at the end of day. 342 * <p> 343 * The transition may be represented as occurring at 24:00. 344 * 345 * @return whether a local time of midnight is at the start or end of the day 346 */ 347 public boolean isMidnightEndOfDay() { 348 return timeEndOfDay; 349 } 350 351 /** 352 * Gets the time definition, specifying how to convert the time to an instant. 353 * <p> 354 * The local time can be converted to an instant using the standard offset, 355 * the wall offset or UTC. 356 * 357 * @return the time definition, not null 358 */ 359 public TimeDefinition getTimeDefinition() { 360 return timeDefinition; 361 } 362 363 /** 364 * Gets the standard offset in force at the transition. 365 * 366 * @return the standard offset, not null 367 */ 368 public ZoneOffset getStandardOffset() { 369 return standardOffset; 370 } 371 372 /** 373 * Gets the offset before the transition. 374 * 375 * @return the offset before, not null 376 */ 377 public ZoneOffset getOffsetBefore() { 378 return offsetBefore; 379 } 380 381 /** 382 * Gets the offset after the transition. 383 * 384 * @return the offset after, not null 385 */ 386 public ZoneOffset getOffsetAfter() { 387 return offsetAfter; 388 } 389 390 //----------------------------------------------------------------------- 391 /** 392 * Creates a transition instance for the specified year. 393 * <p> 394 * Calculations are performed using the ISO-8601 chronology. 395 * 396 * @param year the year to create a transition for, not null 397 * @return the transition instance, not null 398 */ 399 public ZoneOffsetTransition createTransition(int year) { 400 LocalDate date; 401 if (dom < 0) { 402 date = LocalDate.of(year, month, month.length(ISOChrono.INSTANCE.isLeapYear(year)) + 1 + dom); 403 if (dow != null) { 404 date = date.with(previousOrSame(dow)); 405 } 406 } else { 407 date = LocalDate.of(year, month, dom); 408 if (dow != null) { 409 date = date.with(nextOrSame(dow)); 410 } 411 } 412 if (timeEndOfDay) { 413 date = date.plusDays(1); 414 } 415 LocalDateTime localDT = LocalDateTime.of(date, time); 416 LocalDateTime transition = timeDefinition.createDateTime(localDT, standardOffset, offsetBefore); 417 return new ZoneOffsetTransition(transition, offsetBefore, offsetAfter); 418 } 419 420 //----------------------------------------------------------------------- 421 /** 422 * Checks if this object equals another. 423 * <p> 424 * The entire state of the object is compared. 425 * 426 * @param otherRule the other object to compare to, null returns false 427 * @return true if equal 428 */ 429 @Override 430 public boolean equals(Object otherRule) { 431 if (otherRule == this) { 432 return true; 433 } 434 if (otherRule instanceof ZoneOffsetTransitionRule) { 435 ZoneOffsetTransitionRule other = (ZoneOffsetTransitionRule) otherRule; 436 return month == other.month && dom == other.dom && dow == other.dow && 437 timeDefinition == other.timeDefinition && 438 time.equals(other.time) && 439 timeEndOfDay == other.timeEndOfDay && 440 standardOffset.equals(other.standardOffset) && 441 offsetBefore.equals(other.offsetBefore) && 442 offsetAfter.equals(other.offsetAfter); 443 } 444 return false; 445 } 446 447 /** 448 * Returns a suitable hash code. 449 * 450 * @return the hash code 451 */ 452 @Override 453 public int hashCode() { 454 int hash = ((time.toSecondOfDay() + (timeEndOfDay ? 1 : 0)) << 15) + 455 (month.ordinal() << 11) + ((dom + 32) << 5) + 456 ((dow == null ? 7 : dow.ordinal()) << 2) + (timeDefinition.ordinal()); 457 return hash ^ standardOffset.hashCode() ^ 458 offsetBefore.hashCode() ^ offsetAfter.hashCode(); 459 } 460 461 //----------------------------------------------------------------------- 462 /** 463 * Returns a string describing this object. 464 * 465 * @return a string for debugging, not null 466 */ 467 @Override 468 public String toString() { 469 StringBuilder buf = new StringBuilder(); 470 buf.append("TransitionRule[") 471 .append(offsetBefore.compareTo(offsetAfter) > 0 ? "Gap " : "Overlap ") 472 .append(offsetBefore).append(" to ").append(offsetAfter).append(", "); 473 if (dow != null) { 474 if (dom == -1) { 475 buf.append(dow.name()).append(" on or before last day of ").append(month.name()); 476 } else if (dom < 0) { 477 buf.append(dow.name()).append(" on or before last day minus ").append(-dom - 1).append(" of ").append(month.name()); 478 } else { 479 buf.append(dow.name()).append(" on or after ").append(month.name()).append(' ').append(dom); 480 } 481 } else { 482 buf.append(month.name()).append(' ').append(dom); 483 } 484 buf.append(" at ").append(timeEndOfDay ? "24:00" : time.toString()) 485 .append(" ").append(timeDefinition) 486 .append(", standard offset ").append(standardOffset) 487 .append(']'); 488 return buf.toString(); 489 } 490 491 //----------------------------------------------------------------------- 492 /** 493 * A definition of the way a local time can be converted to the actual 494 * transition date-time. 495 * <p> 496 * Time zone rules are expressed in one of three ways: 497 * <p><ul> 498 * <li>Relative to UTC</li> 499 * <li>Relative to the standard offset in force</li> 500 * <li>Relative to the wall offset (what you would see on a clock on the wall)</li> 501 * </ul><p> 502 */ 503 public static enum TimeDefinition { 504 /** The local date-time is expressed in terms of the UTC offset. */ 505 UTC, 506 /** The local date-time is expressed in terms of the wall offset. */ 507 WALL, 508 /** The local date-time is expressed in terms of the standard offset. */ 509 STANDARD; 510 511 /** 512 * Converts the specified local date-time to the local date-time actually 513 * seen on a wall clock. 514 * <p> 515 * This method converts using the type of this enum. 516 * The output is defined relative to the 'before' offset of the transition. 517 * <p> 518 * The UTC type uses the UTC offset. 519 * The STANDARD type uses the standard offset. 520 * The WALL type returns the input date-time. 521 * The result is intended for use with the wall-offset. 522 * 523 * @param dateTime the local date-time, not null 524 * @param standardOffset the standard offset, not null 525 * @param wallOffset the wall offset, not null 526 * @return the date-time relative to the wall/before offset, not null 527 */ 528 public LocalDateTime createDateTime(LocalDateTime dateTime, ZoneOffset standardOffset, ZoneOffset wallOffset) { 529 switch (this) { 530 case UTC: { 531 int difference = wallOffset.getTotalSeconds() - ZoneOffset.UTC.getTotalSeconds(); 532 return dateTime.plusSeconds(difference); 533 } 534 case STANDARD: { 535 int difference = wallOffset.getTotalSeconds() - standardOffset.getTotalSeconds(); 536 return dateTime.plusSeconds(difference); 537 } 538 default: // WALL 539 return dateTime; 540 } 541 } 542 } 543 544}