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