1 /*
2  * hunt-time: A time library for D programming language.
3  *
4  * Copyright (C) 2015-2018 HuntLabs
5  *
6  * Website: https://www.huntlabs.net/
7  *
8  * Licensed under the Apache-2.0 License.
9  *
10  */
11 
12 module hunt.time.zone.ZoneOffsetTransition;
13 
14 import hunt.stream.DataInput;
15 import hunt.stream.DataOutput;
16 import hunt.Exceptions;
17 
18 //import hunt.io.ObjectInputStream;
19 import hunt.stream.Common;
20 import hunt.time.Duration;
21 import hunt.time.Instant;
22 import hunt.time.LocalDateTime;
23 import hunt.time.ZoneOffset;
24 import hunt.time.zone.Ser;
25 import hunt.collection.Collections;
26 import hunt.collection;
27 import hunt.Functions;
28 import hunt.Integer;
29 import hunt.util.Comparator;
30 import hunt.util.Common;
31 import hunt.util.StringBuilder;
32 /**
33  * A transition between two offsets caused by a discontinuity _in the local time-line.
34  * !(p)
35  * A transition between two offsets is normally the result of a daylight savings cutover.
36  * The discontinuity is normally a gap _in spring and an overlap _in autumn.
37  * {@code ZoneOffsetTransition} models the transition between the two offsets.
38  * !(p)
39  * Gaps occur where there are local date-times that simply do not exist.
40  * An example would be when the offset changes from {@code +03:00} to {@code +04:00}.
41  * This might be described as 'the clocks will move forward one hour tonight at 1am'.
42  * !(p)
43  * Overlaps occur where there are local date-times that exist twice.
44  * An example would be when the offset changes from {@code +04:00} to {@code +03:00}.
45  * This might be described as 'the clocks will move back one hour tonight at 2am'.
46  *
47  * @implSpec
48  * This class is immutable and thread-safe.
49  *
50  * @since 1.8
51  */
52 final class ZoneOffsetTransition
53         : Comparable!(ZoneOffsetTransition) { //, Serializable
54 
55     /**
56      * The transition epoch-second.
57      */
58     private  long epochSecond;
59     /**
60      * The local transition date-time at the transition.
61      */
62     private  LocalDateTime transition;
63     /**
64      * The offset before transition.
65      */
66     private  ZoneOffset offsetBefore;
67     /**
68      * The offset after transition.
69      */
70     private  ZoneOffset offsetAfter;
71 
72     //-----------------------------------------------------------------------
73     /**
74      * Obtains an instance defining a transition between two offsets.
75      * !(p)
76      * Applications should normally obtain an instance from {@link ZoneRules}.
77      * This factory is only intended for use when creating {@link ZoneRules}.
78      *
79      * @param transition  the transition date-time at the transition, which never
80      *  actually occurs, expressed local to the before offset, not null
81      * @param offsetBefore  the offset before the transition, not null
82      * @param offsetAfter  the offset at and after the transition, not null
83      * @return the transition, not null
84      * @throws IllegalArgumentException if {@code offsetBefore} and {@code offsetAfter}
85      *         are equal, or {@code transition.getNano()} returns non-zero value
86      */
87     public static ZoneOffsetTransition of(LocalDateTime transition, ZoneOffset offsetBefore, ZoneOffset offsetAfter) {
88         assert(transition, "transition");
89         assert(offsetBefore, "offsetBefore");
90         assert(offsetAfter, "offsetAfter");
91         if (offsetBefore == offsetAfter) {
92             throw new IllegalArgumentException("Offsets must not be equal");
93         }
94         if (transition.getNano() != 0) {
95             throw new IllegalArgumentException("Nano-of-second must be zero");
96         }
97         return new ZoneOffsetTransition(transition, offsetBefore, offsetAfter);
98     }
99 
100     /**
101      * Creates an instance defining a transition between two offsets.
102      *
103      * @param transition  the transition date-time with the offset before the transition, not null
104      * @param offsetBefore  the offset before the transition, not null
105      * @param offsetAfter  the offset at and after the transition, not null
106      */
107     this(LocalDateTime transition, ZoneOffset offsetBefore, ZoneOffset offsetAfter) {
108         assert(transition.getNano() == 0);
109         this.epochSecond = transition.toEpochSecond(offsetBefore);
110         this.transition = transition;
111         this.offsetBefore = offsetBefore;
112         this.offsetAfter = offsetAfter;
113     }
114 
115     /**
116      * Creates an instance from epoch-second and offsets.
117      *
118      * @param epochSecond  the transition epoch-second
119      * @param offsetBefore  the offset before the transition, not null
120      * @param offsetAfter  the offset at and after the transition, not null
121      */
122     this(long epochSecond, ZoneOffset offsetBefore, ZoneOffset offsetAfter) {
123         this.epochSecond = epochSecond;
124         this.transition = LocalDateTime.ofEpochSecond(epochSecond, 0, offsetBefore);
125         this.offsetBefore = offsetBefore;
126         this.offsetAfter = offsetAfter;
127     }
128 
129     //-----------------------------------------------------------------------
130     /**
131      * Defend against malicious streams.
132      *
133      * @param s the stream to read
134      * @throws InvalidObjectException always
135      */
136      ///@gxc
137     // private void readObject(ObjectInputStream s) /*throws InvalidObjectException*/ {
138     //     throw new InvalidObjectException("Deserialization via serialization delegate");
139     // }
140 
141     /**
142      * Writes the object using a
143      * <a href="{@docRoot}/serialized-form.html#hunt.time.zone.Ser">dedicated serialized form</a>.
144      * @serialData
145      * Refer to the serialized form of
146      * <a href="{@docRoot}/serialized-form.html#hunt.time.zone.ZoneRules">ZoneRules.writeReplace</a>
147      * for the encoding of epoch seconds and offsets.
148      * <pre style="font-size:1.0em">{@code
149      *
150      *   _out.writeByte(2);                // identifies a ZoneOffsetTransition
151      *   _out.writeEpochSec(toEpochSecond);
152      *   _out.writeOffset(offsetBefore);
153      *   _out.writeOffset(offsetAfter);
154      * }
155      * </pre>
156      * @return the replacing object, not null
157      */
158     private Object writeReplace() {
159         return new Ser(Ser.ZOT, this);
160     }
161 
162     /**
163      * Writes the state to the stream.
164      *
165      * @param _out  the output stream, not null
166      * @throws IOException if an error occurs
167      */
168     void writeExternal(DataOutput _out) /*throws IOException*/ {
169         Ser.writeEpochSec(epochSecond, _out);
170         Ser.writeOffset(offsetBefore, _out);
171         Ser.writeOffset(offsetAfter, _out);
172     }
173 
174     /**
175      * Reads the state from the stream.
176      *
177      * @param _in  the input stream, not null
178      * @return the created object, not null
179      * @throws IOException if an error occurs
180      */
181     static ZoneOffsetTransition readExternal(DataInput _in) /*throws IOException*/ {
182         long epochSecond = Ser.readEpochSec(_in);
183         ZoneOffset before = Ser.readOffset(_in);
184         ZoneOffset after = Ser.readOffset(_in);
185         if (before == after) {
186             throw new IllegalArgumentException("Offsets must not be equal");
187         }
188         return new ZoneOffsetTransition(epochSecond, before, after);
189     }
190 
191     //-----------------------------------------------------------------------
192     /**
193      * Gets the transition instant.
194      * !(p)
195      * This is the instant of the discontinuity, which is defined as the first
196      * instant that the 'after' offset applies.
197      * !(p)
198      * The methods {@link #getInstant()}, {@link #getDateTimeBefore()} and {@link #getDateTimeAfter()}
199      * all represent the same instant.
200      *
201      * @return the transition instant, not null
202      */
203     public Instant getInstant() {
204         return Instant.ofEpochSecond(epochSecond);
205     }
206 
207     /**
208      * Gets the transition instant as an epoch second.
209      *
210      * @return the transition epoch second
211      */
212     public long toEpochSecond() {
213         return epochSecond;
214     }
215 
216     //-------------------------------------------------------------------------
217     /**
218      * Gets the local transition date-time, as would be expressed with the 'before' offset.
219      * !(p)
220      * This is the date-time where the discontinuity begins expressed with the 'before' offset.
221      * At this instant, the 'after' offset is actually used, therefore the combination of this
222      * date-time and the 'before' offset will never occur.
223      * !(p)
224      * The combination of the 'before' date-time and offset represents the same instant
225      * as the 'after' date-time and offset.
226      *
227      * @return the transition date-time expressed with the before offset, not null
228      */
229     public LocalDateTime getDateTimeBefore() {
230         return transition;
231     }
232 
233     /**
234      * Gets the local transition date-time, as would be expressed with the 'after' offset.
235      * !(p)
236      * This is the first date-time after the discontinuity, when the new offset applies.
237      * !(p)
238      * The combination of the 'before' date-time and offset represents the same instant
239      * as the 'after' date-time and offset.
240      *
241      * @return the transition date-time expressed with the after offset, not null
242      */
243     public LocalDateTime getDateTimeAfter() {
244         return transition.plusSeconds(getDurationSeconds());
245     }
246 
247     /**
248      * Gets the offset before the transition.
249      * !(p)
250      * This is the offset _in use before the instant of the transition.
251      *
252      * @return the offset before the transition, not null
253      */
254     public ZoneOffset getOffsetBefore() {
255         return offsetBefore;
256     }
257 
258     /**
259      * Gets the offset after the transition.
260      * !(p)
261      * This is the offset _in use on and after the instant of the transition.
262      *
263      * @return the offset after the transition, not null
264      */
265     public ZoneOffset getOffsetAfter() {
266         return offsetAfter;
267     }
268 
269     /**
270      * Gets the duration of the transition.
271      * !(p)
272      * In most cases, the transition duration is one hour, however this is not always the case.
273      * The duration will be positive for a gap and negative for an overlap.
274      * Time-zones are second-based, so the nanosecond part of the duration will be zero.
275      *
276      * @return the duration of the transition, positive for gaps, negative for overlaps
277      */
278     public Duration getDuration() {
279         return Duration.ofSeconds(getDurationSeconds());
280     }
281 
282     /**
283      * Gets the duration of the transition _in seconds.
284      *
285      * @return the duration _in seconds
286      */
287     private int getDurationSeconds() {
288         return getOffsetAfter().getTotalSeconds() - getOffsetBefore().getTotalSeconds();
289     }
290 
291     /**
292      * Does this transition represent a gap _in the local time-line.
293      * !(p)
294      * Gaps occur where there are local date-times that simply do not exist.
295      * An example would be when the offset changes from {@code +01:00} to {@code +02:00}.
296      * This might be described as 'the clocks will move forward one hour tonight at 1am'.
297      *
298      * @return true if this transition is a gap, false if it is an overlap
299      */
300     public bool isGap() {
301         return getOffsetAfter().getTotalSeconds() > getOffsetBefore().getTotalSeconds();
302     }
303 
304     /**
305      * Does this transition represent an overlap _in the local time-line.
306      * !(p)
307      * Overlaps occur where there are local date-times that exist twice.
308      * An example would be when the offset changes from {@code +02:00} to {@code +01:00}.
309      * This might be described as 'the clocks will move back one hour tonight at 2am'.
310      *
311      * @return true if this transition is an overlap, false if it is a gap
312      */
313     public bool isOverlap() {
314         return getOffsetAfter().getTotalSeconds() < getOffsetBefore().getTotalSeconds();
315     }
316 
317     /**
318      * Checks if the specified offset is valid during this transition.
319      * !(p)
320      * This checks to see if the given offset will be valid at some point _in the transition.
321      * A gap will always return false.
322      * An overlap will return true if the offset is either the before or after offset.
323      *
324      * @param offset  the offset to check, null returns false
325      * @return true if the offset is valid during the transition
326      */
327     public bool isValidOffset(ZoneOffset offset) {
328         return isGap() ? false : (getOffsetBefore() == offset) || (getOffsetAfter()== offset);
329     }
330 
331     /**
332      * Gets the valid offsets during this transition.
333      * !(p)
334      * A gap will return an empty list, while an overlap will return both offsets.
335      *
336      * @return the list of valid offsets
337      */
338     List!(ZoneOffset) getValidOffsets() {
339         if (isGap()) {
340             return new ArrayList!(ZoneOffset)();
341         }
342         auto l = new ArrayList!(ZoneOffset)();
343         l.add(getOffsetBefore());
344         l.add(getOffsetAfter());
345         return l;
346     }
347 
348     //-----------------------------------------------------------------------
349     /**
350      * Compares this transition to another based on the transition instant.
351      * !(p)
352      * This compares the instants of each transition.
353      * The offsets are ignored, making this order inconsistent with equals.
354      *
355      * @param transition  the transition to compare to, not null
356      * @return the comparator value, negative if less, positive if greater
357      */
358     // override
359     public int compareTo(ZoneOffsetTransition transition) {
360         return compare(epochSecond, transition.epochSecond);
361     }
362 
363     override
364     public int opCmp(ZoneOffsetTransition transition) {
365         return compare(epochSecond, transition.epochSecond);
366     }
367 
368     //-----------------------------------------------------------------------
369     /**
370      * Checks if this object equals another.
371      * !(p)
372      * The entire state of the object is compared.
373      *
374      * @param other  the other object to compare to, null returns false
375      * @return true if equal
376      */
377     override
378     public bool opEquals(Object other) {
379         if (other == this) {
380             return true;
381         }
382         if (cast(ZoneOffsetTransition)(other) !is null) {
383             ZoneOffsetTransition d = cast(ZoneOffsetTransition) other;
384             return epochSecond == d.epochSecond &&
385                 (offsetBefore == d.offsetBefore) && (offsetAfter == d.offsetAfter);
386         }
387         return false;
388     }
389 
390     /**
391      * Returns a suitable hash code.
392      *
393      * @return the hash code
394      */
395     override
396     public size_t toHash() @trusted nothrow {
397         try{
398             return transition.toHash() ^ offsetBefore.toHash() ^ Integer.rotateLeft(cast(int)(offsetAfter.toHash()), 16);
399         }catch(Exception e){
400         return int.init;
401         }
402     }
403 
404     //-----------------------------------------------------------------------
405     /**
406      * Returns a string describing this object.
407      *
408      * @return a string for debugging, not null
409      */
410     override
411     public string toString() {
412         StringBuilder buf = new StringBuilder();
413         buf.append("Transition[")
414             .append(isGap() ? "Gap" : "Overlap")
415             .append(" at ")
416             .append(transition.toString)
417             .append(offsetBefore.toString)
418             .append(" to ")
419             .append(offsetAfter.toString)
420             .append(']');
421         return buf.toString();
422     }
423 
424 }