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="../../../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 }
1028