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.ZoneRules; 13 14 import hunt.stream.DataInput; 15 import hunt.stream.DataOutput; 16 import hunt.Exceptions; 17 18 //import hunt.io.ObjectInputStream; 19 import hunt.stream.Common; 20 // import hunt.time.Duration; 21 import hunt.time.Instant; 22 import hunt.time.LocalDate; 23 import hunt.time.LocalDateTime; 24 import hunt.time.ZoneOffset; 25 // import hunt.time.Year; 26 import hunt.collection.ArrayList; 27 import hunt.time.zone.Ser; 28 import hunt.collection.Collections; 29 import hunt.collection.List; 30 import hunt.Integer; 31 import hunt.Long; 32 import hunt.math.Helper; 33 import hunt.collection.HashMap; 34 import hunt.time.zone.ZoneOffsetTransitionRule; 35 import hunt.time.zone.ZoneOffsetTransition; 36 import hunt.text.Common; 37 import hunt.util.ArrayHelper; 38 import hunt.time.util.Common; 39 import hunt.util.ArrayHelper; 40 41 import std.algorithm.searching; 42 import std.concurrency : initOnce; 43 44 /** 45 * The rules defining how the zone offset varies for a single time-zone. 46 * !(p) 47 * The rules model all the historic and future transitions for a time-zone. 48 * {@link ZoneOffsetTransition} is used for known transitions, typically historic. 49 * {@link ZoneOffsetTransitionRule} is used for future transitions that are based 50 * on the result of an algorithm. 51 * !(p) 52 * The rules are loaded via {@link ZoneRulesProvider} using a {@link ZoneId}. 53 * The same rules may be shared internally between multiple zone IDs. 54 * !(p) 55 * Serializing an instance of {@code ZoneRules} will store the entire set of rules. 56 * It does not store the zone ID as it is not part of the state of this object. 57 * !(p) 58 * A rule implementation may or may not store full information about historic 59 * and future transitions, and the information stored is only as accurate as 60 * that supplied to the implementation by the rules provider. 61 * Applications should treat the data provided as representing the best information 62 * available to the implementation of this rule. 63 * 64 * @implSpec 65 * This class is immutable and thread-safe. 66 * 67 */ 68 public final class ZoneRules // : Serializable 69 { 70 71 /** 72 * The last year to have its transitions cached. 73 */ 74 private enum int LAST_CACHED_YEAR = 2100; 75 76 /** 77 * The transitions between standard offsets (epoch seconds), sorted. 78 */ 79 private long[] standardTransitions; 80 /** 81 * The standard offsets. 82 */ 83 private ZoneOffset[] standardOffsets; 84 /** 85 * The transitions between instants (epoch seconds), sorted. 86 */ 87 private long[] savingsInstantTransitions; 88 /** 89 * The transitions between local date-times, sorted. 90 * This is a paired array, where the first entry is the start of the transition 91 * and the second entry is the end of the transition. 92 */ 93 private LocalDateTime[] savingsLocalTransitions; 94 /** 95 * The wall offsets. 96 */ 97 private ZoneOffset[] wallOffsets; 98 /** 99 * The last rule. 100 */ 101 private ZoneOffsetTransitionRule[] lastRules; 102 /** 103 * The map of recent transitions. 104 */ 105 // private ConcurrentMap!(Integer, ZoneOffsetTransition[]) lastRulesCache = 106 // new ConcurrentHashMap!(Integer, ZoneOffsetTransition[])(); 107 private HashMap!(Integer, ZoneOffsetTransition[]) lastRulesCache; 108 109 /** 110 * The zero-length long array. 111 */ 112 __gshared long[] EMPTY_LONG_ARRAY; 113 114 /** 115 * The zero-length lastrules array. 116 */ 117 __gshared ZoneOffsetTransitionRule[] EMPTY_LASTRULES; 118 119 /** 120 * The zero-length ldt array. 121 */ 122 __gshared LocalDateTime[] EMPTY_LDT_ARRAY; 123 124 125 /** 126 * Obtains an instance of a ZoneRules. 127 * 128 * @param baseStandardOffset the standard offset to use before legal rules were set, not null 129 * @param baseWallOffset the wall offset to use before legal rules were set, not null 130 * @param standardOffsetTransitionList the list of changes to the standard offset, not null 131 * @param transitionList the list of transitions, not null 132 * @param lastRules the recurring last rules, size 16 or less, not null 133 * @return the zone rules, not null 134 */ 135 public static ZoneRules of(ZoneOffset baseStandardOffset, ZoneOffset baseWallOffset, 136 List!(ZoneOffsetTransition) standardOffsetTransitionList, 137 List!(ZoneOffsetTransition) transitionList, List!(ZoneOffsetTransitionRule) lastRules) 138 { 139 assert(baseStandardOffset, "baseStandardOffset"); 140 assert(baseWallOffset, "baseWallOffset"); 141 assert(standardOffsetTransitionList, "standardOffsetTransitionList"); 142 assert(transitionList, "transitionList"); 143 assert(lastRules, "lastRules"); 144 return new ZoneRules(baseStandardOffset, baseWallOffset, 145 standardOffsetTransitionList, transitionList, lastRules); 146 } 147 148 /** 149 * Obtains an instance of ZoneRules that has fixed zone rules. 150 * 151 * @param offset the offset this fixed zone rules is based on, not null 152 * @return the zone rules, not null 153 * @see #isFixedOffset() 154 */ 155 public static ZoneRules of(ZoneOffset offset) 156 { 157 assert(offset, "offset"); 158 return new ZoneRules(offset); 159 } 160 161 /** 162 * Creates an instance. 163 * 164 * @param baseStandardOffset the standard offset to use before legal rules were set, not null 165 * @param baseWallOffset the wall offset to use before legal rules were set, not null 166 * @param standardOffsetTransitionList the list of changes to the standard offset, not null 167 * @param transitionList the list of transitions, not null 168 * @param lastRules the recurring last rules, size 16 or less, not null 169 */ 170 this(ZoneOffset baseStandardOffset, ZoneOffset baseWallOffset, 171 List!(ZoneOffsetTransition) standardOffsetTransitionList, 172 List!(ZoneOffsetTransition) transitionList, List!(ZoneOffsetTransitionRule) lastRules) 173 { 174 // super(); 175 lastRulesCache = new HashMap!(Integer, ZoneOffsetTransition[])(); 176 177 // convert standard transitions 178 179 this.standardTransitions = new long[standardOffsetTransitionList.size()]; 180 181 this.standardOffsets = new ZoneOffset[standardOffsetTransitionList.size() + 1]; 182 this.standardOffsets[0] = baseStandardOffset; 183 for (int i = 0; i < standardOffsetTransitionList.size(); i++) 184 { 185 this.standardTransitions[i] = standardOffsetTransitionList.get(i).toEpochSecond(); 186 this.standardOffsets[i + 1] = standardOffsetTransitionList.get(i).getOffsetAfter(); 187 } 188 189 // convert savings transitions to locals 190 List!(LocalDateTime) localTransitionList = new ArrayList!(LocalDateTime)(); 191 List!(ZoneOffset) localTransitionOffsetList = new ArrayList!(ZoneOffset)(); 192 localTransitionOffsetList.add(baseWallOffset); 193 foreach (ZoneOffsetTransition trans; transitionList) 194 { 195 if (trans.isGap()) 196 { 197 localTransitionList.add(trans.getDateTimeBefore()); 198 localTransitionList.add(trans.getDateTimeAfter()); 199 } 200 else 201 { 202 localTransitionList.add(trans.getDateTimeAfter()); 203 localTransitionList.add(trans.getDateTimeBefore()); 204 } 205 localTransitionOffsetList.add(trans.getOffsetAfter()); 206 } 207 // this.savingsLocalTransitions = new LocalDateTime[localTransitionList.size()]; 208 // foreach (data; localTransitionList) 209 // this.savingsLocalTransitions ~= data; 210 // this.wallOffsets = new ZoneOffset[localTransitionOffsetList.size()]; 211 // foreach (data; localTransitionOffsetList) 212 // { 213 // this.wallOffsets ~= data; 214 // } 215 216 this.savingsLocalTransitions = localTransitionList.toArray(); 217 this.wallOffsets = localTransitionOffsetList.toArray(); 218 // convert savings transitions to instants 219 this.savingsInstantTransitions = new long[transitionList.size()]; 220 for (int i = 0; i < transitionList.size(); i++) 221 { 222 this.savingsInstantTransitions[i] = transitionList.get(i).toEpochSecond(); 223 } 224 225 // last rules 226 if (lastRules.size() > 16) 227 { 228 throw new IllegalArgumentException("Too many transition rules"); 229 } 230 // this.lastRules = new ZoneOffsetTransitionRule[lastRules.size()]; 231 // foreach (data; lastRules) 232 // this.lastRules ~= data; 233 this.lastRules = lastRules.toArray(); 234 } 235 236 /** 237 * Constructor. 238 * 239 * @param standardTransitions the standard transitions, not null 240 * @param standardOffsets the standard offsets, not null 241 * @param savingsInstantTransitions the standard transitions, not null 242 * @param wallOffsets the wall offsets, not null 243 * @param lastRules the recurring last rules, size 15 or less, not null 244 */ 245 private this(long[] standardTransitions, ZoneOffset[] standardOffsets, 246 long[] savingsInstantTransitions, ZoneOffset[] wallOffsets, 247 ZoneOffsetTransitionRule[] lastRules) 248 { 249 // super(); 250 251 lastRulesCache = new HashMap!(Integer, ZoneOffsetTransition[])(); 252 this.standardTransitions = standardTransitions; 253 this.standardOffsets = standardOffsets; 254 this.savingsInstantTransitions = savingsInstantTransitions; 255 this.wallOffsets = wallOffsets; 256 this.lastRules = lastRules; 257 258 if (savingsInstantTransitions.length == 0) 259 { 260 this.savingsLocalTransitions = EMPTY_LDT_ARRAY; 261 } 262 else 263 { 264 // convert savings transitions to locals 265 List!(LocalDateTime) localTransitionList = new ArrayList!(LocalDateTime)(); 266 for (int i = 0; i < savingsInstantTransitions.length; i++) 267 { 268 ZoneOffset before = wallOffsets[i]; 269 ZoneOffset after = wallOffsets[i + 1]; 270 ZoneOffsetTransition trans = new ZoneOffsetTransition(savingsInstantTransitions[i], 271 before, after); 272 if (trans.isGap()) 273 { 274 localTransitionList.add(trans.getDateTimeBefore()); 275 localTransitionList.add(trans.getDateTimeAfter()); 276 } 277 else 278 { 279 localTransitionList.add(trans.getDateTimeAfter()); 280 localTransitionList.add(trans.getDateTimeBefore()); 281 } 282 } 283 // this.savingsLocalTransitions = new LocalDateTime[localTransitionList.size()]; 284 // foreach (data; localTransitionList) 285 // this.savingsLocalTransitions ~= data; 286 this.savingsLocalTransitions = localTransitionList.toArray(); 287 } 288 } 289 290 /** 291 * Creates an instance of ZoneRules that has fixed zone rules. 292 * 293 * @param offset the offset this fixed zone rules is based on, not null 294 * @see #isFixedOffset() 295 */ 296 private this(ZoneOffset offset) 297 { 298 lastRulesCache = new HashMap!(Integer, ZoneOffsetTransition[])(); 299 this.standardOffsets = new ZoneOffset[1]; 300 this.standardOffsets[0] = offset; 301 this.standardTransitions = EMPTY_LONG_ARRAY; 302 this.savingsInstantTransitions = EMPTY_LONG_ARRAY; 303 this.savingsLocalTransitions = EMPTY_LDT_ARRAY; 304 this.wallOffsets = standardOffsets; 305 this.lastRules = EMPTY_LASTRULES; 306 } 307 308 /** 309 * Defend against malicious streams. 310 * 311 * @param s the stream to read 312 * @throws InvalidObjectException always 313 */ 314 ///@gxc 315 // private void readObject(ObjectInputStream s) /*throws InvalidObjectException*/ { 316 // throw new InvalidObjectException("Deserialization via serialization delegate"); 317 // } 318 319 /** 320 * Writes the object using a 321 * <a href="{@docRoot}/serialized-form.html#hunt.time.zone.Ser">dedicated serialized form</a>. 322 * @serialData 323 * <pre style="font-size:1.0em">{@code 324 * 325 * _out.writeByte(1); // identifies a ZoneRules 326 * _out.writeInt(standardTransitions.length); 327 * foreach(long trans ; standardTransitions) { 328 * Ser.writeEpochSec(trans, _out); 329 * } 330 * foreach(ZoneOffset offset ; standardOffsets) { 331 * Ser.writeOffset(offset, _out); 332 * } 333 * _out.writeInt(savingsInstantTransitions.length); 334 * foreach(long trans ; savingsInstantTransitions) { 335 * Ser.writeEpochSec(trans, _out); 336 * } 337 * foreach(ZoneOffset offset ; wallOffsets) { 338 * Ser.writeOffset(offset, _out); 339 * } 340 * _out.writeByte(lastRules.length); 341 * foreach(ZoneOffsetTransitionRule rule ; lastRules) { 342 * rule.writeExternal(_out); 343 * } 344 * } 345 * </pre> 346 * !(p) 347 * Epoch second values used for offsets are encoded _in a variable 348 * length form to make the common cases put fewer bytes _in the stream. 349 * <pre style="font-size:1.0em">{@code 350 * 351 * static void writeEpochSec(long epochSec, DataOutput _out) throws IOException { 352 * if (epochSec >= -4575744000L && epochSec < 10413792000L && epochSec % 900 == 0) { // quarter hours between 1825 and 2300 353 * int store = cast(int) ((epochSec + 4575744000L) / 900); 354 * _out.writeByte((store >>> 16) & 255); 355 * _out.writeByte((store >>> 8) & 255); 356 * _out.writeByte(store & 255); 357 * } else { 358 * _out.writeByte(255); 359 * _out.writeLong(epochSec); 360 * } 361 * } 362 * } 363 * </pre> 364 * !(p) 365 * ZoneOffset values are encoded _in a variable length form so the 366 * common cases put fewer bytes _in the stream. 367 * <pre style="font-size:1.0em">{@code 368 * 369 * static void writeOffset(ZoneOffset offset, DataOutput _out) throws IOException { 370 * final int offsetSecs = offset.getTotalSeconds(); 371 * int offsetByte = offsetSecs % 900 == 0 ? offsetSecs / 900 : 127; // compress to -72 to +72 372 * _out.writeByte(offsetByte); 373 * if (offsetByte == 127) { 374 * _out.writeInt(offsetSecs); 375 * } 376 * } 377 *} 378 * </pre> 379 * @return the replacing object, not null 380 */ 381 private Object writeReplace() 382 { 383 return new Ser(Ser.ZRULES, this); 384 } 385 386 /** 387 * Writes the state to the stream. 388 * 389 * @param _out the output stream, not null 390 * @throws IOException if an error occurs 391 */ 392 void writeExternal(DataOutput _out) /*throws IOException*/ 393 { 394 _out.writeInt(cast(int)(standardTransitions.length)); 395 foreach (long trans; standardTransitions) 396 { 397 Ser.writeEpochSec(trans, _out); 398 } 399 foreach (ZoneOffset offset; standardOffsets) 400 { 401 Ser.writeOffset(offset, _out); 402 } 403 _out.writeInt(cast(int)(savingsInstantTransitions.length)); 404 foreach (long trans; savingsInstantTransitions) 405 { 406 Ser.writeEpochSec(trans, _out); 407 } 408 foreach (ZoneOffset offset; wallOffsets) 409 { 410 Ser.writeOffset(offset, _out); 411 } 412 _out.writeByte(cast(int)(lastRules.length)); 413 foreach (ZoneOffsetTransitionRule rule; lastRules) 414 { 415 rule.writeExternal(_out); 416 } 417 } 418 419 /** 420 * Reads the state from the stream. 421 * 422 * @param _in the input stream, not null 423 * @return the created object, not null 424 * @throws IOException if an error occurs 425 */ 426 static ZoneRules readExternal(DataInput _in) /*throws IOException, ClassNotFoundException*/ 427 { 428 int stdSize = _in.readInt(); 429 long[] stdTrans = (stdSize == 0) ? EMPTY_LONG_ARRAY : new long[stdSize]; 430 for (int i = 0; i < stdSize; i++) 431 { 432 stdTrans[i] = Ser.readEpochSec(_in); 433 } 434 ZoneOffset[] stdOffsets = new ZoneOffset[stdSize + 1]; 435 for (int i = 0; i < stdOffsets.length; i++) 436 { 437 stdOffsets[i] = Ser.readOffset(_in); 438 } 439 int savSize = _in.readInt(); 440 long[] savTrans = (savSize == 0) ? EMPTY_LONG_ARRAY : new long[savSize]; 441 for (int i = 0; i < savSize; i++) 442 { 443 savTrans[i] = Ser.readEpochSec(_in); 444 } 445 ZoneOffset[] savOffsets = new ZoneOffset[savSize + 1]; 446 for (int i = 0; i < savOffsets.length; i++) 447 { 448 savOffsets[i] = Ser.readOffset(_in); 449 } 450 int ruleSize = _in.readByte(); 451 ZoneOffsetTransitionRule[] rules = (ruleSize == 0) ? EMPTY_LASTRULES 452 : new ZoneOffsetTransitionRule[ruleSize]; 453 for (int i = 0; i < ruleSize; i++) 454 { 455 rules[i] = ZoneOffsetTransitionRule.readExternal(_in); 456 } 457 return new ZoneRules(stdTrans, stdOffsets, savTrans, savOffsets, rules); 458 } 459 460 /** 461 * Checks of the zone rules are fixed, such that the offset never varies. 462 * 463 * @return true if the time-zone is fixed and the offset never changes 464 */ 465 public bool isFixedOffset() 466 { 467 return savingsInstantTransitions.length == 0; 468 } 469 470 /** 471 * Gets the offset applicable at the specified instant _in these rules. 472 * !(p) 473 * The mapping from an instant to an offset is simple, there is only 474 * one valid offset for each instant. 475 * This method returns that offset. 476 * 477 * @param instant the instant to find the offset for, not null, but null 478 * may be ignored if the rules have a single offset for all instants 479 * @return the offset, not null 480 */ 481 public ZoneOffset getOffset(Instant instant) 482 { 483 if (savingsInstantTransitions.length == 0) 484 { 485 return standardOffsets[0]; 486 } 487 long epochSec = instant.getEpochSecond(); 488 // check if using last rules 489 if (lastRules.length > 0 490 && epochSec > savingsInstantTransitions[savingsInstantTransitions.length - 1]) 491 { 492 int year = findYear(epochSec, wallOffsets[wallOffsets.length - 1]); 493 ZoneOffsetTransition[] transArray = findTransitionArray(year); 494 ZoneOffsetTransition trans = null; 495 for (int i = 0; i < transArray.length; i++) 496 { 497 trans = transArray[i]; 498 if (epochSec < trans.toEpochSecond()) 499 { 500 return trans.getOffsetBefore(); 501 } 502 } 503 return trans.getOffsetAfter(); 504 } 505 506 // using historic rules 507 import hunt.text.Common; 508 509 int index = ArrayHelper.binarySearch(savingsInstantTransitions, epochSec); 510 if (index == -1) 511 index = -(cast(int)(savingsInstantTransitions.length)) - 1; 512 if (index < 0) 513 { 514 // switch negative insert position to start of matched range 515 index = -index - 2; 516 } 517 return wallOffsets[index + 1]; 518 } 519 520 /** 521 * Gets a suitable offset for the specified local date-time _in these rules. 522 * !(p) 523 * The mapping from a local date-time to an offset is not straightforward. 524 * There are three cases: 525 * !(ul) 526 * !(li)Normal, with one valid offset. For the vast majority of the year, the normal 527 * case applies, where there is a single valid offset for the local date-time.</li> 528 * !(li)Gap, with zero valid offsets. This is when clocks jump forward typically 529 * due to the spring daylight savings change from "winter" to "summer". 530 * In a gap there are local date-time values with no valid offset.</li> 531 * !(li)Overlap, with two valid offsets. This is when clocks are set back typically 532 * due to the autumn daylight savings change from "summer" to "winter". 533 * In an overlap there are local date-time values with two valid offsets.</li> 534 * </ul> 535 * Thus, for any given local date-time there can be zero, one or two valid offsets. 536 * This method returns the single offset _in the Normal case, and _in the Gap or Overlap 537 * case it returns the offset before the transition. 538 * !(p) 539 * Since, _in the case of Gap and Overlap, the offset returned is a "best" value, rather 540 * than the "correct" value, it should be treated with care. Applications that care 541 * about the correct offset should use a combination of this method, 542 * {@link #getValidOffsets(LocalDateTime)} and {@link #getTransition(LocalDateTime)}. 543 * 544 * @param localDateTime the local date-time to query, not null, but null 545 * may be ignored if the rules have a single offset for all instants 546 * @return the best available offset for the local date-time, not null 547 */ 548 public ZoneOffset getOffset(LocalDateTime localDateTime) 549 { 550 Object info = getOffsetInfo(localDateTime); 551 if (cast(ZoneOffsetTransition)(info) !is null) 552 { 553 return (cast(ZoneOffsetTransition) info).getOffsetBefore(); 554 } 555 return cast(ZoneOffset) info; 556 } 557 558 /** 559 * Gets the offset applicable at the specified local date-time _in these rules. 560 * !(p) 561 * The mapping from a local date-time to an offset is not straightforward. 562 * There are three cases: 563 * !(ul) 564 * !(li)Normal, with one valid offset. For the vast majority of the year, the normal 565 * case applies, where there is a single valid offset for the local date-time.</li> 566 * !(li)Gap, with zero valid offsets. This is when clocks jump forward typically 567 * due to the spring daylight savings change from "winter" to "summer". 568 * In a gap there are local date-time values with no valid offset.</li> 569 * !(li)Overlap, with two valid offsets. This is when clocks are set back typically 570 * due to the autumn daylight savings change from "summer" to "winter". 571 * In an overlap there are local date-time values with two valid offsets.</li> 572 * </ul> 573 * Thus, for any given local date-time there can be zero, one or two valid offsets. 574 * This method returns that list of valid offsets, which is a list of size 0, 1 or 2. 575 * In the case where there are two offsets, the earlier offset is returned at index 0 576 * and the later offset at index 1. 577 * !(p) 578 * There are various ways to handle the conversion from a {@code LocalDateTime}. 579 * One technique, using this method, would be: 580 * !(pre) 581 * List<ZoneOffset> validOffsets = rules.getOffset(localDT); 582 * if (validOffsets.size() == 1) { 583 * // Normal case: only one valid offset 584 * zoneOffset = validOffsets.get(0); 585 * } else { 586 * // Gap or Overlap: determine what to do from transition (which will be non-null) 587 * ZoneOffsetTransition trans = rules.getTransition(localDT); 588 * } 589 * </pre> 590 * !(p) 591 * In theory, it is possible for there to be more than two valid offsets. 592 * This would happen if clocks to be put back more than once _in quick succession. 593 * This has never happened _in the history of time-zones and thus has no special handling. 594 * However, if it were to happen, then the list would return more than 2 entries. 595 * 596 * @param localDateTime the local date-time to query for valid offsets, not null, but null 597 * may be ignored if the rules have a single offset for all instants 598 * @return the list of valid offsets, may be immutable, not null 599 */ 600 public List!(ZoneOffset) getValidOffsets(LocalDateTime localDateTime) 601 { 602 // should probably be optimized 603 Object info = getOffsetInfo(localDateTime); 604 if (cast(ZoneOffsetTransition)(info) !is null) 605 { 606 return (cast(ZoneOffsetTransition) info).getValidOffsets(); 607 } 608 return Collections.singletonList(cast(ZoneOffset) info); 609 } 610 611 /** 612 * Gets the offset transition applicable at the specified local date-time _in these rules. 613 * !(p) 614 * The mapping from a local date-time to an offset is not straightforward. 615 * There are three cases: 616 * !(ul) 617 * !(li)Normal, with one valid offset. For the vast majority of the year, the normal 618 * case applies, where there is a single valid offset for the local date-time.</li> 619 * !(li)Gap, with zero valid offsets. This is when clocks jump forward typically 620 * due to the spring daylight savings change from "winter" to "summer". 621 * In a gap there are local date-time values with no valid offset.</li> 622 * !(li)Overlap, with two valid offsets. This is when clocks are set back typically 623 * due to the autumn daylight savings change from "summer" to "winter". 624 * In an overlap there are local date-time values with two valid offsets.</li> 625 * </ul> 626 * A transition is used to model the cases of a Gap or Overlap. 627 * The Normal case will return null. 628 * !(p) 629 * There are various ways to handle the conversion from a {@code LocalDateTime}. 630 * One technique, using this method, would be: 631 * !(pre) 632 * ZoneOffsetTransition trans = rules.getTransition(localDT); 633 * if (trans !is null) { 634 * // Gap or Overlap: determine what to do from transition 635 * } else { 636 * // Normal case: only one valid offset 637 * zoneOffset = rule.getOffset(localDT); 638 * } 639 * </pre> 640 * 641 * @param localDateTime the local date-time to query for offset transition, not null, but null 642 * may be ignored if the rules have a single offset for all instants 643 * @return the offset transition, null if the local date-time is not _in transition 644 */ 645 public ZoneOffsetTransition getTransition(LocalDateTime localDateTime) 646 { 647 Object info = getOffsetInfo(localDateTime); 648 return (cast(ZoneOffsetTransition)(info) !is null ? cast(ZoneOffsetTransition) info : null); 649 } 650 651 private Object getOffsetInfo(LocalDateTime dt) 652 { 653 if (savingsInstantTransitions.length == 0) 654 { 655 return standardOffsets[0]; 656 } 657 // check if using last rules 658 if (lastRules.length > 0 659 && dt.isAfter(savingsLocalTransitions[savingsLocalTransitions.length - 1])) 660 { 661 ZoneOffsetTransition[] transArray = findTransitionArray(dt.getYear()); 662 Object info = null; 663 foreach (ZoneOffsetTransition trans; transArray) 664 { 665 info = findOffsetInfo(dt, trans); 666 if (cast(ZoneOffsetTransition)(info) !is null || (info == trans.getOffsetBefore())) 667 { 668 return info; 669 } 670 } 671 return info; 672 } 673 674 // using historic rules 675 int index = ArrayHelper.binarySearch(savingsLocalTransitions, dt); 676 if (index == -1) 677 { 678 // before first transition 679 return wallOffsets[0]; 680 } 681 if (index < 0) 682 { 683 // switch negative insert position to start of matched range 684 index = -index - 2; 685 } 686 else if (index < savingsLocalTransitions.length - 1 687 && (savingsLocalTransitions[index] == savingsLocalTransitions[index + 1])) 688 { 689 // handle overlap immediately following gap 690 index++; 691 } 692 if ((index & 1) == 0) 693 { 694 // gap or overlap 695 LocalDateTime dtBefore = savingsLocalTransitions[index]; 696 LocalDateTime dtAfter = savingsLocalTransitions[index + 1]; 697 ZoneOffset offsetBefore = wallOffsets[index / 2]; 698 ZoneOffset offsetAfter = wallOffsets[index / 2 + 1]; 699 if (offsetAfter.getTotalSeconds() > offsetBefore.getTotalSeconds()) 700 { 701 // gap 702 return new ZoneOffsetTransition(dtBefore, offsetBefore, offsetAfter); 703 } 704 else 705 { 706 // overlap 707 return new ZoneOffsetTransition(dtAfter, offsetBefore, offsetAfter); 708 } 709 } 710 else 711 { 712 // normal (neither gap or overlap) 713 return wallOffsets[index / 2 + 1]; 714 } 715 } 716 717 /** 718 * Finds the offset info for a local date-time and transition. 719 * 720 * @param dt the date-time, not null 721 * @param trans the transition, not null 722 * @return the offset info, not null 723 */ 724 private Object findOffsetInfo(LocalDateTime dt, ZoneOffsetTransition trans) 725 { 726 LocalDateTime localTransition = trans.getDateTimeBefore(); 727 if (trans.isGap()) 728 { 729 if (dt.isBefore(localTransition)) 730 { 731 return trans.getOffsetBefore(); 732 } 733 if (dt.isBefore(trans.getDateTimeAfter())) 734 { 735 return trans; 736 } 737 else 738 { 739 return trans.getOffsetAfter(); 740 } 741 } 742 else 743 { 744 if (dt.isBefore(localTransition) == false) 745 { 746 return trans.getOffsetAfter(); 747 } 748 if (dt.isBefore(trans.getDateTimeAfter())) 749 { 750 return trans.getOffsetBefore(); 751 } 752 else 753 { 754 return trans; 755 } 756 } 757 } 758 759 /** 760 * Finds the appropriate transition array for the given year. 761 * 762 * @param year the year, not null 763 * @return the transition array, not null 764 */ 765 private ZoneOffsetTransition[] findTransitionArray(int year) 766 { 767 Integer yearObj = new Integer(year); // should use Year class, but this saves a class load 768 if (lastRulesCache.containsKey(yearObj)) 769 { 770 return lastRulesCache.get(yearObj); 771 } 772 ZoneOffsetTransitionRule[] ruleArray = lastRules; 773 ZoneOffsetTransition[] transArray = new ZoneOffsetTransition[ruleArray.length]; 774 for (int i = 0; i < ruleArray.length; i++) 775 { 776 transArray[i] = ruleArray[i].createTransition(year); 777 } 778 if (year < LAST_CACHED_YEAR) 779 { 780 lastRulesCache.putIfAbsent(yearObj, transArray); 781 } 782 return transArray; 783 } 784 785 /** 786 * Gets the standard offset for the specified instant _in this zone. 787 * !(p) 788 * This provides access to historic information on how the standard offset 789 * has changed over time. 790 * The standard offset is the offset before any daylight saving time is applied. 791 * This is typically the offset applicable during winter. 792 * 793 * @param instant the instant to find the offset information for, not null, but null 794 * may be ignored if the rules have a single offset for all instants 795 * @return the standard offset, not null 796 */ 797 public ZoneOffset getStandardOffset(Instant instant) 798 { 799 if (savingsInstantTransitions.length == 0) 800 { 801 return standardOffsets[0]; 802 } 803 long epochSec = instant.getEpochSecond(); 804 int index = ArrayHelper.binarySearch(standardTransitions, epochSec); 805 if (index < 0) 806 { 807 // switch negative insert position to start of matched range 808 index = -index - 2; 809 } 810 return standardOffsets[index + 1]; 811 } 812 813 /** 814 * Gets the amount of daylight savings _in use for the specified instant _in this zone. 815 * !(p) 816 * This provides access to historic information on how the amount of daylight 817 * savings has changed over time. 818 * This is the difference between the standard offset and the actual offset. 819 * Typically the amount is zero during winter and one hour during summer. 820 * Time-zones are second-based, so the nanosecond part of the duration will be zero. 821 * !(p) 822 * This default implementation calculates the duration from the 823 * {@link #getOffset(hunt.time.Instant) actual} and 824 * {@link #getStandardOffset(hunt.time.Instant) standard} offsets. 825 * 826 * @param instant the instant to find the daylight savings for, not null, but null 827 * may be ignored if the rules have a single offset for all instants 828 * @return the difference between the standard and actual offset, not null 829 */ 830 // public Duration getDaylightSavings(Instant instant) 831 // { 832 // if (savingsInstantTransitions.length == 0) 833 // { 834 // return Duration.ZERO; 835 // } 836 // ZoneOffset standardOffset = getStandardOffset(instant); 837 // ZoneOffset actualOffset = getOffset(instant); 838 // return Duration.ofSeconds(actualOffset.getTotalSeconds() - standardOffset.getTotalSeconds()); 839 // } 840 841 /** 842 * Checks if the specified instant is _in daylight savings. 843 * !(p) 844 * This checks if the standard offset and the actual offset are the same 845 * for the specified instant. 846 * If they are not, it is assumed that daylight savings is _in operation. 847 * !(p) 848 * This default implementation compares the {@link #getOffset(hunt.time.Instant) actual} 849 * and {@link #getStandardOffset(hunt.time.Instant) standard} offsets. 850 * 851 * @param instant the instant to find the offset information for, not null, but null 852 * may be ignored if the rules have a single offset for all instants 853 * @return the standard offset, not null 854 */ 855 public bool isDaylightSavings(Instant instant) 856 { 857 return ((getStandardOffset(instant) == getOffset(instant)) == false); 858 } 859 860 /** 861 * Checks if the offset date-time is valid for these rules. 862 * !(p) 863 * To be valid, the local date-time must not be _in a gap and the offset 864 * must match one of the valid offsets. 865 * !(p) 866 * This default implementation checks if {@link #getValidOffsets(hunt.time.LocalDateTime)} 867 * contains the specified offset. 868 * 869 * @param localDateTime the date-time to check, not null, but null 870 * may be ignored if the rules have a single offset for all instants 871 * @param offset the offset to check, null returns false 872 * @return true if the offset date-time is valid for these rules 873 */ 874 public bool isValidOffset(LocalDateTime localDateTime, ZoneOffset offset) 875 { 876 return getValidOffsets(localDateTime).contains(offset); 877 } 878 879 /** 880 * Gets the next transition after the specified instant. 881 * !(p) 882 * This returns details of the next transition after the specified instant. 883 * For example, if the instant represents a point where "Summer" daylight savings time 884 * applies, then the method will return the transition to the next "Winter" time. 885 * 886 * @param instant the instant to get the next transition after, not null, but null 887 * may be ignored if the rules have a single offset for all instants 888 * @return the next transition after the specified instant, null if this is after the last transition 889 */ 890 public ZoneOffsetTransition nextTransition(Instant instant) 891 { 892 if (savingsInstantTransitions.length == 0) 893 { 894 return null; 895 } 896 long epochSec = instant.getEpochSecond(); 897 // check if using last rules 898 if (epochSec >= savingsInstantTransitions[savingsInstantTransitions.length - 1]) 899 { 900 if (lastRules.length == 0) 901 { 902 return null; 903 } 904 // search year the instant is _in 905 int year = findYear(epochSec, wallOffsets[wallOffsets.length - 1]); 906 ZoneOffsetTransition[] transArray = findTransitionArray(year); 907 foreach (ZoneOffsetTransition trans; transArray) 908 { 909 if (epochSec < trans.toEpochSecond()) 910 { 911 return trans; 912 } 913 } 914 // use first from following year 915 if (year < 999_999_999/* Year.MAX_VALUE */) 916 { 917 transArray = findTransitionArray(year + 1); 918 return transArray[0]; 919 } 920 return null; 921 } 922 923 // using historic rules 924 int index = ArrayHelper.binarySearch(savingsInstantTransitions, epochSec); 925 if (index < 0) 926 { 927 index = -index - 1; // switched value is the next transition 928 } 929 else 930 { 931 index += 1; // exact match, so need to add one to get the next 932 } 933 return new ZoneOffsetTransition(savingsInstantTransitions[index], 934 wallOffsets[index], wallOffsets[index + 1]); 935 } 936 937 /** 938 * Gets the previous transition before the specified instant. 939 * !(p) 940 * This returns details of the previous transition before the specified instant. 941 * For example, if the instant represents a point where "summer" daylight saving time 942 * applies, then the method will return the transition from the previous "winter" time. 943 * 944 * @param instant the instant to get the previous transition after, not null, but null 945 * may be ignored if the rules have a single offset for all instants 946 * @return the previous transition before the specified instant, null if this is before the first transition 947 */ 948 public ZoneOffsetTransition previousTransition(Instant instant) 949 { 950 if (savingsInstantTransitions.length == 0) 951 { 952 return null; 953 } 954 long epochSec = instant.getEpochSecond(); 955 if (instant.getNano() > 0 && epochSec < Long.MAX_VALUE) 956 { 957 epochSec += 1; // allow rest of method to only use seconds 958 } 959 960 // check if using last rules 961 long lastHistoric = savingsInstantTransitions[savingsInstantTransitions.length - 1]; 962 if (lastRules.length > 0 && epochSec > lastHistoric) 963 { 964 // search year the instant is _in 965 ZoneOffset lastHistoricOffset = wallOffsets[wallOffsets.length - 1]; 966 int year = findYear(epochSec, lastHistoricOffset); 967 ZoneOffsetTransition[] transArray = findTransitionArray(year); 968 for (int i = cast(int)(transArray.length) - 1; i >= 0; i--) 969 { 970 if (epochSec > transArray[i].toEpochSecond()) 971 { 972 return transArray[i]; 973 } 974 } 975 // use last from preceding year 976 int lastHistoricYear = findYear(lastHistoric, lastHistoricOffset); 977 if (--year > lastHistoricYear) 978 { 979 transArray = findTransitionArray(year); 980 return transArray[transArray.length - 1]; 981 } 982 // drop through 983 } 984 985 // using historic rules 986 int index = ArrayHelper.binarySearch(savingsInstantTransitions, epochSec); 987 if (index < 0) 988 { 989 index = -index - 1; 990 } 991 if (index <= 0) 992 { 993 return null; 994 } 995 return new ZoneOffsetTransition(savingsInstantTransitions[index - 1], 996 wallOffsets[index - 1], wallOffsets[index]); 997 } 998 999 private int findYear(long epochSecond, ZoneOffset offset) 1000 { 1001 // inline for performance 1002 long localSecond = epochSecond + offset.getTotalSeconds(); 1003 long localEpochDay = MathHelper.floorDiv(localSecond, 86400); 1004 return LocalDate.ofEpochDay(localEpochDay).getYear(); 1005 } 1006 1007 /** 1008 * Gets the complete list of fully defined transitions. 1009 * !(p) 1010 * The complete set of transitions for this rules instance is defined by this method 1011 * and {@link #getTransitionRules()}. This method returns those transitions that have 1012 * been fully defined. These are typically historical, but may be _in the future. 1013 * !(p) 1014 * The list will be empty for fixed offset rules and for any time-zone where there has 1015 * only ever been a single offset. The list will also be empty if the transition rules are unknown. 1016 * 1017 * @return an immutable list of fully defined transitions, not null 1018 */ 1019 public List!(ZoneOffsetTransition) getTransitions() 1020 { 1021 List!(ZoneOffsetTransition) list = new ArrayList!(ZoneOffsetTransition)(); 1022 for (int i = 0; i < savingsInstantTransitions.length; i++) 1023 { 1024 list.add(new ZoneOffsetTransition(savingsInstantTransitions[i], 1025 wallOffsets[i], wallOffsets[i + 1])); 1026 } 1027 return /* Collections.unmodifiableList */ (list); 1028 } 1029 1030 /** 1031 * Gets the list of transition rules for years beyond those defined _in the transition list. 1032 * !(p) 1033 * The complete set of transitions for this rules instance is defined by this method 1034 * and {@link #getTransitions()}. This method returns instances of {@link ZoneOffsetTransitionRule} 1035 * that define an algorithm for when transitions will occur. 1036 * !(p) 1037 * For any given {@code ZoneRules}, this list contains the transition rules for years 1038 * beyond those years that have been fully defined. These rules typically refer to future 1039 * daylight saving time rule changes. 1040 * !(p) 1041 * If the zone defines daylight savings into the future, then the list will normally 1042 * be of size two and hold information about entering and exiting daylight savings. 1043 * If the zone does not have daylight savings, or information about future changes 1044 * is uncertain, then the list will be empty. 1045 * !(p) 1046 * The list will be empty for fixed offset rules and for any time-zone where there is no 1047 * daylight saving time. The list will also be empty if the transition rules are unknown. 1048 * 1049 * @return an immutable list of transition rules, not null 1050 */ 1051 public List!(ZoneOffsetTransitionRule) getTransitionRules() 1052 { 1053 auto l = new ArrayList!ZoneOffsetTransitionRule(); 1054 foreach (item; lastRules) 1055 { 1056 l.add(item); 1057 } 1058 return l; 1059 } 1060 1061 /** 1062 * Checks if this set of rules equals another. 1063 * !(p) 1064 * Two rule sets are equal if they will always result _in the same output 1065 * for any given input instant or local date-time. 1066 * Rules from two different groups may return false even if they are _in fact the same. 1067 * !(p) 1068 * This definition should result _in implementations comparing their entire state. 1069 * 1070 * @param otherRules the other rules, null returns false 1071 * @return true if this rules is the same as that specified 1072 */ 1073 override public bool opEquals(Object otherRules) 1074 { 1075 if (this == otherRules) 1076 { 1077 return true; 1078 } 1079 import std.algorithm : equal; 1080 1081 if (cast(ZoneRules)(otherRules) !is null) 1082 { 1083 ZoneRules other = cast(ZoneRules) otherRules; 1084 return equal(standardTransitions, other.standardTransitions) 1085 && equal(standardOffsets, other.standardOffsets) 1086 && equal(savingsInstantTransitions, 1087 other.savingsInstantTransitions) && equal(wallOffsets, 1088 other.wallOffsets) && equal(lastRules, other.lastRules); 1089 } 1090 return false; 1091 } 1092 1093 /** 1094 * Returns a suitable hash code given the definition of {@code #equals}. 1095 * 1096 * @return the hash code 1097 */ 1098 override public size_t toHash() @trusted nothrow 1099 { 1100 return hashOf(standardTransitions) ^ hashOf(standardOffsets) ^ hashOf( 1101 savingsInstantTransitions) ^ hashOf(wallOffsets) ^ hashOf(lastRules); 1102 } 1103 1104 /** 1105 * Returns a string describing this object. 1106 * 1107 * @return a string for debugging, not null 1108 */ 1109 override public string toString() 1110 { 1111 return "ZoneRules[currentStandardOffset=" 1112 ~ standardOffsets[standardOffsets.length - 1].toString ~ "]"; 1113 } 1114 1115 }