Path: blob/master/src/java.base/share/classes/java/time/zone/ZoneRules.java
41159 views
/*1* Copyright (c) 2012, 2021, Oracle and/or its affiliates. All rights reserved.2* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.3*4* This code is free software; you can redistribute it and/or modify it5* under the terms of the GNU General Public License version 2 only, as6* published by the Free Software Foundation. Oracle designates this7* particular file as subject to the "Classpath" exception as provided8* by Oracle in the LICENSE file that accompanied this code.9*10* This code is distributed in the hope that it will be useful, but WITHOUT11* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or12* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License13* version 2 for more details (a copy is included in the LICENSE file that14* accompanied this code).15*16* You should have received a copy of the GNU General Public License version17* 2 along with this work; if not, write to the Free Software Foundation,18* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.19*20* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA21* or visit www.oracle.com if you need additional information or have any22* questions.23*/2425/*26* This file is available under and governed by the GNU General Public27* License version 2 only, as published by the Free Software Foundation.28* However, the following notice accompanied the original version of this29* file:30*31* Copyright (c) 2009-2012, Stephen Colebourne & Michael Nascimento Santos32*33* All rights reserved.34*35* Redistribution and use in source and binary forms, with or without36* modification, are permitted provided that the following conditions are met:37*38* * Redistributions of source code must retain the above copyright notice,39* this list of conditions and the following disclaimer.40*41* * Redistributions in binary form must reproduce the above copyright notice,42* this list of conditions and the following disclaimer in the documentation43* and/or other materials provided with the distribution.44*45* * Neither the name of JSR-310 nor the names of its contributors46* may be used to endorse or promote products derived from this software47* without specific prior written permission.48*49* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS50* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT51* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR52* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR53* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,54* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,55* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR56* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF57* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING58* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS59* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.60*/61package java.time.zone;6263import java.io.DataInput;64import java.io.DataOutput;65import java.io.IOException;66import java.io.InvalidObjectException;67import java.io.ObjectInputStream;68import java.io.Serializable;69import java.time.Duration;70import java.time.Instant;71import java.time.LocalDateTime;72import java.time.ZoneId;73import java.time.ZoneOffset;74import java.time.Year;75import java.util.ArrayList;76import java.util.Arrays;77import java.util.Collections;78import java.util.List;79import java.util.Objects;80import java.util.concurrent.ConcurrentHashMap;81import java.util.concurrent.ConcurrentMap;8283/**84* The rules defining how the zone offset varies for a single time-zone.85* <p>86* The rules model all the historic and future transitions for a time-zone.87* {@link ZoneOffsetTransition} is used for known transitions, typically historic.88* {@link ZoneOffsetTransitionRule} is used for future transitions that are based89* on the result of an algorithm.90* <p>91* The rules are loaded via {@link ZoneRulesProvider} using a {@link ZoneId}.92* The same rules may be shared internally between multiple zone IDs.93* <p>94* Serializing an instance of {@code ZoneRules} will store the entire set of rules.95* It does not store the zone ID as it is not part of the state of this object.96* <p>97* A rule implementation may or may not store full information about historic98* and future transitions, and the information stored is only as accurate as99* that supplied to the implementation by the rules provider.100* Applications should treat the data provided as representing the best information101* available to the implementation of this rule.102*103* @implSpec104* This class is immutable and thread-safe.105*106* @since 1.8107*/108public final class ZoneRules implements Serializable {109110/**111* Serialization version.112*/113private static final long serialVersionUID = 3044319355680032515L;114/**115* The last year to have its transitions cached.116*/117private static final int LAST_CACHED_YEAR = 2100;118119/**120* The transitions between standard offsets (epoch seconds), sorted.121*/122private final long[] standardTransitions;123/**124* The standard offsets.125*/126private final ZoneOffset[] standardOffsets;127/**128* The transitions between instants (epoch seconds), sorted.129*/130private final long[] savingsInstantTransitions;131/**132* The transitions between local date-times, sorted.133* This is a paired array, where the first entry is the start of the transition134* and the second entry is the end of the transition.135*/136private final LocalDateTime[] savingsLocalTransitions;137/**138* The wall offsets.139*/140private final ZoneOffset[] wallOffsets;141/**142* The last rule.143*/144private final ZoneOffsetTransitionRule[] lastRules;145/**146* The map of recent transitions.147*/148private final transient ConcurrentMap<Integer, ZoneOffsetTransition[]> lastRulesCache =149new ConcurrentHashMap<Integer, ZoneOffsetTransition[]>();150/**151* The zero-length long array.152*/153private static final long[] EMPTY_LONG_ARRAY = new long[0];154/**155* The zero-length lastrules array.156*/157private static final ZoneOffsetTransitionRule[] EMPTY_LASTRULES =158new ZoneOffsetTransitionRule[0];159/**160* The zero-length ldt array.161*/162private static final LocalDateTime[] EMPTY_LDT_ARRAY = new LocalDateTime[0];163/**164* The number of days in a 400 year cycle.165*/166private static final int DAYS_PER_CYCLE = 146097;167/**168* The number of days from year zero to year 1970.169* There are five 400 year cycles from year zero to 2000.170* There are 7 leap years from 1970 to 2000.171*/172private static final long DAYS_0000_TO_1970 = (DAYS_PER_CYCLE * 5L) - (30L * 365L + 7L);173174/**175* Obtains an instance of a ZoneRules.176*177* @param baseStandardOffset the standard offset to use before legal rules were set, not null178* @param baseWallOffset the wall offset to use before legal rules were set, not null179* @param standardOffsetTransitionList the list of changes to the standard offset, not null180* @param transitionList the list of transitions, not null181* @param lastRules the recurring last rules, size 16 or less, not null182* @return the zone rules, not null183*/184public static ZoneRules of(ZoneOffset baseStandardOffset,185ZoneOffset baseWallOffset,186List<ZoneOffsetTransition> standardOffsetTransitionList,187List<ZoneOffsetTransition> transitionList,188List<ZoneOffsetTransitionRule> lastRules) {189Objects.requireNonNull(baseStandardOffset, "baseStandardOffset");190Objects.requireNonNull(baseWallOffset, "baseWallOffset");191Objects.requireNonNull(standardOffsetTransitionList, "standardOffsetTransitionList");192Objects.requireNonNull(transitionList, "transitionList");193Objects.requireNonNull(lastRules, "lastRules");194return new ZoneRules(baseStandardOffset, baseWallOffset,195standardOffsetTransitionList, transitionList, lastRules);196}197198/**199* Obtains an instance of ZoneRules that has fixed zone rules.200*201* @param offset the offset this fixed zone rules is based on, not null202* @return the zone rules, not null203* @see #isFixedOffset()204*/205public static ZoneRules of(ZoneOffset offset) {206Objects.requireNonNull(offset, "offset");207return new ZoneRules(offset);208}209210/**211* Creates an instance.212*213* @param baseStandardOffset the standard offset to use before legal rules were set, not null214* @param baseWallOffset the wall offset to use before legal rules were set, not null215* @param standardOffsetTransitionList the list of changes to the standard offset, not null216* @param transitionList the list of transitions, not null217* @param lastRules the recurring last rules, size 16 or less, not null218*/219ZoneRules(ZoneOffset baseStandardOffset,220ZoneOffset baseWallOffset,221List<ZoneOffsetTransition> standardOffsetTransitionList,222List<ZoneOffsetTransition> transitionList,223List<ZoneOffsetTransitionRule> lastRules) {224super();225226// convert standard transitions227228this.standardTransitions = new long[standardOffsetTransitionList.size()];229230this.standardOffsets = new ZoneOffset[standardOffsetTransitionList.size() + 1];231this.standardOffsets[0] = baseStandardOffset;232for (int i = 0; i < standardOffsetTransitionList.size(); i++) {233this.standardTransitions[i] = standardOffsetTransitionList.get(i).toEpochSecond();234this.standardOffsets[i + 1] = standardOffsetTransitionList.get(i).getOffsetAfter();235}236237// convert savings transitions to locals238List<LocalDateTime> localTransitionList = new ArrayList<>();239List<ZoneOffset> localTransitionOffsetList = new ArrayList<>();240localTransitionOffsetList.add(baseWallOffset);241for (ZoneOffsetTransition trans : transitionList) {242if (trans.isGap()) {243localTransitionList.add(trans.getDateTimeBefore());244localTransitionList.add(trans.getDateTimeAfter());245} else {246localTransitionList.add(trans.getDateTimeAfter());247localTransitionList.add(trans.getDateTimeBefore());248}249localTransitionOffsetList.add(trans.getOffsetAfter());250}251this.savingsLocalTransitions = localTransitionList.toArray(new LocalDateTime[localTransitionList.size()]);252this.wallOffsets = localTransitionOffsetList.toArray(new ZoneOffset[localTransitionOffsetList.size()]);253254// convert savings transitions to instants255this.savingsInstantTransitions = new long[transitionList.size()];256for (int i = 0; i < transitionList.size(); i++) {257this.savingsInstantTransitions[i] = transitionList.get(i).toEpochSecond();258}259260// last rules261Object[] temp = lastRules.toArray();262ZoneOffsetTransitionRule[] rulesArray = Arrays.copyOf(temp, temp.length, ZoneOffsetTransitionRule[].class);263if (rulesArray.length > 16) {264throw new IllegalArgumentException("Too many transition rules");265}266this.lastRules = rulesArray;267}268269/**270* Constructor.271*272* @param standardTransitions the standard transitions, not null273* @param standardOffsets the standard offsets, not null274* @param savingsInstantTransitions the standard transitions, not null275* @param wallOffsets the wall offsets, not null276* @param lastRules the recurring last rules, size 15 or less, not null277*/278private ZoneRules(long[] standardTransitions,279ZoneOffset[] standardOffsets,280long[] savingsInstantTransitions,281ZoneOffset[] wallOffsets,282ZoneOffsetTransitionRule[] lastRules) {283super();284285this.standardTransitions = standardTransitions;286this.standardOffsets = standardOffsets;287this.savingsInstantTransitions = savingsInstantTransitions;288this.wallOffsets = wallOffsets;289this.lastRules = lastRules;290291if (savingsInstantTransitions.length == 0) {292this.savingsLocalTransitions = EMPTY_LDT_ARRAY;293} else {294// convert savings transitions to locals295List<LocalDateTime> localTransitionList = new ArrayList<>();296for (int i = 0; i < savingsInstantTransitions.length; i++) {297ZoneOffset before = wallOffsets[i];298ZoneOffset after = wallOffsets[i + 1];299ZoneOffsetTransition trans = new ZoneOffsetTransition(savingsInstantTransitions[i], before, after);300if (trans.isGap()) {301localTransitionList.add(trans.getDateTimeBefore());302localTransitionList.add(trans.getDateTimeAfter());303} else {304localTransitionList.add(trans.getDateTimeAfter());305localTransitionList.add(trans.getDateTimeBefore());306}307}308this.savingsLocalTransitions = localTransitionList.toArray(new LocalDateTime[localTransitionList.size()]);309}310}311312/**313* Creates an instance of ZoneRules that has fixed zone rules.314*315* @param offset the offset this fixed zone rules is based on, not null316* @see #isFixedOffset()317*/318private ZoneRules(ZoneOffset offset) {319this.standardOffsets = new ZoneOffset[1];320this.standardOffsets[0] = offset;321this.standardTransitions = EMPTY_LONG_ARRAY;322this.savingsInstantTransitions = EMPTY_LONG_ARRAY;323this.savingsLocalTransitions = EMPTY_LDT_ARRAY;324this.wallOffsets = standardOffsets;325this.lastRules = EMPTY_LASTRULES;326}327328/**329* Defend against malicious streams.330*331* @param s the stream to read332* @throws InvalidObjectException always333*/334private void readObject(ObjectInputStream s) throws InvalidObjectException {335throw new InvalidObjectException("Deserialization via serialization delegate");336}337338/**339* Writes the object using a340* <a href="{@docRoot}/serialized-form.html#java.time.zone.Ser">dedicated serialized form</a>.341* @serialData342* <pre style="font-size:1.0em">{@code343*344* out.writeByte(1); // identifies a ZoneRules345* out.writeInt(standardTransitions.length);346* for (long trans : standardTransitions) {347* Ser.writeEpochSec(trans, out);348* }349* for (ZoneOffset offset : standardOffsets) {350* Ser.writeOffset(offset, out);351* }352* out.writeInt(savingsInstantTransitions.length);353* for (long trans : savingsInstantTransitions) {354* Ser.writeEpochSec(trans, out);355* }356* for (ZoneOffset offset : wallOffsets) {357* Ser.writeOffset(offset, out);358* }359* out.writeByte(lastRules.length);360* for (ZoneOffsetTransitionRule rule : lastRules) {361* rule.writeExternal(out);362* }363* }364* </pre>365* <p>366* Epoch second values used for offsets are encoded in a variable367* length form to make the common cases put fewer bytes in the stream.368* <pre style="font-size:1.0em">{@code369*370* static void writeEpochSec(long epochSec, DataOutput out) throws IOException {371* if (epochSec >= -4575744000L && epochSec < 10413792000L && epochSec % 900 == 0) { // quarter hours between 1825 and 2300372* int store = (int) ((epochSec + 4575744000L) / 900);373* out.writeByte((store >>> 16) & 255);374* out.writeByte((store >>> 8) & 255);375* out.writeByte(store & 255);376* } else {377* out.writeByte(255);378* out.writeLong(epochSec);379* }380* }381* }382* </pre>383* <p>384* ZoneOffset values are encoded in a variable length form so the385* common cases put fewer bytes in the stream.386* <pre style="font-size:1.0em">{@code387*388* static void writeOffset(ZoneOffset offset, DataOutput out) throws IOException {389* final int offsetSecs = offset.getTotalSeconds();390* int offsetByte = offsetSecs % 900 == 0 ? offsetSecs / 900 : 127; // compress to -72 to +72391* out.writeByte(offsetByte);392* if (offsetByte == 127) {393* out.writeInt(offsetSecs);394* }395* }396*}397* </pre>398* @return the replacing object, not null399*/400private Object writeReplace() {401return new Ser(Ser.ZRULES, this);402}403404/**405* Writes the state to the stream.406*407* @param out the output stream, not null408* @throws IOException if an error occurs409*/410void writeExternal(DataOutput out) throws IOException {411out.writeInt(standardTransitions.length);412for (long trans : standardTransitions) {413Ser.writeEpochSec(trans, out);414}415for (ZoneOffset offset : standardOffsets) {416Ser.writeOffset(offset, out);417}418out.writeInt(savingsInstantTransitions.length);419for (long trans : savingsInstantTransitions) {420Ser.writeEpochSec(trans, out);421}422for (ZoneOffset offset : wallOffsets) {423Ser.writeOffset(offset, out);424}425out.writeByte(lastRules.length);426for (ZoneOffsetTransitionRule rule : lastRules) {427rule.writeExternal(out);428}429}430431/**432* Reads the state from the stream.433*434* @param in the input stream, not null435* @return the created object, not null436* @throws IOException if an error occurs437*/438static ZoneRules readExternal(DataInput in) throws IOException, ClassNotFoundException {439int stdSize = in.readInt();440long[] stdTrans = (stdSize == 0) ? EMPTY_LONG_ARRAY441: new long[stdSize];442for (int i = 0; i < stdSize; i++) {443stdTrans[i] = Ser.readEpochSec(in);444}445ZoneOffset[] stdOffsets = new ZoneOffset[stdSize + 1];446for (int i = 0; i < stdOffsets.length; i++) {447stdOffsets[i] = Ser.readOffset(in);448}449int savSize = in.readInt();450long[] savTrans = (savSize == 0) ? EMPTY_LONG_ARRAY451: new long[savSize];452for (int i = 0; i < savSize; i++) {453savTrans[i] = Ser.readEpochSec(in);454}455ZoneOffset[] savOffsets = new ZoneOffset[savSize + 1];456for (int i = 0; i < savOffsets.length; i++) {457savOffsets[i] = Ser.readOffset(in);458}459int ruleSize = in.readByte();460ZoneOffsetTransitionRule[] rules = (ruleSize == 0) ?461EMPTY_LASTRULES : new ZoneOffsetTransitionRule[ruleSize];462for (int i = 0; i < ruleSize; i++) {463rules[i] = ZoneOffsetTransitionRule.readExternal(in);464}465return new ZoneRules(stdTrans, stdOffsets, savTrans, savOffsets, rules);466}467468/**469* Checks of the zone rules are fixed, such that the offset never varies.470*471* @return true if the time-zone is fixed and the offset never changes472*/473public boolean isFixedOffset() {474return standardOffsets[0].equals(wallOffsets[0]) &&475standardTransitions.length == 0 &&476savingsInstantTransitions.length == 0 &&477lastRules.length == 0;478}479480/**481* Gets the offset applicable at the specified instant in these rules.482* <p>483* The mapping from an instant to an offset is simple, there is only484* one valid offset for each instant.485* This method returns that offset.486*487* @param instant the instant to find the offset for, not null, but null488* may be ignored if the rules have a single offset for all instants489* @return the offset, not null490*/491public ZoneOffset getOffset(Instant instant) {492if (savingsInstantTransitions.length == 0) {493return wallOffsets[0];494}495long epochSec = instant.getEpochSecond();496// check if using last rules497if (lastRules.length > 0 &&498epochSec > savingsInstantTransitions[savingsInstantTransitions.length - 1]) {499int year = findYear(epochSec, wallOffsets[wallOffsets.length - 1]);500ZoneOffsetTransition[] transArray = findTransitionArray(year);501ZoneOffsetTransition trans = null;502for (int i = 0; i < transArray.length; i++) {503trans = transArray[i];504if (epochSec < trans.toEpochSecond()) {505return trans.getOffsetBefore();506}507}508return trans.getOffsetAfter();509}510511// using historic rules512int index = Arrays.binarySearch(savingsInstantTransitions, epochSec);513if (index < 0) {514// switch negative insert position to start of matched range515index = -index - 2;516}517return wallOffsets[index + 1];518}519520/**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 normal527* 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 typically529* 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 typically532* 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 Overlap537* 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, rather540* than the "correct" value, it should be treated with care. Applications that care541* 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 null545* may be ignored if the rules have a single offset for all instants546* @return the best available offset for the local date-time, not null547*/548public ZoneOffset getOffset(LocalDateTime localDateTime) {549Object info = getOffsetInfo(localDateTime);550if (info instanceof ZoneOffsetTransition) {551return ((ZoneOffsetTransition) info).getOffsetBefore();552}553return (ZoneOffset) info;554}555556/**557* Gets the offset applicable at the specified local date-time in these rules.558* <p>559* The mapping from a local date-time to an offset is not straightforward.560* There are three cases:561* <ul>562* <li>Normal, with one valid offset. For the vast majority of the year, the normal563* case applies, where there is a single valid offset for the local date-time.</li>564* <li>Gap, with zero valid offsets. This is when clocks jump forward typically565* due to the spring daylight savings change from "winter" to "summer".566* In a gap there are local date-time values with no valid offset.</li>567* <li>Overlap, with two valid offsets. This is when clocks are set back typically568* due to the autumn daylight savings change from "summer" to "winter".569* In an overlap there are local date-time values with two valid offsets.</li>570* </ul>571* Thus, for any given local date-time there can be zero, one or two valid offsets.572* This method returns that list of valid offsets, which is a list of size 0, 1 or 2.573* In the case where there are two offsets, the earlier offset is returned at index 0574* and the later offset at index 1.575* <p>576* There are various ways to handle the conversion from a {@code LocalDateTime}.577* One technique, using this method, would be:578* <pre>579* List<ZoneOffset> validOffsets = rules.getValidOffsets(localDT);580* if (validOffsets.size() == 1) {581* // Normal case: only one valid offset582* zoneOffset = validOffsets.get(0);583* } else {584* // Gap or Overlap: determine what to do from transition (which will be non-null)585* ZoneOffsetTransition trans = rules.getTransition(localDT);586* }587* </pre>588* <p>589* In theory, it is possible for there to be more than two valid offsets.590* This would happen if clocks to be put back more than once in quick succession.591* This has never happened in the history of time-zones and thus has no special handling.592* However, if it were to happen, then the list would return more than 2 entries.593*594* @param localDateTime the local date-time to query for valid offsets, not null, but null595* may be ignored if the rules have a single offset for all instants596* @return the list of valid offsets, may be immutable, not null597*/598public List<ZoneOffset> getValidOffsets(LocalDateTime localDateTime) {599// should probably be optimized600Object info = getOffsetInfo(localDateTime);601if (info instanceof ZoneOffsetTransition) {602return ((ZoneOffsetTransition) info).getValidOffsets();603}604return Collections.singletonList((ZoneOffset) info);605}606607/**608* Gets the offset transition applicable at the specified local date-time in these rules.609* <p>610* The mapping from a local date-time to an offset is not straightforward.611* There are three cases:612* <ul>613* <li>Normal, with one valid offset. For the vast majority of the year, the normal614* case applies, where there is a single valid offset for the local date-time.</li>615* <li>Gap, with zero valid offsets. This is when clocks jump forward typically616* due to the spring daylight savings change from "winter" to "summer".617* In a gap there are local date-time values with no valid offset.</li>618* <li>Overlap, with two valid offsets. This is when clocks are set back typically619* due to the autumn daylight savings change from "summer" to "winter".620* In an overlap there are local date-time values with two valid offsets.</li>621* </ul>622* A transition is used to model the cases of a Gap or Overlap.623* The Normal case will return null.624* <p>625* There are various ways to handle the conversion from a {@code LocalDateTime}.626* One technique, using this method, would be:627* <pre>628* ZoneOffsetTransition trans = rules.getTransition(localDT);629* if (trans != null) {630* // Gap or Overlap: determine what to do from transition631* } else {632* // Normal case: only one valid offset633* zoneOffset = rule.getOffset(localDT);634* }635* </pre>636*637* @param localDateTime the local date-time to query for offset transition, not null, but null638* may be ignored if the rules have a single offset for all instants639* @return the offset transition, null if the local date-time is not in transition640*/641public ZoneOffsetTransition getTransition(LocalDateTime localDateTime) {642Object info = getOffsetInfo(localDateTime);643return (info instanceof ZoneOffsetTransition ? (ZoneOffsetTransition) info : null);644}645646private Object getOffsetInfo(LocalDateTime dt) {647if (savingsLocalTransitions.length == 0) {648return wallOffsets[0];649}650// check if using last rules651if (lastRules.length > 0 &&652dt.isAfter(savingsLocalTransitions[savingsLocalTransitions.length - 1])) {653ZoneOffsetTransition[] transArray = findTransitionArray(dt.getYear());654Object info = null;655for (ZoneOffsetTransition trans : transArray) {656info = findOffsetInfo(dt, trans);657if (info instanceof ZoneOffsetTransition || info.equals(trans.getOffsetBefore())) {658return info;659}660}661return info;662}663664// using historic rules665int index = Arrays.binarySearch(savingsLocalTransitions, dt);666if (index == -1) {667// before first transition668return wallOffsets[0];669}670if (index < 0) {671// switch negative insert position to start of matched range672index = -index - 2;673} else if (index < savingsLocalTransitions.length - 1 &&674savingsLocalTransitions[index].equals(savingsLocalTransitions[index + 1])) {675// handle overlap immediately following gap676index++;677}678if ((index & 1) == 0) {679// gap or overlap680LocalDateTime dtBefore = savingsLocalTransitions[index];681LocalDateTime dtAfter = savingsLocalTransitions[index + 1];682ZoneOffset offsetBefore = wallOffsets[index / 2];683ZoneOffset offsetAfter = wallOffsets[index / 2 + 1];684if (offsetAfter.getTotalSeconds() > offsetBefore.getTotalSeconds()) {685// gap686return new ZoneOffsetTransition(dtBefore, offsetBefore, offsetAfter);687} else {688// overlap689return new ZoneOffsetTransition(dtAfter, offsetBefore, offsetAfter);690}691} else {692// normal (neither gap or overlap)693return wallOffsets[index / 2 + 1];694}695}696697/**698* Finds the offset info for a local date-time and transition.699*700* @param dt the date-time, not null701* @param trans the transition, not null702* @return the offset info, not null703*/704private Object findOffsetInfo(LocalDateTime dt, ZoneOffsetTransition trans) {705LocalDateTime localTransition = trans.getDateTimeBefore();706if (trans.isGap()) {707if (dt.isBefore(localTransition)) {708return trans.getOffsetBefore();709}710if (dt.isBefore(trans.getDateTimeAfter())) {711return trans;712} else {713return trans.getOffsetAfter();714}715} else {716if (dt.isBefore(localTransition) == false) {717return trans.getOffsetAfter();718}719if (dt.isBefore(trans.getDateTimeAfter())) {720return trans.getOffsetBefore();721} else {722return trans;723}724}725}726727/**728* Finds the appropriate transition array for the given year.729*730* @param year the year, not null731* @return the transition array, not null732*/733private ZoneOffsetTransition[] findTransitionArray(int year) {734Integer yearObj = year; // should use Year class, but this saves a class load735ZoneOffsetTransition[] transArray = lastRulesCache.get(yearObj);736if (transArray != null) {737return transArray;738}739ZoneOffsetTransitionRule[] ruleArray = lastRules;740transArray = new ZoneOffsetTransition[ruleArray.length];741for (int i = 0; i < ruleArray.length; i++) {742transArray[i] = ruleArray[i].createTransition(year);743}744if (year < LAST_CACHED_YEAR) {745lastRulesCache.putIfAbsent(yearObj, transArray);746}747return transArray;748}749750/**751* Gets the standard offset for the specified instant in this zone.752* <p>753* This provides access to historic information on how the standard offset754* has changed over time.755* The standard offset is the offset before any daylight saving time is applied.756* This is typically the offset applicable during winter.757*758* @param instant the instant to find the offset information for, not null, but null759* may be ignored if the rules have a single offset for all instants760* @return the standard offset, not null761*/762public ZoneOffset getStandardOffset(Instant instant) {763if (standardTransitions.length == 0) {764return standardOffsets[0];765}766long epochSec = instant.getEpochSecond();767int index = Arrays.binarySearch(standardTransitions, epochSec);768if (index < 0) {769// switch negative insert position to start of matched range770index = -index - 2;771}772return standardOffsets[index + 1];773}774775/**776* Gets the amount of daylight savings in use for the specified instant in this zone.777* <p>778* This provides access to historic information on how the amount of daylight779* savings has changed over time.780* This is the difference between the standard offset and the actual offset.781* Typically the amount is zero during winter and one hour during summer.782* Time-zones are second-based, so the nanosecond part of the duration will be zero.783* <p>784* This default implementation calculates the duration from the785* {@link #getOffset(java.time.Instant) actual} and786* {@link #getStandardOffset(java.time.Instant) standard} offsets.787*788* @param instant the instant to find the daylight savings for, not null, but null789* may be ignored if the rules have a single offset for all instants790* @return the difference between the standard and actual offset, not null791*/792public Duration getDaylightSavings(Instant instant) {793if (isFixedOffset()) {794return Duration.ZERO;795}796ZoneOffset standardOffset = getStandardOffset(instant);797ZoneOffset actualOffset = getOffset(instant);798return Duration.ofSeconds(actualOffset.getTotalSeconds() - standardOffset.getTotalSeconds());799}800801/**802* Checks if the specified instant is in daylight savings.803* <p>804* This checks if the standard offset and the actual offset are the same805* for the specified instant.806* If they are not, it is assumed that daylight savings is in operation.807* <p>808* This default implementation compares the {@link #getOffset(java.time.Instant) actual}809* and {@link #getStandardOffset(java.time.Instant) standard} offsets.810*811* @param instant the instant to find the offset information for, not null, but null812* may be ignored if the rules have a single offset for all instants813* @return the standard offset, not null814*/815public boolean isDaylightSavings(Instant instant) {816return (getStandardOffset(instant).equals(getOffset(instant)) == false);817}818819/**820* Checks if the offset date-time is valid for these rules.821* <p>822* To be valid, the local date-time must not be in a gap and the offset823* must match one of the valid offsets.824* <p>825* This default implementation checks if {@link #getValidOffsets(java.time.LocalDateTime)}826* contains the specified offset.827*828* @param localDateTime the date-time to check, not null, but null829* may be ignored if the rules have a single offset for all instants830* @param offset the offset to check, null returns false831* @return true if the offset date-time is valid for these rules832*/833public boolean isValidOffset(LocalDateTime localDateTime, ZoneOffset offset) {834return getValidOffsets(localDateTime).contains(offset);835}836837/**838* Gets the next transition after the specified instant.839* <p>840* This returns details of the next transition after the specified instant.841* For example, if the instant represents a point where "Summer" daylight savings time842* applies, then the method will return the transition to the next "Winter" time.843*844* @param instant the instant to get the next transition after, not null, but null845* may be ignored if the rules have a single offset for all instants846* @return the next transition after the specified instant, null if this is after the last transition847*/848public ZoneOffsetTransition nextTransition(Instant instant) {849if (savingsInstantTransitions.length == 0) {850return null;851}852long epochSec = instant.getEpochSecond();853// check if using last rules854if (epochSec >= savingsInstantTransitions[savingsInstantTransitions.length - 1]) {855if (lastRules.length == 0) {856return null;857}858// search year the instant is in859int year = findYear(epochSec, wallOffsets[wallOffsets.length - 1]);860ZoneOffsetTransition[] transArray = findTransitionArray(year);861for (ZoneOffsetTransition trans : transArray) {862if (epochSec < trans.toEpochSecond()) {863return trans;864}865}866// use first from following year867if (year < Year.MAX_VALUE) {868transArray = findTransitionArray(year + 1);869return transArray[0];870}871return null;872}873874// using historic rules875int index = Arrays.binarySearch(savingsInstantTransitions, epochSec);876if (index < 0) {877index = -index - 1; // switched value is the next transition878} else {879index += 1; // exact match, so need to add one to get the next880}881return new ZoneOffsetTransition(savingsInstantTransitions[index], wallOffsets[index], wallOffsets[index + 1]);882}883884/**885* Gets the previous transition before the specified instant.886* <p>887* This returns details of the previous transition before the specified instant.888* For example, if the instant represents a point where "summer" daylight saving time889* applies, then the method will return the transition from the previous "winter" time.890*891* @param instant the instant to get the previous transition after, not null, but null892* may be ignored if the rules have a single offset for all instants893* @return the previous transition before the specified instant, null if this is before the first transition894*/895public ZoneOffsetTransition previousTransition(Instant instant) {896if (savingsInstantTransitions.length == 0) {897return null;898}899long epochSec = instant.getEpochSecond();900if (instant.getNano() > 0 && epochSec < Long.MAX_VALUE) {901epochSec += 1; // allow rest of method to only use seconds902}903904// check if using last rules905long lastHistoric = savingsInstantTransitions[savingsInstantTransitions.length - 1];906if (lastRules.length > 0 && epochSec > lastHistoric) {907// search year the instant is in908ZoneOffset lastHistoricOffset = wallOffsets[wallOffsets.length - 1];909int year = findYear(epochSec, lastHistoricOffset);910ZoneOffsetTransition[] transArray = findTransitionArray(year);911for (int i = transArray.length - 1; i >= 0; i--) {912if (epochSec > transArray[i].toEpochSecond()) {913return transArray[i];914}915}916// use last from preceding year917int lastHistoricYear = findYear(lastHistoric, lastHistoricOffset);918if (--year > lastHistoricYear) {919transArray = findTransitionArray(year);920return transArray[transArray.length - 1];921}922// drop through923}924925// using historic rules926int index = Arrays.binarySearch(savingsInstantTransitions, epochSec);927if (index < 0) {928index = -index - 1;929}930if (index <= 0) {931return null;932}933return new ZoneOffsetTransition(savingsInstantTransitions[index - 1], wallOffsets[index - 1], wallOffsets[index]);934}935936private int findYear(long epochSecond, ZoneOffset offset) {937long localSecond = epochSecond + offset.getTotalSeconds();938long zeroDay = Math.floorDiv(localSecond, 86400) + DAYS_0000_TO_1970;939940// find the march-based year941zeroDay -= 60; // adjust to 0000-03-01 so leap day is at end of four year cycle942long adjust = 0;943if (zeroDay < 0) {944// adjust negative years to positive for calculation945long adjustCycles = (zeroDay + 1) / DAYS_PER_CYCLE - 1;946adjust = adjustCycles * 400;947zeroDay += -adjustCycles * DAYS_PER_CYCLE;948}949long yearEst = (400 * zeroDay + 591) / DAYS_PER_CYCLE;950long doyEst = zeroDay - (365 * yearEst + yearEst / 4 - yearEst / 100 + yearEst / 400);951if (doyEst < 0) {952// fix estimate953yearEst--;954doyEst = zeroDay - (365 * yearEst + yearEst / 4 - yearEst / 100 + yearEst / 400);955}956yearEst += adjust; // reset any negative year957int marchDoy0 = (int) doyEst;958959// convert march-based values back to january-based960int marchMonth0 = (marchDoy0 * 5 + 2) / 153;961yearEst += marchMonth0 / 10;962963// Cap to the max value964return (int)Math.min(yearEst, Year.MAX_VALUE);965}966967/**968* Gets the complete list of fully defined transitions.969* <p>970* The complete set of transitions for this rules instance is defined by this method971* and {@link #getTransitionRules()}. This method returns those transitions that have972* been fully defined. These are typically historical, but may be in the future.973* <p>974* The list will be empty for fixed offset rules and for any time-zone where there has975* only ever been a single offset. The list will also be empty if the transition rules are unknown.976*977* @return an immutable list of fully defined transitions, not null978*/979public List<ZoneOffsetTransition> getTransitions() {980List<ZoneOffsetTransition> list = new ArrayList<>();981for (int i = 0; i < savingsInstantTransitions.length; i++) {982list.add(new ZoneOffsetTransition(savingsInstantTransitions[i], wallOffsets[i], wallOffsets[i + 1]));983}984return Collections.unmodifiableList(list);985}986987/**988* Gets the list of transition rules for years beyond those defined in the transition list.989* <p>990* The complete set of transitions for this rules instance is defined by this method991* and {@link #getTransitions()}. This method returns instances of {@link ZoneOffsetTransitionRule}992* that define an algorithm for when transitions will occur.993* <p>994* For any given {@code ZoneRules}, this list contains the transition rules for years995* beyond those years that have been fully defined. These rules typically refer to future996* daylight saving time rule changes.997* <p>998* If the zone defines daylight savings into the future, then the list will normally999* be of size two and hold information about entering and exiting daylight savings.1000* If the zone does not have daylight savings, or information about future changes1001* is uncertain, then the list will be empty.1002* <p>1003* The list will be empty for fixed offset rules and for any time-zone where there is no1004* daylight saving time. The list will also be empty if the transition rules are unknown.1005*1006* @return an immutable list of transition rules, not null1007*/1008public List<ZoneOffsetTransitionRule> getTransitionRules() {1009return List.of(lastRules);1010}10111012/**1013* Checks if this set of rules equals another.1014* <p>1015* Two rule sets are equal if they will always result in the same output1016* for any given input instant or local date-time.1017* Rules from two different groups may return false even if they are in fact the same.1018* <p>1019* This definition should result in implementations comparing their entire state.1020*1021* @param otherRules the other rules, null returns false1022* @return true if this rules is the same as that specified1023*/1024@Override1025public boolean equals(Object otherRules) {1026if (this == otherRules) {1027return true;1028}1029return (otherRules instanceof ZoneRules other)1030&& Arrays.equals(standardTransitions, other.standardTransitions)1031&& Arrays.equals(standardOffsets, other.standardOffsets)1032&& Arrays.equals(savingsInstantTransitions, other.savingsInstantTransitions)1033&& Arrays.equals(wallOffsets, other.wallOffsets)1034&& Arrays.equals(lastRules, other.lastRules);1035}10361037/**1038* Returns a suitable hash code given the definition of {@code #equals}.1039*1040* @return the hash code1041*/1042@Override1043public int hashCode() {1044return Arrays.hashCode(standardTransitions) ^1045Arrays.hashCode(standardOffsets) ^1046Arrays.hashCode(savingsInstantTransitions) ^1047Arrays.hashCode(wallOffsets) ^1048Arrays.hashCode(lastRules);1049}10501051/**1052* Returns a string describing this object.1053*1054* @return a string for debugging, not null1055*/1056@Override1057public String toString() {1058return "ZoneRules[currentStandardOffset=" + standardOffsets[standardOffsets.length - 1] + "]";1059}10601061}106210631064