001/*
002 * Copyright (c) 2007-2013, Stephen Colebourne & Michael Nascimento Santos
003 *
004 * All rights reserved.
005 *
006 * Redistribution and use in source and binary forms, with or without
007 * modification, are permitted provided that the following conditions are met:
008 *
009 *  * Redistributions of source code must retain the above copyright notice,
010 *    this list of conditions and the following disclaimer.
011 *
012 *  * Redistributions in binary form must reproduce the above copyright notice,
013 *    this list of conditions and the following disclaimer in the documentation
014 *    and/or other materials provided with the distribution.
015 *
016 *  * Neither the name of JSR-310 nor the names of its contributors
017 *    may be used to endorse or promote products derived from this software
018 *    without specific prior written permission.
019 *
020 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
021 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
022 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
023 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
024 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
025 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
026 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
027 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
028 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
029 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
030 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
031 */
032package org.threeten.bp.zone;
033
034import java.io.DataInput;
035import java.io.DataOutput;
036import java.io.IOException;
037import java.io.Serializable;
038import java.util.Arrays;
039import java.util.Collections;
040import java.util.List;
041import java.util.Objects;
042
043import org.threeten.bp.Duration;
044import org.threeten.bp.Instant;
045import org.threeten.bp.LocalDateTime;
046import org.threeten.bp.ZoneOffset;
047
048/**
049 * A transition between two offsets caused by a discontinuity in the local time-line.
050 * <p>
051 * A transition between two offsets is normally the result of a daylight savings cutover.
052 * The discontinuity is normally a gap in spring and an overlap in autumn.
053 * {@code ZoneOffsetTransition} models the transition between the two offsets.
054 * <p>
055 * Gaps occur where there are local date-times that simply do not not exist.
056 * An example would be when the offset changes from {@code +03:00} to {@code +04:00}.
057 * This might be described as 'the clocks will move forward one hour tonight at 1am'.
058 * <p>
059 * Overlaps occur where there are local date-times that exist twice.
060 * An example would be when the offset changes from {@code +04:00} to {@code +03:00}.
061 * This might be described as 'the clocks will move back one hour tonight at 2am'.
062 *
063 * <h3>Specification for implementors</h3>
064 * This class is immutable and thread-safe.
065 */
066public final class ZoneOffsetTransition
067        implements Comparable<ZoneOffsetTransition>, Serializable {
068
069    /**
070     * Serialization version.
071     */
072    private static final long serialVersionUID = -6946044323557704546L;
073    /**
074     * The local transition date-time at the transition.
075     */
076    private final LocalDateTime transition;
077    /**
078     * The offset before transition.
079     */
080    private final ZoneOffset offsetBefore;
081    /**
082     * The offset after transition.
083     */
084    private final ZoneOffset offsetAfter;
085
086    //-----------------------------------------------------------------------
087    /**
088     * Obtains an instance defining a transition between two offsets.
089     * <p>
090     * Applications should normally obtain an instance from {@link ZoneRules}.
091     * This factory is only intended for use when creating {@link ZoneRules}.
092     *
093     * @param transition  the transition date-time at the transition, which never
094     *  actually occurs, expressed local to the before offset, not null
095     * @param offsetBefore  the offset before the transition, not null
096     * @param offsetAfter  the offset at and after the transition, not null
097     * @return the transition, not null
098     * @throws IllegalArgumentException if {@code offsetBefore} and {@code offsetAfter}
099     *         are equal, or {@code transition.getNano()} returns non-zero value
100     */
101    public static ZoneOffsetTransition of(LocalDateTime transition, ZoneOffset offsetBefore, ZoneOffset offsetAfter) {
102        Objects.requireNonNull(transition, "transition");
103        Objects.requireNonNull(offsetBefore, "offsetBefore");
104        Objects.requireNonNull(offsetAfter, "offsetAfter");
105        if (offsetBefore.equals(offsetAfter)) {
106            throw new IllegalArgumentException("Offsets must not be equal");
107        }
108        if (transition.getNano() != 0) {
109            throw new IllegalArgumentException("Nano-of-second must be zero");
110        }
111        return new ZoneOffsetTransition(transition, offsetBefore, offsetAfter);
112    }
113
114    /**
115     * Creates an instance defining a transition between two offsets.
116     *
117     * @param transition  the transition date-time with the offset before the transition, not null
118     * @param offsetBefore  the offset before the transition, not null
119     * @param offsetAfter  the offset at and after the transition, not null
120     */
121    ZoneOffsetTransition(LocalDateTime transition, ZoneOffset offsetBefore, ZoneOffset offsetAfter) {
122        this.transition = transition;
123        this.offsetBefore = offsetBefore;
124        this.offsetAfter = offsetAfter;
125    }
126
127    /**
128     * Creates an instance from epoch-second and offsets.
129     *
130     * @param epochSecond  the transition epoch-second
131     * @param offsetBefore  the offset before the transition, not null
132     * @param offsetAfter  the offset at and after the transition, not null
133     */
134    ZoneOffsetTransition(long epochSecond, ZoneOffset offsetBefore, ZoneOffset offsetAfter) {
135        this.transition = LocalDateTime.ofEpochSecond(epochSecond, 0, offsetBefore);
136        this.offsetBefore = offsetBefore;
137        this.offsetAfter = offsetAfter;
138    }
139
140    //-----------------------------------------------------------------------
141    /**
142     * Uses a serialization delegate.
143     *
144     * @return the replacing object, not null
145     */
146    private Object writeReplace() {
147        return new Ser(Ser.ZOT, this);
148    }
149
150    /**
151     * Writes the state to the stream.
152     *
153     * @param out  the output stream, not null
154     * @throws IOException if an error occurs
155     */
156    void writeExternal(DataOutput out) throws IOException {
157        Ser.writeEpochSec(toEpochSecond(), out);
158        Ser.writeOffset(offsetBefore, out);
159        Ser.writeOffset(offsetAfter, out);
160    }
161
162    /**
163     * Reads the state from the stream.
164     *
165     * @param in  the input stream, not null
166     * @return the created object, not null
167     * @throws IOException if an error occurs
168     */
169    static ZoneOffsetTransition readExternal(DataInput in) throws IOException {
170        long epochSecond = Ser.readEpochSec(in);
171        ZoneOffset before = Ser.readOffset(in);
172        ZoneOffset after = Ser.readOffset(in);
173        if (before.equals(after)) {
174            throw new IllegalArgumentException("Offsets must not be equal");
175        }
176        return new ZoneOffsetTransition(epochSecond, before, after);
177    }
178
179    //-----------------------------------------------------------------------
180    /**
181     * Gets the transition instant.
182     * <p>
183     * This is the instant of the discontinuity, which is defined as the first
184     * instant that the 'after' offset applies.
185     * <p>
186     * The methods {@link #getInstant()}, {@link #getDateTimeBefore()} and {@link #getDateTimeAfter()}
187     * all represent the same instant.
188     *
189     * @return the transition instant, not null
190     */
191    public Instant getInstant() {
192        return transition.toInstant(offsetBefore);
193    }
194
195    /**
196     * Gets the transition instant as an epoch second.
197     *
198     * @return the transition epoch second
199     */
200    public long toEpochSecond() {
201        return transition.toEpochSecond(offsetBefore);
202    }
203
204    //-------------------------------------------------------------------------
205    /**
206     * Gets the local transition date-time, as would be expressed with the 'before' offset.
207     * <p>
208     * This is the date-time where the discontinuity begins expressed with the 'before' offset.
209     * At this instant, the 'after' offset is actually used, therefore the combination of this
210     * date-time and the 'before' offset will never occur.
211     * <p>
212     * The combination of the 'before' date-time and offset represents the same instant
213     * as the 'after' date-time and offset.
214     *
215     * @return the transition date-time expressed with the before offset, not null
216     */
217    public LocalDateTime getDateTimeBefore() {
218        return transition;
219    }
220
221    /**
222     * Gets the local transition date-time, as would be expressed with the 'after' offset.
223     * <p>
224     * This is the first date-time after the discontinuity, when the new offset applies.
225     * <p>
226     * The combination of the 'before' date-time and offset represents the same instant
227     * as the 'after' date-time and offset.
228     *
229     * @return the transition date-time expressed with the after offset, not null
230     */
231    public LocalDateTime getDateTimeAfter() {
232        return transition.plusSeconds(getDurationSeconds());
233    }
234
235    /**
236     * Gets the offset before the transition.
237     * <p>
238     * This is the offset in use before the instant of the transition.
239     *
240     * @return the offset before the transition, not null
241     */
242    public ZoneOffset getOffsetBefore() {
243        return offsetBefore;
244    }
245
246    /**
247     * Gets the offset after the transition.
248     * <p>
249     * This is the offset in use on and after the instant of the transition.
250     *
251     * @return the offset after the transition, not null
252     */
253    public ZoneOffset getOffsetAfter() {
254        return offsetAfter;
255    }
256
257    /**
258     * Gets the duration of the transition.
259     * <p>
260     * In most cases, the transition duration is one hour, however this is not always the case.
261     * The duration will be positive for a gap and negative for an overlap.
262     * Time-zones are second-based, so the nanosecond part of the duration will be zero.
263     *
264     * @return the duration of the transition, positive for gaps, negative for overlaps
265     */
266    public Duration getDuration() {
267        return Duration.ofSeconds(getDurationSeconds());
268    }
269
270    /**
271     * Gets the duration of the transition in seconds.
272     *
273     * @return the duration in seconds
274     */
275    private int getDurationSeconds() {
276        return getOffsetAfter().getTotalSeconds() - getOffsetBefore().getTotalSeconds();
277    }
278
279    /**
280     * Does this transition represent a gap in the local time-line.
281     * <p>
282     * Gaps occur where there are local date-times that simply do not not exist.
283     * An example would be when the offset changes from {@code +01:00} to {@code +02:00}.
284     * This might be described as 'the clocks will move forward one hour tonight at 1am'.
285     *
286     * @return true if this transition is a gap, false if it is an overlap
287     */
288    public boolean isGap() {
289        return getOffsetAfter().getTotalSeconds() > getOffsetBefore().getTotalSeconds();
290    }
291
292    /**
293     * Does this transition represent a gap in the local time-line.
294     * <p>
295     * Overlaps occur where there are local date-times that exist twice.
296     * An example would be when the offset changes from {@code +02:00} to {@code +01:00}.
297     * This might be described as 'the clocks will move back one hour tonight at 2am'.
298     *
299     * @return true if this transition is an overlap, false if it is a gap
300     */
301    public boolean isOverlap() {
302        return getOffsetAfter().getTotalSeconds() < getOffsetBefore().getTotalSeconds();
303    }
304
305    /**
306     * Checks if the specified offset is valid during this transition.
307     * <p>
308     * This checks to see if the given offset will be valid at some point in the transition.
309     * A gap will always return false.
310     * An overlap will return true if the offset is either the before or after offset.
311     *
312     * @param offset  the offset to check, null returns false
313     * @return true if the offset is valid during the transition
314     */
315    public boolean isValidOffset(ZoneOffset offset) {
316        return isGap() ? false : (getOffsetBefore().equals(offset) || getOffsetAfter().equals(offset));
317    }
318
319    /**
320     * Gets the valid offsets during this transition.
321     * <p>
322     * A gap will return an empty list, while an overlap will return both offsets.
323     *
324     * @return the list of valid offsets
325     */
326    List<ZoneOffset> getValidOffsets() {
327        if (isGap()) {
328            return Collections.emptyList();
329        }
330        return Arrays.asList(getOffsetBefore(), getOffsetAfter());
331    }
332
333    //-----------------------------------------------------------------------
334    /**
335     * Compares this transition to another based on the transition instant.
336     * <p>
337     * This compares the instants of each transition.
338     * The offsets are ignored, making this order inconsistent with equals.
339     *
340     * @param transition  the transition to compare to, not null
341     * @return the comparator value, negative if less, positive if greater
342     */
343    @Override
344    public int compareTo(ZoneOffsetTransition transition) {
345        return this.getInstant().compareTo(transition.getInstant());
346    }
347
348    //-----------------------------------------------------------------------
349    /**
350     * Checks if this object equals another.
351     * <p>
352     * The entire state of the object is compared.
353     *
354     * @param other  the other object to compare to, null returns false
355     * @return true if equal
356     */
357    @Override
358    public boolean equals(Object other) {
359        if (other == this) {
360            return true;
361        }
362        if (other instanceof ZoneOffsetTransition) {
363            ZoneOffsetTransition d = (ZoneOffsetTransition) other;
364            return transition.equals(d.transition) &&
365                offsetBefore.equals(d.offsetBefore) && offsetAfter.equals(d.offsetAfter);
366        }
367        return false;
368    }
369
370    /**
371     * Returns a suitable hash code.
372     *
373     * @return the hash code
374     */
375    @Override
376    public int hashCode() {
377        return transition.hashCode() ^ offsetBefore.hashCode() ^ Integer.rotateLeft(offsetAfter.hashCode(), 16);
378    }
379
380    //-----------------------------------------------------------------------
381    /**
382     * Returns a string describing this object.
383     *
384     * @return a string for debugging, not null
385     */
386    @Override
387    public String toString() {
388        StringBuilder buf = new StringBuilder();
389        buf.append("Transition[")
390            .append(isGap() ? "Gap" : "Overlap")
391            .append(" at ")
392            .append(transition)
393            .append(offsetBefore)
394            .append(" to ")
395            .append(offsetAfter)
396            .append(']');
397        return buf.toString();
398    }
399
400}