• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos
3  *
4  * All rights reserved.
5  *
6  * Redistribution and use in source and binary forms, with or without
7  * modification, are permitted provided that the following conditions are met:
8  *
9  *  * Redistributions of source code must retain the above copyright notice,
10  *    this list of conditions and the following disclaimer.
11  *
12  *  * Redistributions in binary form must reproduce the above copyright notice,
13  *    this list of conditions and the following disclaimer in the documentation
14  *    and/or other materials provided with the distribution.
15  *
16  *  * Neither the name of JSR-310 nor the names of its contributors
17  *    may be used to endorse or promote products derived from this software
18  *    without specific prior written permission.
19  *
20  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
24  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
25  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
26  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
27  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
29  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31  */
32 package org.threeten.bp.zone;
33 
34 import java.io.DataInput;
35 import java.io.DataOutput;
36 import java.io.IOException;
37 import java.io.Serializable;
38 import java.util.Arrays;
39 import java.util.Collections;
40 import java.util.List;
41 
42 import org.threeten.bp.Duration;
43 import org.threeten.bp.Instant;
44 import org.threeten.bp.LocalDateTime;
45 import org.threeten.bp.ZoneOffset;
46 import org.threeten.bp.jdk8.Jdk8Methods;
47 
48 /**
49  * A transition between two offsets caused by a discontinuity in the local time-line.
50  * <p>
51  * A transition between two offsets is normally the result of a daylight savings cutover.
52  * The discontinuity is normally a gap in spring and an overlap in autumn.
53  * {@code ZoneOffsetTransition} models the transition between the two offsets.
54  * <p>
55  * Gaps occur where there are local date-times that simply do not exist.
56  * An example would be when the offset changes from {@code +03:00} to {@code +04:00}.
57  * This might be described as 'the clocks will move forward one hour tonight at 1am'.
58  * <p>
59  * Overlaps occur where there are local date-times that exist twice.
60  * An example would be when the offset changes from {@code +04:00} to {@code +03:00}.
61  * This might be described as 'the clocks will move back one hour tonight at 2am'.
62  *
63  * <h3>Specification for implementors</h3>
64  * This class is immutable and thread-safe.
65  */
66 public final class ZoneOffsetTransition
67         implements Comparable<ZoneOffsetTransition>, Serializable {
68 
69     /**
70      * Serialization version.
71      */
72     private static final long serialVersionUID = -6946044323557704546L;
73     /**
74      * The local transition date-time at the transition.
75      */
76     private final LocalDateTime transition;
77     /**
78      * The offset before transition.
79      */
80     private final ZoneOffset offsetBefore;
81     /**
82      * The offset after transition.
83      */
84     private final ZoneOffset offsetAfter;
85 
86     //-----------------------------------------------------------------------
87     /**
88      * Obtains an instance defining a transition between two offsets.
89      * <p>
90      * Applications should normally obtain an instance from {@link ZoneRules}.
91      * This factory is only intended for use when creating {@link ZoneRules}.
92      *
93      * @param transition  the transition date-time at the transition, which never
94      *  actually occurs, expressed local to the before offset, not null
95      * @param offsetBefore  the offset before the transition, not null
96      * @param offsetAfter  the offset at and after the transition, not null
97      * @return the transition, not null
98      * @throws IllegalArgumentException if {@code offsetBefore} and {@code offsetAfter}
99      *         are equal, or {@code transition.getNano()} returns non-zero value
100      */
of(LocalDateTime transition, ZoneOffset offsetBefore, ZoneOffset offsetAfter)101     public static ZoneOffsetTransition of(LocalDateTime transition, ZoneOffset offsetBefore, ZoneOffset offsetAfter) {
102         Jdk8Methods.requireNonNull(transition, "transition");
103         Jdk8Methods.requireNonNull(offsetBefore, "offsetBefore");
104         Jdk8Methods.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      */
ZoneOffsetTransition(LocalDateTime transition, ZoneOffset offsetBefore, ZoneOffset offsetAfter)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      */
ZoneOffsetTransition(long epochSecond, ZoneOffset offsetBefore, ZoneOffset offsetAfter)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      */
writeReplace()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      */
writeExternal(DataOutput out)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      */
readExternal(DataInput in)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      */
getInstant()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      */
toEpochSecond()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      */
getDateTimeBefore()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      */
getDateTimeAfter()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      */
getOffsetBefore()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      */
getOffsetAfter()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      */
getDuration()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      */
getDurationSeconds()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 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      */
isGap()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      */
isOverlap()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      */
isValidOffset(ZoneOffset offset)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      */
getValidOffsets()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
compareTo(ZoneOffsetTransition transition)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
equals(Object other)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
hashCode()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
toString()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 }
401