jdk/src/share/classes/java/time/zone/ZoneRules.java
changeset 15289 3ac550392e43
child 15658 55b829ca2334
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/jdk/src/share/classes/java/time/zone/ZoneRules.java	Tue Jan 22 20:59:21 2013 -0800
@@ -0,0 +1,959 @@
+/*
+ * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved.
+ * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
+ *
+ * This code is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 2 only, as
+ * published by the Free Software Foundation.  Oracle designates this
+ * particular file as subject to the "Classpath" exception as provided
+ * by Oracle in the LICENSE file that accompanied this code.
+ *
+ * This code is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * version 2 for more details (a copy is included in the LICENSE file that
+ * accompanied this code).
+ *
+ * You should have received a copy of the GNU General Public License version
+ * 2 along with this work; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
+ *
+ * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
+ * or visit www.oracle.com if you need additional information or have any
+ * questions.
+ */
+
+/*
+ * This file is available under and governed by the GNU General Public
+ * License version 2 only, as published by the Free Software Foundation.
+ * However, the following notice accompanied the original version of this
+ * file:
+ *
+ * Copyright (c) 2009-2012, Stephen Colebourne & Michael Nascimento Santos
+ *
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ *  * Redistributions of source code must retain the above copyright notice,
+ *    this list of conditions and the following disclaimer.
+ *
+ *  * Redistributions in binary form must reproduce the above copyright notice,
+ *    this list of conditions and the following disclaimer in the documentation
+ *    and/or other materials provided with the distribution.
+ *
+ *  * Neither the name of JSR-310 nor the names of its contributors
+ *    may be used to endorse or promote products derived from this software
+ *    without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
+ * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
+ * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
+ * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
+ * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
+ * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+ * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package java.time.zone;
+
+import java.io.DataInput;
+import java.io.DataOutput;
+import java.io.IOException;
+import java.io.Serializable;
+import java.time.Duration;
+import java.time.Instant;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.ZoneOffset;
+import java.time.temporal.Year;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * The rules defining how the zone offset varies for a single time-zone.
+ * <p>
+ * The rules model all the historic and future transitions for a time-zone.
+ * {@link ZoneOffsetTransition} is used for known transitions, typically historic.
+ * {@link ZoneOffsetTransitionRule} is used for future transitions that are based
+ * on the result of an algorithm.
+ * <p>
+ * The rules are loaded via {@link ZoneRulesProvider} using a {@link ZoneId}.
+ * The same rules may be shared internally between multiple zone IDs.
+ * <p>
+ * Serializing an instance of {@code ZoneRules} will store the entire set of rules.
+ * It does not store the zone ID as it is not part of the state of this object.
+ * <p>
+ * A rule implementation may or may not store full information about historic
+ * and future transitions, and the information stored is only as accurate as
+ * that supplied to the implementation by the rules provider.
+ * Applications should treat the data provided as representing the best information
+ * available to the implementation of this rule.
+ *
+ * <h3>Specification for implementors</h3>
+ * This class is immutable and thread-safe.
+ *
+ * @since 1.8
+ */
+public final class ZoneRules implements Serializable {
+
+    /**
+     * Serialization version.
+     */
+    private static final long serialVersionUID = 3044319355680032515L;
+    /**
+     * The last year to have its transitions cached.
+     */
+    private static final int LAST_CACHED_YEAR = 2100;
+
+    /**
+     * The transitions between standard offsets (epoch seconds), sorted.
+     */
+    private final long[] standardTransitions;
+    /**
+     * The standard offsets.
+     */
+    private final ZoneOffset[] standardOffsets;
+    /**
+     * The transitions between instants (epoch seconds), sorted.
+     */
+    private final long[] savingsInstantTransitions;
+    /**
+     * The transitions between local date-times, sorted.
+     * This is a paired array, where the first entry is the start of the transition
+     * and the second entry is the end of the transition.
+     */
+    private final LocalDateTime[] savingsLocalTransitions;
+    /**
+     * The wall offsets.
+     */
+    private final ZoneOffset[] wallOffsets;
+    /**
+     * The last rule.
+     */
+    private final ZoneOffsetTransitionRule[] lastRules;
+    /**
+     * The map of recent transitions.
+     */
+    private final ConcurrentMap<Integer, ZoneOffsetTransition[]> lastRulesCache =
+                new ConcurrentHashMap<Integer, ZoneOffsetTransition[]>();
+    /**
+     * The zero-length long array.
+     */
+    private static final long[] EMPTY_LONG_ARRAY = new long[0];
+    /**
+     * The zero-length lastrules array.
+     */
+    private static final ZoneOffsetTransitionRule[] EMPTY_LASTRULES =
+        new ZoneOffsetTransitionRule[0];
+    /**
+     * The zero-length ldt array.
+     */
+    private static final LocalDateTime[] EMPTY_LDT_ARRAY = new LocalDateTime[0];
+
+    /**
+     * Obtains an instance of a ZoneRules.
+     *
+     * @param baseStandardOffset  the standard offset to use before legal rules were set, not null
+     * @param baseWallOffset  the wall offset to use before legal rules were set, not null
+     * @param standardOffsetTransitionList  the list of changes to the standard offset, not null
+     * @param transitionList  the list of transitions, not null
+     * @param lastRules  the recurring last rules, size 16 or less, not null
+     * @return the zone rules, not null
+     */
+    public static ZoneRules of(ZoneOffset baseStandardOffset,
+                               ZoneOffset baseWallOffset,
+                               List<ZoneOffsetTransition> standardOffsetTransitionList,
+                               List<ZoneOffsetTransition> transitionList,
+                               List<ZoneOffsetTransitionRule> lastRules) {
+        Objects.requireNonNull(baseStandardOffset, "baseStandardOffset");
+        Objects.requireNonNull(baseWallOffset, "baseWallOffset");
+        Objects.requireNonNull(standardOffsetTransitionList, "standardOffsetTransitionList");
+        Objects.requireNonNull(transitionList, "transitionList");
+        Objects.requireNonNull(lastRules, "lastRules");
+        return new ZoneRules(baseStandardOffset, baseWallOffset,
+                             standardOffsetTransitionList, transitionList, lastRules);
+    }
+
+    /**
+     * Obtains an instance of ZoneRules that has fixed zone rules.
+     *
+     * @param offset  the offset this fixed zone rules is based on, not null
+     * @return the zone rules, not null
+     * @see #isFixedOffset()
+     */
+    public static ZoneRules of(ZoneOffset offset) {
+        Objects.requireNonNull(offset, "offset");
+        return new ZoneRules(offset);
+    }
+
+    /**
+     * Creates an instance.
+     *
+     * @param baseStandardOffset  the standard offset to use before legal rules were set, not null
+     * @param baseWallOffset  the wall offset to use before legal rules were set, not null
+     * @param standardOffsetTransitionList  the list of changes to the standard offset, not null
+     * @param transitionList  the list of transitions, not null
+     * @param lastRules  the recurring last rules, size 16 or less, not null
+     */
+    ZoneRules(ZoneOffset baseStandardOffset,
+              ZoneOffset baseWallOffset,
+              List<ZoneOffsetTransition> standardOffsetTransitionList,
+              List<ZoneOffsetTransition> transitionList,
+              List<ZoneOffsetTransitionRule> lastRules) {
+        super();
+
+        // convert standard transitions
+
+        this.standardTransitions = new long[standardOffsetTransitionList.size()];
+
+        this.standardOffsets = new ZoneOffset[standardOffsetTransitionList.size() + 1];
+        this.standardOffsets[0] = baseStandardOffset;
+        for (int i = 0; i < standardOffsetTransitionList.size(); i++) {
+            this.standardTransitions[i] = standardOffsetTransitionList.get(i).toEpochSecond();
+            this.standardOffsets[i + 1] = standardOffsetTransitionList.get(i).getOffsetAfter();
+        }
+
+        // convert savings transitions to locals
+        List<LocalDateTime> localTransitionList = new ArrayList<>();
+        List<ZoneOffset> localTransitionOffsetList = new ArrayList<>();
+        localTransitionOffsetList.add(baseWallOffset);
+        for (ZoneOffsetTransition trans : transitionList) {
+            if (trans.isGap()) {
+                localTransitionList.add(trans.getDateTimeBefore());
+                localTransitionList.add(trans.getDateTimeAfter());
+            } else {
+                localTransitionList.add(trans.getDateTimeAfter());
+                localTransitionList.add(trans.getDateTimeBefore());
+            }
+            localTransitionOffsetList.add(trans.getOffsetAfter());
+        }
+        this.savingsLocalTransitions = localTransitionList.toArray(new LocalDateTime[localTransitionList.size()]);
+        this.wallOffsets = localTransitionOffsetList.toArray(new ZoneOffset[localTransitionOffsetList.size()]);
+
+        // convert savings transitions to instants
+        this.savingsInstantTransitions = new long[transitionList.size()];
+        for (int i = 0; i < transitionList.size(); i++) {
+            this.savingsInstantTransitions[i] = transitionList.get(i).toEpochSecond();
+        }
+
+        // last rules
+        if (lastRules.size() > 16) {
+            throw new IllegalArgumentException("Too many transition rules");
+        }
+        this.lastRules = lastRules.toArray(new ZoneOffsetTransitionRule[lastRules.size()]);
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param standardTransitions  the standard transitions, not null
+     * @param standardOffsets  the standard offsets, not null
+     * @param savingsInstantTransitions  the standard transitions, not null
+     * @param wallOffsets  the wall offsets, not null
+     * @param lastRules  the recurring last rules, size 15 or less, not null
+     */
+    private ZoneRules(long[] standardTransitions,
+                      ZoneOffset[] standardOffsets,
+                      long[] savingsInstantTransitions,
+                      ZoneOffset[] wallOffsets,
+                      ZoneOffsetTransitionRule[] lastRules) {
+        super();
+
+        this.standardTransitions = standardTransitions;
+        this.standardOffsets = standardOffsets;
+        this.savingsInstantTransitions = savingsInstantTransitions;
+        this.wallOffsets = wallOffsets;
+        this.lastRules = lastRules;
+
+        if (savingsInstantTransitions.length == 0) {
+            this.savingsLocalTransitions = EMPTY_LDT_ARRAY;
+        } else {
+            // convert savings transitions to locals
+            List<LocalDateTime> localTransitionList = new ArrayList<>();
+            for (int i = 0; i < savingsInstantTransitions.length; i++) {
+                ZoneOffset before = wallOffsets[i];
+                ZoneOffset after = wallOffsets[i + 1];
+                ZoneOffsetTransition trans = new ZoneOffsetTransition(savingsInstantTransitions[i], before, after);
+                if (trans.isGap()) {
+                    localTransitionList.add(trans.getDateTimeBefore());
+                    localTransitionList.add(trans.getDateTimeAfter());
+                } else {
+                    localTransitionList.add(trans.getDateTimeAfter());
+                    localTransitionList.add(trans.getDateTimeBefore());
+               }
+            }
+            this.savingsLocalTransitions = localTransitionList.toArray(new LocalDateTime[localTransitionList.size()]);
+        }
+    }
+
+    /**
+     * Creates an instance of ZoneRules that has fixed zone rules.
+     *
+     * @param offset  the offset this fixed zone rules is based on, not null
+     * @return the zone rules, not null
+     * @see #isFixedOffset()
+     */
+    private ZoneRules(ZoneOffset offset) {
+        this.standardOffsets = new ZoneOffset[1];
+        this.standardOffsets[0] = offset;
+        this.standardTransitions = EMPTY_LONG_ARRAY;
+        this.savingsInstantTransitions = EMPTY_LONG_ARRAY;
+        this.savingsLocalTransitions = EMPTY_LDT_ARRAY;
+        this.wallOffsets = standardOffsets;
+        this.lastRules = EMPTY_LASTRULES;
+    }
+
+    /**
+     * Uses a serialization delegate.
+     *
+     * @return the replacing object, not null
+     */
+    private Object writeReplace() {
+        return new Ser(Ser.ZRULES, this);
+    }
+
+    /**
+     * Writes the state to the stream.
+     *
+     * @param out  the output stream, not null
+     * @throws IOException if an error occurs
+     */
+    void writeExternal(DataOutput out) throws IOException {
+        out.writeInt(standardTransitions.length);
+        for (long trans : standardTransitions) {
+            Ser.writeEpochSec(trans, out);
+        }
+        for (ZoneOffset offset : standardOffsets) {
+            Ser.writeOffset(offset, out);
+        }
+        out.writeInt(savingsInstantTransitions.length);
+        for (long trans : savingsInstantTransitions) {
+            Ser.writeEpochSec(trans, out);
+        }
+        for (ZoneOffset offset : wallOffsets) {
+            Ser.writeOffset(offset, out);
+        }
+        out.writeByte(lastRules.length);
+        for (ZoneOffsetTransitionRule rule : lastRules) {
+            rule.writeExternal(out);
+        }
+    }
+
+    /**
+     * Reads the state from the stream.
+     *
+     * @param in  the input stream, not null
+     * @return the created object, not null
+     * @throws IOException if an error occurs
+     */
+    static ZoneRules readExternal(DataInput in) throws IOException, ClassNotFoundException {
+        int stdSize = in.readInt();
+        long[] stdTrans = (stdSize == 0) ? EMPTY_LONG_ARRAY
+                                         : new long[stdSize];
+        for (int i = 0; i < stdSize; i++) {
+            stdTrans[i] = Ser.readEpochSec(in);
+        }
+        ZoneOffset[] stdOffsets = new ZoneOffset[stdSize + 1];
+        for (int i = 0; i < stdOffsets.length; i++) {
+            stdOffsets[i] = Ser.readOffset(in);
+        }
+        int savSize = in.readInt();
+        long[] savTrans = (savSize == 0) ? EMPTY_LONG_ARRAY
+                                         : new long[savSize];
+        for (int i = 0; i < savSize; i++) {
+            savTrans[i] = Ser.readEpochSec(in);
+        }
+        ZoneOffset[] savOffsets = new ZoneOffset[savSize + 1];
+        for (int i = 0; i < savOffsets.length; i++) {
+            savOffsets[i] = Ser.readOffset(in);
+        }
+        int ruleSize = in.readByte();
+        ZoneOffsetTransitionRule[] rules = (ruleSize == 0) ?
+            EMPTY_LASTRULES : new ZoneOffsetTransitionRule[ruleSize];
+        for (int i = 0; i < ruleSize; i++) {
+            rules[i] = ZoneOffsetTransitionRule.readExternal(in);
+        }
+        return new ZoneRules(stdTrans, stdOffsets, savTrans, savOffsets, rules);
+    }
+
+    /**
+     * Checks of the zone rules are fixed, such that the offset never varies.
+     *
+     * @return true if the time-zone is fixed and the offset never changes
+     */
+    public boolean isFixedOffset() {
+        return savingsInstantTransitions.length == 0;
+    }
+
+    /**
+     * Gets the offset applicable at the specified instant in these rules.
+     * <p>
+     * The mapping from an instant to an offset is simple, there is only
+     * one valid offset for each instant.
+     * This method returns that offset.
+     *
+     * @param instant  the instant to find the offset for, not null, but null
+     *  may be ignored if the rules have a single offset for all instants
+     * @return the offset, not null
+     */
+    public ZoneOffset getOffset(Instant instant) {
+        if (savingsInstantTransitions.length == 0) {
+            return standardOffsets[0];
+        }
+        long epochSec = instant.getEpochSecond();
+        // check if using last rules
+        if (lastRules.length > 0 &&
+                epochSec > savingsInstantTransitions[savingsInstantTransitions.length - 1]) {
+            int year = findYear(epochSec, wallOffsets[wallOffsets.length - 1]);
+            ZoneOffsetTransition[] transArray = findTransitionArray(year);
+            ZoneOffsetTransition trans = null;
+            for (int i = 0; i < transArray.length; i++) {
+                trans = transArray[i];
+                if (epochSec < trans.toEpochSecond()) {
+                    return trans.getOffsetBefore();
+                }
+            }
+            return trans.getOffsetAfter();
+        }
+
+        // using historic rules
+        int index  = Arrays.binarySearch(savingsInstantTransitions, epochSec);
+        if (index < 0) {
+            // switch negative insert position to start of matched range
+            index = -index - 2;
+        }
+        return wallOffsets[index + 1];
+    }
+
+    /**
+     * Gets a suitable offset for the specified local date-time in these rules.
+     * <p>
+     * The mapping from a local date-time to an offset is not straightforward.
+     * There are three cases:
+     * <p><ul>
+     * <li>Normal, with one valid offset. For the vast majority of the year, the normal
+     *  case applies, where there is a single valid offset for the local date-time.</li>
+     * <li>Gap, with zero valid offsets. This is when clocks jump forward typically
+     *  due to the spring daylight savings change from "winter" to "summer".
+     *  In a gap there are local date-time values with no valid offset.</li>
+     * <li>Overlap, with two valid offsets. This is when clocks are set back typically
+     *  due to the autumn daylight savings change from "summer" to "winter".
+     *  In an overlap there are local date-time values with two valid offsets.</li>
+     * </ul><p>
+     * Thus, for any given local date-time there can be zero, one or two valid offsets.
+     * This method returns the single offset in the Normal case, and in the Gap or Overlap
+     * case it returns the offset before the transition.
+     * <p>
+     * Since, in the case of Gap and Overlap, the offset returned is a "best" value, rather
+     * than the "correct" value, it should be treated with care. Applications that care
+     * about the correct offset should use a combination of this method,
+     * {@link #getValidOffsets(LocalDateTime)} and {@link #getTransition(LocalDateTime)}.
+     *
+     * @param localDateTime  the local date-time to query, not null, but null
+     *  may be ignored if the rules have a single offset for all instants
+     * @return the best available offset for the local date-time, not null
+     */
+    public ZoneOffset getOffset(LocalDateTime localDateTime) {
+        Object info = getOffsetInfo(localDateTime);
+        if (info instanceof ZoneOffsetTransition) {
+            return ((ZoneOffsetTransition) info).getOffsetBefore();
+        }
+        return (ZoneOffset) info;
+    }
+
+    /**
+     * Gets the offset applicable at the specified local date-time in these rules.
+     * <p>
+     * The mapping from a local date-time to an offset is not straightforward.
+     * There are three cases:
+     * <p><ul>
+     * <li>Normal, with one valid offset. For the vast majority of the year, the normal
+     *  case applies, where there is a single valid offset for the local date-time.</li>
+     * <li>Gap, with zero valid offsets. This is when clocks jump forward typically
+     *  due to the spring daylight savings change from "winter" to "summer".
+     *  In a gap there are local date-time values with no valid offset.</li>
+     * <li>Overlap, with two valid offsets. This is when clocks are set back typically
+     *  due to the autumn daylight savings change from "summer" to "winter".
+     *  In an overlap there are local date-time values with two valid offsets.</li>
+     * </ul><p>
+     * Thus, for any given local date-time there can be zero, one or two valid offsets.
+     * This method returns that list of valid offsets, which is a list of size 0, 1 or 2.
+     * In the case where there are two offsets, the earlier offset is returned at index 0
+     * and the later offset at index 1.
+     * <p>
+     * There are various ways to handle the conversion from a {@code LocalDateTime}.
+     * One technique, using this method, would be:
+     * <pre>
+     *  List&lt;ZoneOffset&gt; validOffsets = rules.getOffset(localDT);
+     *  if (validOffsets.size() == 1) {
+     *    // Normal case: only one valid offset
+     *    zoneOffset = validOffsets.get(0);
+     *  } else {
+     *    // Gap or Overlap: determine what to do from transition (which will be non-null)
+     *    ZoneOffsetTransition trans = rules.getTransition(localDT);
+     *  }
+     * </pre>
+     * <p>
+     * In theory, it is possible for there to be more than two valid offsets.
+     * This would happen if clocks to be put back more than once in quick succession.
+     * This has never happened in the history of time-zones and thus has no special handling.
+     * However, if it were to happen, then the list would return more than 2 entries.
+     *
+     * @param localDateTime  the local date-time to query for valid offsets, not null, but null
+     *  may be ignored if the rules have a single offset for all instants
+     * @return the list of valid offsets, may be immutable, not null
+     */
+    public List<ZoneOffset> getValidOffsets(LocalDateTime localDateTime) {
+        // should probably be optimized
+        Object info = getOffsetInfo(localDateTime);
+        if (info instanceof ZoneOffsetTransition) {
+            return ((ZoneOffsetTransition) info).getValidOffsets();
+        }
+        return Collections.singletonList((ZoneOffset) info);
+    }
+
+    /**
+     * Gets the offset transition applicable at the specified local date-time in these rules.
+     * <p>
+     * The mapping from a local date-time to an offset is not straightforward.
+     * There are three cases:
+     * <p><ul>
+     * <li>Normal, with one valid offset. For the vast majority of the year, the normal
+     *  case applies, where there is a single valid offset for the local date-time.</li>
+     * <li>Gap, with zero valid offsets. This is when clocks jump forward typically
+     *  due to the spring daylight savings change from "winter" to "summer".
+     *  In a gap there are local date-time values with no valid offset.</li>
+     * <li>Overlap, with two valid offsets. This is when clocks are set back typically
+     *  due to the autumn daylight savings change from "summer" to "winter".
+     *  In an overlap there are local date-time values with two valid offsets.</li>
+     * </ul><p>
+     * A transition is used to model the cases of a Gap or Overlap.
+     * The Normal case will return null.
+     * <p>
+     * There are various ways to handle the conversion from a {@code LocalDateTime}.
+     * One technique, using this method, would be:
+     * <pre>
+     *  ZoneOffsetTransition trans = rules.getTransition(localDT);
+     *  if (trans == null) {
+     *    // Gap or Overlap: determine what to do from transition
+     *  } else {
+     *    // Normal case: only one valid offset
+     *    zoneOffset = rule.getOffset(localDT);
+     *  }
+     * </pre>
+     *
+     * @param localDateTime  the local date-time to query for offset transition, not null, but null
+     *  may be ignored if the rules have a single offset for all instants
+     * @return the offset transition, null if the local date-time is not in transition
+     */
+    public ZoneOffsetTransition getTransition(LocalDateTime localDateTime) {
+        Object info = getOffsetInfo(localDateTime);
+        return (info instanceof ZoneOffsetTransition ? (ZoneOffsetTransition) info : null);
+    }
+
+    private Object getOffsetInfo(LocalDateTime dt) {
+        if (savingsInstantTransitions.length == 0) {
+            return standardOffsets[0];
+        }
+        // check if using last rules
+        if (lastRules.length > 0 &&
+                dt.isAfter(savingsLocalTransitions[savingsLocalTransitions.length - 1])) {
+            ZoneOffsetTransition[] transArray = findTransitionArray(dt.getYear());
+            Object info = null;
+            for (ZoneOffsetTransition trans : transArray) {
+                info = findOffsetInfo(dt, trans);
+                if (info instanceof ZoneOffsetTransition || info.equals(trans.getOffsetBefore())) {
+                    return info;
+                }
+            }
+            return info;
+        }
+
+        // using historic rules
+        int index  = Arrays.binarySearch(savingsLocalTransitions, dt);
+        if (index == -1) {
+            // before first transition
+            return wallOffsets[0];
+        }
+        if (index < 0) {
+            // switch negative insert position to start of matched range
+            index = -index - 2;
+        } else if (index < savingsLocalTransitions.length - 1 &&
+                savingsLocalTransitions[index].equals(savingsLocalTransitions[index + 1])) {
+            // handle overlap immediately following gap
+            index++;
+        }
+        if ((index & 1) == 0) {
+            // gap or overlap
+            LocalDateTime dtBefore = savingsLocalTransitions[index];
+            LocalDateTime dtAfter = savingsLocalTransitions[index + 1];
+            ZoneOffset offsetBefore = wallOffsets[index / 2];
+            ZoneOffset offsetAfter = wallOffsets[index / 2 + 1];
+            if (offsetAfter.getTotalSeconds() > offsetBefore.getTotalSeconds()) {
+                // gap
+                return new ZoneOffsetTransition(dtBefore, offsetBefore, offsetAfter);
+            } else {
+                // overlap
+                return new ZoneOffsetTransition(dtAfter, offsetBefore, offsetAfter);
+            }
+        } else {
+            // normal (neither gap or overlap)
+            return wallOffsets[index / 2 + 1];
+        }
+    }
+
+    /**
+     * Finds the offset info for a local date-time and transition.
+     *
+     * @param dt  the date-time, not null
+     * @param trans  the transition, not null
+     * @return the offset info, not null
+     */
+    private Object findOffsetInfo(LocalDateTime dt, ZoneOffsetTransition trans) {
+        LocalDateTime localTransition = trans.getDateTimeBefore();
+        if (trans.isGap()) {
+            if (dt.isBefore(localTransition)) {
+                return trans.getOffsetBefore();
+            }
+            if (dt.isBefore(trans.getDateTimeAfter())) {
+                return trans;
+            } else {
+                return trans.getOffsetAfter();
+            }
+        } else {
+            if (dt.isBefore(localTransition) == false) {
+                return trans.getOffsetAfter();
+            }
+            if (dt.isBefore(trans.getDateTimeAfter())) {
+                return trans.getOffsetBefore();
+            } else {
+                return trans;
+            }
+        }
+    }
+
+    /**
+     * Finds the appropriate transition array for the given year.
+     *
+     * @param year  the year, not null
+     * @return the transition array, not null
+     */
+    private ZoneOffsetTransition[] findTransitionArray(int year) {
+        Integer yearObj = year;  // should use Year class, but this saves a class load
+        ZoneOffsetTransition[] transArray = lastRulesCache.get(yearObj);
+        if (transArray != null) {
+            return transArray;
+        }
+        ZoneOffsetTransitionRule[] ruleArray = lastRules;
+        transArray  = new ZoneOffsetTransition[ruleArray.length];
+        for (int i = 0; i < ruleArray.length; i++) {
+            transArray[i] = ruleArray[i].createTransition(year);
+        }
+        if (year < LAST_CACHED_YEAR) {
+            lastRulesCache.putIfAbsent(yearObj, transArray);
+        }
+        return transArray;
+    }
+
+    /**
+     * Gets the standard offset for the specified instant in this zone.
+     * <p>
+     * This provides access to historic information on how the standard offset
+     * has changed over time.
+     * The standard offset is the offset before any daylight saving time is applied.
+     * This is typically the offset applicable during winter.
+     *
+     * @param instant  the instant to find the offset information for, not null, but null
+     *  may be ignored if the rules have a single offset for all instants
+     * @return the standard offset, not null
+     */
+    public ZoneOffset getStandardOffset(Instant instant) {
+        if (savingsInstantTransitions.length == 0) {
+            return standardOffsets[0];
+        }
+        long epochSec = instant.getEpochSecond();
+        int index  = Arrays.binarySearch(standardTransitions, epochSec);
+        if (index < 0) {
+            // switch negative insert position to start of matched range
+            index = -index - 2;
+        }
+        return standardOffsets[index + 1];
+    }
+
+    /**
+     * Gets the amount of daylight savings in use for the specified instant in this zone.
+     * <p>
+     * This provides access to historic information on how the amount of daylight
+     * savings has changed over time.
+     * This is the difference between the standard offset and the actual offset.
+     * Typically the amount is zero during winter and one hour during summer.
+     * Time-zones are second-based, so the nanosecond part of the duration will be zero.
+     * <p>
+     * This default implementation calculates the duration from the
+     * {@link #getOffset(java.time.Instant) actual} and
+     * {@link #getStandardOffset(java.time.Instant) standard} offsets.
+     *
+     * @param instant  the instant to find the daylight savings for, not null, but null
+     *  may be ignored if the rules have a single offset for all instants
+     * @return the difference between the standard and actual offset, not null
+     */
+    public Duration getDaylightSavings(Instant instant) {
+        if (savingsInstantTransitions.length == 0) {
+            return Duration.ZERO;
+        }
+        ZoneOffset standardOffset = getStandardOffset(instant);
+        ZoneOffset actualOffset = getOffset(instant);
+        return Duration.ofSeconds(actualOffset.getTotalSeconds() - standardOffset.getTotalSeconds());
+    }
+
+    /**
+     * Checks if the specified instant is in daylight savings.
+     * <p>
+     * This checks if the standard offset and the actual offset are the same
+     * for the specified instant.
+     * If they are not, it is assumed that daylight savings is in operation.
+     * <p>
+     * This default implementation compares the {@link #getOffset(java.time.Instant) actual}
+     * and {@link #getStandardOffset(java.time.Instant) standard} offsets.
+     *
+     * @param instant  the instant to find the offset information for, not null, but null
+     *  may be ignored if the rules have a single offset for all instants
+     * @return the standard offset, not null
+     */
+    public boolean isDaylightSavings(Instant instant) {
+        return (getStandardOffset(instant).equals(getOffset(instant)) == false);
+    }
+
+    /**
+     * Checks if the offset date-time is valid for these rules.
+     * <p>
+     * To be valid, the local date-time must not be in a gap and the offset
+     * must match one of the valid offsets.
+     * <p>
+     * This default implementation checks if {@link #getValidOffsets(java.time.LocalDateTime)}
+     * contains the specified offset.
+     *
+     * @param localDateTime  the date-time to check, not null, but null
+     *  may be ignored if the rules have a single offset for all instants
+     * @param offset  the offset to check, null returns false
+     * @return true if the offset date-time is valid for these rules
+     */
+    public boolean isValidOffset(LocalDateTime localDateTime, ZoneOffset offset) {
+        return getValidOffsets(localDateTime).contains(offset);
+    }
+
+    /**
+     * Gets the next transition after the specified instant.
+     * <p>
+     * This returns details of the next transition after the specified instant.
+     * For example, if the instant represents a point where "Summer" daylight savings time
+     * applies, then the method will return the transition to the next "Winter" time.
+     *
+     * @param instant  the instant to get the next transition after, not null, but null
+     *  may be ignored if the rules have a single offset for all instants
+     * @return the next transition after the specified instant, null if this is after the last transition
+     */
+    public ZoneOffsetTransition nextTransition(Instant instant) {
+        if (savingsInstantTransitions.length == 0) {
+            return null;
+        }
+        long epochSec = instant.getEpochSecond();
+        // check if using last rules
+        if (epochSec >= savingsInstantTransitions[savingsInstantTransitions.length - 1]) {
+            if (lastRules.length == 0) {
+                return null;
+            }
+            // search year the instant is in
+            int year = findYear(epochSec, wallOffsets[wallOffsets.length - 1]);
+            ZoneOffsetTransition[] transArray = findTransitionArray(year);
+            for (ZoneOffsetTransition trans : transArray) {
+                if (epochSec < trans.toEpochSecond()) {
+                    return trans;
+                }
+            }
+            // use first from following year
+            if (year < Year.MAX_VALUE) {
+                transArray = findTransitionArray(year + 1);
+                return transArray[0];
+            }
+            return null;
+        }
+
+        // using historic rules
+        int index  = Arrays.binarySearch(savingsInstantTransitions, epochSec);
+        if (index < 0) {
+            index = -index - 1;  // switched value is the next transition
+        } else {
+            index += 1;  // exact match, so need to add one to get the next
+        }
+        return new ZoneOffsetTransition(savingsInstantTransitions[index], wallOffsets[index], wallOffsets[index + 1]);
+    }
+
+    /**
+     * Gets the previous transition before the specified instant.
+     * <p>
+     * This returns details of the previous transition after the specified instant.
+     * For example, if the instant represents a point where "summer" daylight saving time
+     * applies, then the method will return the transition from the previous "winter" time.
+     *
+     * @param instant  the instant to get the previous transition after, not null, but null
+     *  may be ignored if the rules have a single offset for all instants
+     * @return the previous transition after the specified instant, null if this is before the first transition
+     */
+    public ZoneOffsetTransition previousTransition(Instant instant) {
+        if (savingsInstantTransitions.length == 0) {
+            return null;
+        }
+        long epochSec = instant.getEpochSecond();
+        if (instant.getNano() > 0 && epochSec < Long.MAX_VALUE) {
+            epochSec += 1;  // allow rest of method to only use seconds
+        }
+
+        // check if using last rules
+        long lastHistoric = savingsInstantTransitions[savingsInstantTransitions.length - 1];
+        if (lastRules.length > 0 && epochSec > lastHistoric) {
+            // search year the instant is in
+            ZoneOffset lastHistoricOffset = wallOffsets[wallOffsets.length - 1];
+            int year = findYear(epochSec, lastHistoricOffset);
+            ZoneOffsetTransition[] transArray = findTransitionArray(year);
+            for (int i = transArray.length - 1; i >= 0; i--) {
+                if (epochSec > transArray[i].toEpochSecond()) {
+                    return transArray[i];
+                }
+            }
+            // use last from preceeding year
+            int lastHistoricYear = findYear(lastHistoric, lastHistoricOffset);
+            if (--year > lastHistoricYear) {
+                transArray = findTransitionArray(year);
+                return transArray[transArray.length - 1];
+            }
+            // drop through
+        }
+
+        // using historic rules
+        int index  = Arrays.binarySearch(savingsInstantTransitions, epochSec);
+        if (index < 0) {
+            index = -index - 1;
+        }
+        if (index <= 0) {
+            return null;
+        }
+        return new ZoneOffsetTransition(savingsInstantTransitions[index - 1], wallOffsets[index - 1], wallOffsets[index]);
+    }
+
+    private int findYear(long epochSecond, ZoneOffset offset) {
+        // inline for performance
+        long localSecond = epochSecond + offset.getTotalSeconds();
+        long localEpochDay = Math.floorDiv(localSecond, 86400);
+        return LocalDate.ofEpochDay(localEpochDay).getYear();
+    }
+
+    /**
+     * Gets the complete list of fully defined transitions.
+     * <p>
+     * The complete set of transitions for this rules instance is defined by this method
+     * and {@link #getTransitionRules()}. This method returns those transitions that have
+     * been fully defined. These are typically historical, but may be in the future.
+     * <p>
+     * The list will be empty for fixed offset rules and for any time-zone where there has
+     * only ever been a single offset. The list will also be empty if the transition rules are unknown.
+     *
+     * @return an immutable list of fully defined transitions, not null
+     */
+    public List<ZoneOffsetTransition> getTransitions() {
+        List<ZoneOffsetTransition> list = new ArrayList<>();
+        for (int i = 0; i < savingsInstantTransitions.length; i++) {
+            list.add(new ZoneOffsetTransition(savingsInstantTransitions[i], wallOffsets[i], wallOffsets[i + 1]));
+        }
+        return Collections.unmodifiableList(list);
+    }
+
+    /**
+     * Gets the list of transition rules for years beyond those defined in the transition list.
+     * <p>
+     * The complete set of transitions for this rules instance is defined by this method
+     * and {@link #getTransitions()}. This method returns instances of {@link ZoneOffsetTransitionRule}
+     * that define an algorithm for when transitions will occur.
+     * <p>
+     * For any given {@code ZoneRules}, this list contains the transition rules for years
+     * beyond those years that have been fully defined. These rules typically refer to future
+     * daylight saving time rule changes.
+     * <p>
+     * If the zone defines daylight savings into the future, then the list will normally
+     * be of size two and hold information about entering and exiting daylight savings.
+     * If the zone does not have daylight savings, or information about future changes
+     * is uncertain, then the list will be empty.
+     * <p>
+     * The list will be empty for fixed offset rules and for any time-zone where there is no
+     * daylight saving time. The list will also be empty if the transition rules are unknown.
+     *
+     * @return an immutable list of transition rules, not null
+     */
+    public List<ZoneOffsetTransitionRule> getTransitionRules() {
+        return Collections.unmodifiableList(Arrays.asList(lastRules));
+    }
+
+    /**
+     * Checks if this set of rules equals another.
+     * <p>
+     * Two rule sets are equal if they will always result in the same output
+     * for any given input instant or local date-time.
+     * Rules from two different groups may return false even if they are in fact the same.
+     * <p>
+     * This definition should result in implementations comparing their entire state.
+     *
+     * @param otherRules  the other rules, null returns false
+     * @return true if this rules is the same as that specified
+     */
+    @Override
+    public boolean equals(Object otherRules) {
+        if (this == otherRules) {
+           return true;
+        }
+        if (otherRules instanceof ZoneRules) {
+            ZoneRules other = (ZoneRules) otherRules;
+            return Arrays.equals(standardTransitions, other.standardTransitions) &&
+                    Arrays.equals(standardOffsets, other.standardOffsets) &&
+                    Arrays.equals(savingsInstantTransitions, other.savingsInstantTransitions) &&
+                    Arrays.equals(wallOffsets, other.wallOffsets) &&
+                    Arrays.equals(lastRules, other.lastRules);
+        }
+        return false;
+    }
+
+    /**
+     * Returns a suitable hash code given the definition of {@code #equals}.
+     *
+     * @return the hash code
+     */
+    @Override
+    public int hashCode() {
+        return Arrays.hashCode(standardTransitions) ^
+                Arrays.hashCode(standardOffsets) ^
+                Arrays.hashCode(savingsInstantTransitions) ^
+                Arrays.hashCode(wallOffsets) ^
+                Arrays.hashCode(lastRules);
+    }
+
+    /**
+     * Returns a string describing this object.
+     *
+     * @return a string for debugging, not null
+     */
+    @Override
+    public String toString() {
+        return "ZoneRules[currentStandardOffset=" + standardOffsets[standardOffsets.length - 1] + "]";
+    }
+
+}