1 /* 2 * hunt-time: A time library for D programming language. 3 * 4 * Copyright (C) 2015-2018 HuntLabs 5 * 6 * Website: https://www.huntlabs.net/ 7 * 8 * Licensed under the Apache-2.0 License. 9 * 10 */ 11 12 module hunt.time.zone.ZoneOffsetTransitionRule; 13 14 import hunt.time.temporal.TemporalAdjusters; 15 16 import hunt.stream.DataInput; 17 import hunt.stream.DataOutput; 18 import hunt.Exceptions; 19 20 import hunt.time.zone.Ser; 21 import hunt.stream.Common; 22 import hunt.time.DayOfWeek; 23 import hunt.time.LocalDate; 24 import hunt.time.LocalDateTime; 25 import hunt.time.LocalTime; 26 import hunt.time.Month; 27 import hunt.time.ZoneOffset; 28 import hunt.time.chrono.IsoChronology; 29 import hunt.time.zone.ZoneOffsetTransition; 30 import hunt.util.StringBuilder; 31 import hunt.time.util.Common; 32 33 import hunt.util.Common; 34 // import hunt.serialization.JsonSerializer; 35 36 37 /** 38 * A rule expressing how to create a transition. 39 * !(p) 40 * This class allows rules for identifying future transitions to be expressed. 41 * A rule might be written _in many forms: 42 * !(ul) 43 * !(li)the 16th March 44 * !(li)the Sunday on or after the 16th March 45 * !(li)the Sunday on or before the 16th March 46 * !(li)the last Sunday _in February 47 * </ul> 48 * These different rule types can be expressed and queried. 49 * 50 * @implSpec 51 * This class is immutable and thread-safe. 52 * 53 * @since 1.8 54 */ 55 public final class ZoneOffsetTransitionRule { // : Serializable 56 57 58 /** 59 * The month of the month-day of the first day of the cutover week. 60 * The actual date will be adjusted by the dowChange field. 61 */ 62 private Month month; 63 /** 64 * The day-of-month of the month-day of the cutover week. 65 * If positive, it is the start of the week where the cutover can occur. 66 * If negative, it represents the end of the week where cutover can occur. 67 * The value is the number of days from the end of the month, such that 68 * {@code -1} is the last day of the month, {@code -2} is the second 69 * to last day, and so on. 70 */ 71 private byte dom; 72 /** 73 * The cutover day-of-week, null to retain the day-of-month. 74 */ 75 private DayOfWeek dow; 76 /** 77 * The cutover time _in the 'before' offset. 78 */ 79 private LocalTime time; 80 /** 81 * Whether the cutover time is midnight at the end of day. 82 */ 83 private bool timeEndOfDay; 84 /** 85 * The definition of how the local time should be interpreted. 86 */ 87 private TimeDefinition timeDefinition; 88 /** 89 * The standard offset at the cutover. 90 */ 91 private ZoneOffset standardOffset; 92 /** 93 * The offset before the cutover. 94 */ 95 private ZoneOffset offsetBefore; 96 /** 97 * The offset after the cutover. 98 */ 99 private ZoneOffset offsetAfter; 100 101 /** 102 * Obtains an instance defining the yearly rule to create transitions between two offsets. 103 * !(p) 104 * Applications should normally obtain an instance from {@link ZoneRules}. 105 * This factory is only intended for use when creating {@link ZoneRules}. 106 * 107 * @param month the month of the month-day of the first day of the cutover week, not null 108 * @param dayOfMonthIndicator the day of the month-day of the cutover week, positive if the week is that 109 * day or later, negative if the week is that day or earlier, counting from the last day of the month, 110 * from -28 to 31 excluding 0 111 * @param dayOfWeek the required day-of-week, null if the month-day should not be changed 112 * @param time the cutover time _in the 'before' offset, not null 113 * @param timeEndOfDay whether the time is midnight at the end of day 114 * @param timeDefnition how to interpret the cutover 115 * @param standardOffset the standard offset _in force at the cutover, not null 116 * @param offsetBefore the offset before the cutover, not null 117 * @param offsetAfter the offset after the cutover, not null 118 * @return the rule, not null 119 * @throws IllegalArgumentException if the day of month indicator is invalid 120 * @throws IllegalArgumentException if the end of day flag is true when the time is not midnight 121 * @throws IllegalArgumentException if {@code time.getNano()} returns non-zero value 122 */ 123 public static ZoneOffsetTransitionRule of( 124 Month month, 125 int dayOfMonthIndicator, 126 DayOfWeek dayOfWeek, 127 LocalTime time, 128 bool timeEndOfDay, 129 TimeDefinition timeDefnition, 130 ZoneOffset standardOffset, 131 ZoneOffset offsetBefore, 132 ZoneOffset offsetAfter) { 133 assert(month, "month"); 134 assert(time, "time"); 135 // assert(timeDefnition, "timeDefnition"); 136 assert(standardOffset, "standardOffset"); 137 assert(offsetBefore, "offsetBefore"); 138 assert(offsetAfter, "offsetAfter"); 139 if (dayOfMonthIndicator < -28 || dayOfMonthIndicator > 31 || dayOfMonthIndicator == 0) { 140 throw new IllegalArgumentException("Day of month indicator must be between -28 and 31 inclusive excluding zero"); 141 } 142 if (timeEndOfDay && (time == LocalTime.MIDNIGHT) == false) { 143 throw new IllegalArgumentException("Time must be midnight when end of day flag is true"); 144 } 145 if (time.getNano() != 0) { 146 throw new IllegalArgumentException("Time's nano-of-second must be zero"); 147 } 148 return new ZoneOffsetTransitionRule(month, dayOfMonthIndicator, dayOfWeek, time, timeEndOfDay, timeDefnition, standardOffset, offsetBefore, offsetAfter); 149 } 150 151 /** 152 * Creates an instance defining the yearly rule to create transitions between two offsets. 153 * 154 * @param month the month of the month-day of the first day of the cutover week, not null 155 * @param dayOfMonthIndicator the day of the month-day of the cutover week, positive if the week is that 156 * day or later, negative if the week is that day or earlier, counting from the last day of the month, 157 * from -28 to 31 excluding 0 158 * @param dayOfWeek the required day-of-week, null if the month-day should not be changed 159 * @param time the cutover time _in the 'before' offset, not null 160 * @param timeEndOfDay whether the time is midnight at the end of day 161 * @param timeDefnition how to interpret the cutover 162 * @param standardOffset the standard offset _in force at the cutover, not null 163 * @param offsetBefore the offset before the cutover, not null 164 * @param offsetAfter the offset after the cutover, not null 165 * @throws IllegalArgumentException if the day of month indicator is invalid 166 * @throws IllegalArgumentException if the end of day flag is true when the time is not midnight 167 */ 168 this( 169 Month month, 170 int dayOfMonthIndicator, 171 DayOfWeek dayOfWeek, 172 LocalTime time, 173 bool timeEndOfDay, 174 TimeDefinition timeDefnition, 175 ZoneOffset standardOffset, 176 ZoneOffset offsetBefore, 177 ZoneOffset offsetAfter) { 178 assert(time.getNano() == 0); 179 this.month = month; 180 this.dom = cast(byte) dayOfMonthIndicator; 181 this.dow = dayOfWeek; 182 this.time = time; 183 this.timeEndOfDay = timeEndOfDay; 184 this.timeDefinition = timeDefnition; 185 this.standardOffset = standardOffset; 186 this.offsetBefore = offsetBefore; 187 this.offsetAfter = offsetAfter; 188 } 189 190 //----------------------------------------------------------------------- 191 /** 192 * Defend against malicious streams. 193 * 194 * @param s the stream to read 195 * @throws InvalidObjectException always 196 */ 197 ///@gxc 198 // private void readObject(ObjectInputStream s) /*throws InvalidObjectException*/ { 199 // throw new InvalidObjectException("Deserialization via serialization delegate"); 200 // } 201 202 /** 203 * Writes the object using a 204 * <a href="{@docRoot}/serialized-form.html#hunt.time.zone.Ser">dedicated serialized form</a>. 205 * @serialData 206 * Refer to the serialized form of 207 * <a href="{@docRoot}/serialized-form.html#hunt.time.zone.ZoneRules">ZoneRules.writeReplace</a> 208 * for the encoding of epoch seconds and offsets. 209 * <pre style="font-size:1.0em">{@code 210 * 211 * _out.writeByte(3); // identifies a ZoneOffsetTransition 212 * final int timeSecs = (timeEndOfDay ? 86400 : time.toSecondOfDay()); 213 * final int stdOffset = standardOffset.getTotalSeconds(); 214 * final int beforeDiff = offsetBefore.getTotalSeconds() - stdOffset; 215 * final int afterDiff = offsetAfter.getTotalSeconds() - stdOffset; 216 * final int timeByte = (timeSecs % 3600 == 0 ? (timeEndOfDay ? 24 : time.getHour()) : 31); 217 * final int stdOffsetByte = (stdOffset % 900 == 0 ? stdOffset / 900 + 128 : 255); 218 * final int beforeByte = (beforeDiff == 0 || beforeDiff == 1800 || beforeDiff == 3600 ? beforeDiff / 1800 : 3); 219 * final int afterByte = (afterDiff == 0 || afterDiff == 1800 || afterDiff == 3600 ? afterDiff / 1800 : 3); 220 * final int dowByte = (dow is null ? 0 : dow.getValue()); 221 * int b = (month.getValue() << 28) + // 4 bits 222 * ((dom + 32) << 22) + // 6 bits 223 * (dowByte << 19) + // 3 bits 224 * (timeByte << 14) + // 5 bits 225 * (timeDefinition.ordinal() << 12) + // 2 bits 226 * (stdOffsetByte << 4) + // 8 bits 227 * (beforeByte << 2) + // 2 bits 228 * afterByte; // 2 bits 229 * _out.writeInt(b); 230 * if (timeByte == 31) { 231 * _out.writeInt(timeSecs); 232 * } 233 * if (stdOffsetByte == 255) { 234 * _out.writeInt(stdOffset); 235 * } 236 * if (beforeByte == 3) { 237 * _out.writeInt(offsetBefore.getTotalSeconds()); 238 * } 239 * if (afterByte == 3) { 240 * _out.writeInt(offsetAfter.getTotalSeconds()); 241 * } 242 * } 243 * </pre> 244 * 245 * @return the replacing object, not null 246 */ 247 private Object writeReplace() { 248 return new Ser(Ser.ZOTRULE, this); 249 } 250 251 /** 252 * Writes the state to the stream. 253 * 254 * @param _out the output stream, not null 255 * @throws IOException if an error occurs 256 */ 257 void writeExternal(DataOutput _out) /*throws IOException*/ { 258 int timeSecs = (timeEndOfDay ? 86400 : time.toSecondOfDay()); 259 int stdOffset = standardOffset.getTotalSeconds(); 260 int beforeDiff = offsetBefore.getTotalSeconds() - stdOffset; 261 int afterDiff = offsetAfter.getTotalSeconds() - stdOffset; 262 int timeByte = (timeSecs % 3600 == 0 ? (timeEndOfDay ? 24 : time.getHour()) : 31); 263 int stdOffsetByte = (stdOffset % 900 == 0 ? stdOffset / 900 + 128 : 255); 264 int beforeByte = (beforeDiff == 0 || beforeDiff == 1800 || beforeDiff == 3600 ? beforeDiff / 1800 : 3); 265 int afterByte = (afterDiff == 0 || afterDiff == 1800 || afterDiff == 3600 ? afterDiff / 1800 : 3); 266 int dowByte = (dow is null ? 0 : dow.getValue()); 267 int b = (month.getValue() << 28) + // 4 bits 268 ((dom + 32) << 22) + // 6 bits 269 (dowByte << 19) + // 3 bits 270 (timeByte << 14) + // 5 bits 271 (timeDefinition.ordinal() << 12) + // 2 bits 272 (stdOffsetByte << 4) + // 8 bits 273 (beforeByte << 2) + // 2 bits 274 afterByte; // 2 bits 275 _out.writeInt(b); 276 if (timeByte == 31) { 277 _out.writeInt(timeSecs); 278 } 279 if (stdOffsetByte == 255) { 280 _out.writeInt(stdOffset); 281 } 282 if (beforeByte == 3) { 283 _out.writeInt(offsetBefore.getTotalSeconds()); 284 } 285 if (afterByte == 3) { 286 _out.writeInt(offsetAfter.getTotalSeconds()); 287 } 288 } 289 290 /** 291 * Reads the state from the stream. 292 * 293 * @param _in the input stream, not null 294 * @return the created object, not null 295 * @throws IOException if an error occurs 296 */ 297 static ZoneOffsetTransitionRule readExternal(DataInput _in) /*throws IOException*/ { 298 int data = _in.readInt(); 299 Month month = Month.of(data >>> 28); 300 int dom = ((data & (63 << 22)) >>> 22) - 32; 301 int dowByte = (data & (7 << 19)) >>> 19; 302 DayOfWeek dow = dowByte == 0 ? null : DayOfWeek.of(dowByte); 303 int timeByte = (data & (31 << 14)) >>> 14; 304 TimeDefinition defn = TimeDefinition.values()[(data & (3 << 12)) >>> 12]; 305 int stdByte = (data & (255 << 4)) >>> 4; 306 int beforeByte = (data & (3 << 2)) >>> 2; 307 int afterByte = (data & 3); 308 LocalTime time = (timeByte == 31 ? LocalTime.ofSecondOfDay(_in.readInt()) : LocalTime.of(timeByte % 24, 0)); 309 ZoneOffset std = (stdByte == 255 ? ZoneOffset.ofTotalSeconds(_in.readInt()) : ZoneOffset.ofTotalSeconds((stdByte - 128) * 900)); 310 ZoneOffset before = (beforeByte == 3 ? ZoneOffset.ofTotalSeconds(_in.readInt()) : ZoneOffset.ofTotalSeconds(std.getTotalSeconds() + beforeByte * 1800)); 311 ZoneOffset after = (afterByte == 3 ? ZoneOffset.ofTotalSeconds(_in.readInt()) : ZoneOffset.ofTotalSeconds(std.getTotalSeconds() + afterByte * 1800)); 312 return ZoneOffsetTransitionRule.of(month, dom, dow, time, timeByte == 24, defn, std, before, after); 313 } 314 315 //----------------------------------------------------------------------- 316 /** 317 * Gets the month of the transition. 318 * !(p) 319 * If the rule defines an exact date then the month is the month of that date. 320 * !(p) 321 * If the rule defines a week where the transition might occur, then the month 322 * if the month of either the earliest or latest possible date of the cutover. 323 * 324 * @return the month of the transition, not null 325 */ 326 public Month getMonth() { 327 return month; 328 } 329 330 /** 331 * Gets the indicator of the day-of-month of the transition. 332 * !(p) 333 * If the rule defines an exact date then the day is the month of that date. 334 * !(p) 335 * If the rule defines a week where the transition might occur, then the day 336 * defines either the start of the end of the transition week. 337 * !(p) 338 * If the value is positive, then it represents a normal day-of-month, and is the 339 * earliest possible date that the transition can be. 340 * The date may refer to 29th February which should be treated as 1st March _in non-leap years. 341 * !(p) 342 * If the value is negative, then it represents the number of days back from the 343 * end of the month where {@code -1} is the last day of the month. 344 * In this case, the day identified is the latest possible date that the transition can be. 345 * 346 * @return the day-of-month indicator, from -28 to 31 excluding 0 347 */ 348 public int getDayOfMonthIndicator() { 349 return dom; 350 } 351 352 /** 353 * Gets the day-of-week of the transition. 354 * !(p) 355 * If the rule defines an exact date then this returns null. 356 * !(p) 357 * If the rule defines a week where the cutover might occur, then this method 358 * returns the day-of-week that the month-day will be adjusted to. 359 * If the day is positive then the adjustment is later. 360 * If the day is negative then the adjustment is earlier. 361 * 362 * @return the day-of-week that the transition occurs, null if the rule defines an exact date 363 */ 364 public DayOfWeek getDayOfWeek() { 365 return dow; 366 } 367 368 /** 369 * Gets the local time of day of the transition which must be checked with 370 * {@link #isMidnightEndOfDay()}. 371 * !(p) 372 * The time is converted into an instant using the time definition. 373 * 374 * @return the local time of day of the transition, not null 375 */ 376 public LocalTime getLocalTime() { 377 return time; 378 } 379 380 /** 381 * Is the transition local time midnight at the end of day. 382 * !(p) 383 * The transition may be represented as occurring at 24:00. 384 * 385 * @return whether a local time of midnight is at the start or end of the day 386 */ 387 public bool isMidnightEndOfDay() { 388 return timeEndOfDay; 389 } 390 391 /** 392 * Gets the time definition, specifying how to convert the time to an instant. 393 * !(p) 394 * The local time can be converted to an instant using the standard offset, 395 * the wall offset or UTC. 396 * 397 * @return the time definition, not null 398 */ 399 public TimeDefinition getTimeDefinition() { 400 return timeDefinition; 401 } 402 403 /** 404 * Gets the standard offset _in force at the transition. 405 * 406 * @return the standard offset, not null 407 */ 408 public ZoneOffset getStandardOffset() { 409 return standardOffset; 410 } 411 412 /** 413 * Gets the offset before the transition. 414 * 415 * @return the offset before, not null 416 */ 417 public ZoneOffset getOffsetBefore() { 418 return offsetBefore; 419 } 420 421 /** 422 * Gets the offset after the transition. 423 * 424 * @return the offset after, not null 425 */ 426 public ZoneOffset getOffsetAfter() { 427 return offsetAfter; 428 } 429 430 //----------------------------------------------------------------------- 431 /** 432 * Creates a transition instance for the specified year. 433 * !(p) 434 * Calculations are performed using the ISO-8601 chronology. 435 * 436 * @param year the year to create a transition for, not null 437 * @return the transition instance, not null 438 */ 439 public ZoneOffsetTransition createTransition(int year) { 440 LocalDate date; 441 if (dom < 0) { 442 date = LocalDate.of(year, month, month.length(IsoChronology.INSTANCE.isLeapYear(year)) + 1 + dom); 443 if (dow !is null) { 444 date = date._with(TemporalAdjusters.previousOrSame(dow)); 445 } 446 } else { 447 date = LocalDate.of(year, month, dom); 448 if (dow !is null) { 449 date = date._with(TemporalAdjusters.nextOrSame(dow)); 450 } 451 } 452 if (timeEndOfDay) { 453 date = date.plusDays(1); 454 } 455 LocalDateTime localDT = LocalDateTime.of(date, time); 456 LocalDateTime transition = timeDefinition.createDateTime(localDT, standardOffset, offsetBefore); 457 return new ZoneOffsetTransition(transition, offsetBefore, offsetAfter); 458 } 459 460 //----------------------------------------------------------------------- 461 /** 462 * Checks if this object equals another. 463 * !(p) 464 * The entire state of the object is compared. 465 * 466 * @param otherRule the other object to compare to, null returns false 467 * @return true if equal 468 */ 469 override 470 public bool opEquals(Object otherRule) { 471 if (otherRule == this) { 472 return true; 473 } 474 if (cast(ZoneOffsetTransitionRule)(otherRule) !is null) { 475 ZoneOffsetTransitionRule other = cast(ZoneOffsetTransitionRule) otherRule; 476 return month == other.month && dom == other.dom && dow == other.dow && 477 timeDefinition == other.timeDefinition && 478 (time == other.time) && 479 timeEndOfDay == other.timeEndOfDay && 480 (standardOffset == other.standardOffset) && 481 (offsetBefore == other.offsetBefore) && 482 (offsetAfter == other.offsetAfter); 483 } 484 return false; 485 } 486 487 /** 488 * Returns a suitable hash code. 489 * 490 * @return the hash code 491 */ 492 override 493 public size_t toHash() @trusted nothrow { 494 try{ 495 int hash = ((time.toSecondOfDay() + (timeEndOfDay ? 1 : 0)) << 15) + 496 (month.ordinal() << 11) + ((dom + 32) << 5) + 497 ((dow is null ? 7 : dow.ordinal()) << 2) + (timeDefinition.ordinal()); 498 return hash ^ standardOffset.toHash() ^ 499 offsetBefore.toHash() ^ offsetAfter.toHash(); 500 }catch(Exception e){} 501 return int.init; 502 } 503 504 //----------------------------------------------------------------------- 505 /** 506 * Returns a string describing this object. 507 * 508 * @return a string for debugging, not null 509 */ 510 override 511 public string toString() { 512 StringBuilder buf = new StringBuilder(); 513 buf.append("TransitionRule[") 514 .append(offsetBefore.compareTo(offsetAfter) > 0 ? "Gap " : "Overlap ") 515 .append(offsetBefore.toString).append(" to ").append(offsetAfter).append(", "); 516 if (dow !is null) { 517 if (dom == -1) { 518 buf.append(dow.name()).append(" on or before last day of ").append(month.name()); 519 } else if (dom < 0) { 520 buf.append(dow.name()).append(" on or before last day minus ").append(-(cast(int)dom) - 1).append(" of ").append(month.name()); 521 } else { 522 buf.append(dow.name()).append(" on or after ").append(month.name()).append(' ').append(dom); 523 } 524 } else { 525 buf.append(month.name()).append(' ').append(dom); 526 } 527 buf.append(" at ").append(timeEndOfDay ? "24:00" : time.toString()) 528 .append(" ").append(timeDefinition) 529 .append(", standard offset ").append(standardOffset) 530 .append(']'); 531 return buf.toString(); 532 } 533 534 //----------------------------------------------------------------------- 535 /** 536 * A definition of the way a local time can be converted to the actual 537 * transition date-time. 538 * !(p) 539 * Time zone rules are expressed _in one of three ways: 540 * !(ul) 541 * !(li)Relative to UTC</li> 542 * !(li)Relative to the standard offset _in force</li> 543 * !(li)Relative to the wall offset (what you would see on a clock on the wall)</li> 544 * </ul> 545 */ 546 public static class TimeDefinition { 547 /** The local date-time is expressed _in terms of the UTC offset. */ 548 // static TimeDefinition UTC; 549 // /** The local date-time is expressed _in terms of the wall offset. */ 550 // static TimeDefinition WALL; 551 // /** The local date-time is expressed _in terms of the standard offset. */ 552 // static TimeDefinition STANDARD; 553 554 /** 555 * Converts the specified local date-time to the local date-time actually 556 * seen on a wall clock. 557 * !(p) 558 * This method converts using the type of this enum. 559 * The output is defined relative to the 'before' offset of the transition. 560 * !(p) 561 * The UTC type uses the UTC offset. 562 * The STANDARD type uses the standard offset. 563 * The WALL type returns the input date-time. 564 * The result is intended for use with the wall-offset. 565 * 566 * @param dateTime the local date-time, not null 567 * @param standardOffset the standard offset, not null 568 * @param wallOffset the wall offset, not null 569 * @return the date-time relative to the wall/before offset, not null 570 */ 571 private int _ordinal; 572 private string _name; 573 static TimeDefinition[] _values; 574 575 // static this(){ 576 // UTC = new TimeDefinition(0,"UTC"); 577 mixin(MakeGlobalVar!(TimeDefinition)("UTC",`new TimeDefinition(0,"UTC")`)); 578 // WALL = new TimeDefinition(1,"WALL"); 579 mixin(MakeGlobalVar!(TimeDefinition)("WALL",`new TimeDefinition(1,"WALL")`)); 580 581 // STANDARD = new TimeDefinition(2,"STANDARD"); 582 mixin(MakeGlobalVar!(TimeDefinition)("STANDARD",`new TimeDefinition(2,"STANDARD")`)); 583 584 585 // } 586 587 static ref TimeDefinition[] values() 588 { 589 if(_values.length == 0) 590 { 591 _values ~= UTC; 592 _values ~= WALL; 593 _values ~= STANDARD; 594 } 595 return _values; 596 } 597 598 public int ordinal() 599 { 600 return _ordinal; 601 } 602 public string name() 603 { 604 return _name; 605 } 606 this(int ordinal , string name) 607 { 608 _ordinal = ordinal; 609 _name = name; 610 } 611 public LocalDateTime createDateTime(LocalDateTime dateTime, ZoneOffset standardOffset, ZoneOffset wallOffset) { 612 auto name = this.name(); 613 switch (name) { 614 case "UTC": { 615 int difference = wallOffset.getTotalSeconds() - ZoneOffset.UTC.getTotalSeconds(); 616 return dateTime.plusSeconds(difference); 617 } 618 case "STANDARD": { 619 int difference = wallOffset.getTotalSeconds() - standardOffset.getTotalSeconds(); 620 return dateTime.plusSeconds(difference); 621 } 622 default: // WALL 623 return dateTime; 624 } 625 } 626 } 627 628 // mixin SerializationMember!(typeof(this)); 629 630 }