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