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.chrono.ChronoZonedDateTimeImpl;
13 
14 import hunt.time.temporal.ChronoUnit;
15 
16 import hunt.Exceptions;
17 import hunt.Integer;
18 import hunt.Long;
19 import hunt.stream.ObjectInput;
20 import hunt.stream.ObjectOutput;
21 import hunt.stream.Common;
22 import hunt.util.Comparator;
23 import hunt.time.Instant;
24 import hunt.time.LocalDateTime;
25 import hunt.time.ZoneId;
26 import hunt.time.ZoneOffset;
27 import hunt.time.temporal.ChronoField;
28 import hunt.time.temporal.ChronoUnit;
29 import hunt.time.temporal.Temporal;
30 import hunt.time.temporal.TemporalField;
31 import hunt.time.temporal.TemporalUnit;
32 import hunt.time.zone.ZoneOffsetTransition;
33 import hunt.time.zone.ZoneRules;
34 import hunt.collection.List;
35 import hunt.time.chrono.ChronoLocalDate;
36 import hunt.time.chrono.ChronoZonedDateTime;
37 import hunt.time.chrono.ChronoLocalDateTimeImpl;
38 import hunt.time.chrono.Chronology;
39 import hunt.time.chrono.ChronoLocalDateTime;
40 import hunt.time.temporal.TemporalAdjuster;
41 import hunt.time.chrono.Ser;
42 import hunt.time.temporal.ValueRange;
43 import hunt.time.temporal.TemporalAmount;
44 import std.conv;
45 import hunt.time.Exceptions;
46 import hunt.time.Exceptions;
47 // import hunt.time.format.DateTimeFormatter;
48 import hunt.time.LocalTime;
49 /**
50  * A date-time with a time-zone _in the calendar neutral API.
51  * !(p)
52  * {@code ZoneChronoDateTime} is an immutable representation of a date-time with a time-zone.
53  * This class stores all date and time fields, to a precision of nanoseconds,
54  * as well as a time-zone and zone offset.
55  * !(p)
56  * The purpose of storing the time-zone is to distinguish the ambiguous case where
57  * the local time-line overlaps, typically as a result of the end of daylight time.
58  * Information about the local-time can be obtained using methods on the time-zone.
59  *
60  * @implSpec
61  * This class is immutable and thread-safe.
62  *
63  * @serial Document the delegation of this class _in the serialized-form specification.
64  * @param !(D) the concrete type for the date of this date-time
65  * @since 1.8
66  */
67 final class ChronoZonedDateTimeImpl(D = ChronoLocalDate) if(is(D : ChronoLocalDate))
68         : ChronoZonedDateTime!(D) { //, Serializable
69 
70 
71     /**
72      * The local date-time.
73      */
74     private  /*transient*/ ChronoLocalDateTimeImpl!(D) dateTime;
75     /**
76      * The zone offset.
77      */
78     private  /*transient*/ ZoneOffset offset;
79     /**
80      * The zone ID.
81      */
82     private  /*transient*/ ZoneId zone;
83 
84     //-----------------------------------------------------------------------
85     /**
86      * Obtains an instance from a local date-time using the preferred offset if possible.
87      *
88      * @param localDateTime  the local date-time, not null
89      * @param zone  the zone identifier, not null
90      * @param preferredOffset  the zone offset, null if no preference
91      * @return the zoned date-time, not null
92      */
93     static ChronoZonedDateTime!(R) ofBest(R)(
94             ChronoLocalDateTimeImpl!(R) localDateTime, ZoneId zone, ZoneOffset preferredOffset) /* if(is(R : ChronoLocalDate)) */ {
95         assert(localDateTime, "localDateTime");
96         assert(zone, "zone");
97         if (cast(ZoneOffset)(zone) !is null) {
98             return new ChronoZonedDateTimeImpl!()(localDateTime, cast(ZoneOffset) zone, zone);
99         }
100         ZoneRules rules = zone.getRules();
101         LocalDateTime isoLDT = LocalDateTime.from(localDateTime);
102         List!(ZoneOffset) validOffsets = rules.getValidOffsets(isoLDT);
103         ZoneOffset offset;
104         if (validOffsets.size() == 1) {
105             offset = validOffsets.get(0);
106         } else if (validOffsets.size() == 0) {
107             ZoneOffsetTransition trans = rules.getTransition(isoLDT);
108             localDateTime = localDateTime.plusSeconds(trans.getDuration().getSeconds());
109             offset = trans.getOffsetAfter();
110         } else {
111             if (preferredOffset !is null && validOffsets.contains(preferredOffset)) {
112                 offset = preferredOffset;
113             } else {
114                 offset = validOffsets.get(0);
115             }
116         }
117         assert(offset, "offset");  // protect against bad ZoneRules
118         return new ChronoZonedDateTimeImpl!()(localDateTime, offset, zone);
119     }
120 
121     /**
122      * Obtains an instance from an instant using the specified time-zone.
123      *
124      * @param chrono  the chronology, not null
125      * @param instant  the instant, not null
126      * @param zone  the zone identifier, not null
127      * @return the zoned date-time, not null
128      */
129     static ChronoZonedDateTimeImpl!(ChronoLocalDate) ofInstant(Chronology chrono, Instant instant, ZoneId zone) {
130         ZoneRules rules = zone.getRules();
131         ZoneOffset offset = rules.getOffset(instant);
132         assert(offset, "offset");  // protect against bad ZoneRules
133         LocalDateTime ldt = LocalDateTime.ofEpochSecond(instant.getEpochSecond(), instant.getNano(), offset);
134         ChronoLocalDateTimeImpl!(ChronoLocalDate) cldt = cast(ChronoLocalDateTimeImpl!(ChronoLocalDate))chrono.localDateTime(ldt);
135         return new ChronoZonedDateTimeImpl!(ChronoLocalDate)(cldt, offset, zone);
136     }
137 
138     /**
139      * Obtains an instance from an {@code Instant}.
140      *
141      * @param instant  the instant to create the date-time from, not null
142      * @param zone  the time-zone to use, validated not null
143      * @return the zoned date-time, validated not null
144      */
145     /*@SuppressWarnings("unchecked")*/
146     private ChronoZonedDateTimeImpl!(D) create(Instant instant, ZoneId zone) {
147         return cast(ChronoZonedDateTimeImpl!(D))ofInstant(getChronology(), instant, zone);
148     }
149 
150     /**
151      * Casts the {@code Temporal} to {@code ChronoZonedDateTimeImpl} ensuring it bas the specified chronology.
152      *
153      * @param chrono  the chronology to check for, not null
154      * @param temporal  a date-time to cast, not null
155      * @return the date-time checked and cast to {@code ChronoZonedDateTimeImpl}, not null
156      * @throws ClassCastException if the date-time cannot be cast to ChronoZonedDateTimeImpl
157      *  or the chronology is not equal this Chronology
158      */
159     static  ChronoZonedDateTimeImpl!(R) ensureValid(R)(Chronology chrono, Temporal temporal) {
160         /*@SuppressWarnings("unchecked")*/
161         ChronoZonedDateTimeImpl!(R) other = cast(ChronoZonedDateTimeImpl!(R))temporal;
162         if ((chrono == other.getChronology()) == false) {
163             throw new ClassCastException("Chronology mismatch, required: " ~ chrono.getId()
164                     ~ ", actual: " ~ other.getChronology().getId());
165         }
166         return other;
167     }
168 
169     //-----------------------------------------------------------------------
170     /**
171      * Constructor.
172      *
173      * @param dateTime  the date-time, not null
174      * @param offset  the zone offset, not null
175      * @param zone  the zone ID, not null
176      */
177     private this(ChronoLocalDateTimeImpl!(D) dateTime, ZoneOffset offset, ZoneId zone) {
178         this.dateTime = dateTime;
179         this.offset = offset;
180         this.zone = zone;
181     }
182 
183     //-----------------------------------------------------------------------
184     override
185     public ZoneOffset getOffset() {
186         return offset;
187     }
188 
189     override
190     public ChronoZonedDateTime!(D) withEarlierOffsetAtOverlap() {
191         ZoneOffsetTransition trans = getZone().getRules().getTransition(LocalDateTime.from(this));
192         if (trans !is null && trans.isOverlap()) {
193             ZoneOffset earlierOffset = trans.getOffsetBefore();
194             if ((earlierOffset  == offset) == false) {
195                 return new ChronoZonedDateTimeImpl!()(dateTime, earlierOffset, zone);
196             }
197         }
198         return this;
199     }
200 
201     override
202     public ChronoZonedDateTime!(D) withLaterOffsetAtOverlap() {
203         ZoneOffsetTransition trans = getZone().getRules().getTransition(LocalDateTime.from(this));
204         if (trans !is null) {
205             ZoneOffset offset = trans.getOffsetAfter();
206             if ((offset == getOffset()) == false) {
207                 return new ChronoZonedDateTimeImpl!(D)(dateTime, offset, zone);
208             }
209         }
210         return this;
211     }
212 
213     //-----------------------------------------------------------------------
214     override
215     public ChronoLocalDateTime!(D) toLocalDateTime() {
216         return dateTime;
217     }
218 
219     override
220     public ZoneId getZone() {
221         return zone;
222     }
223 
224     override
225     public ChronoZonedDateTime!(D) withZoneSameLocal(ZoneId zone) {
226         return ofBest(dateTime, zone, offset);
227     }
228 
229     override
230     public ChronoZonedDateTime!(D) withZoneSameInstant(ZoneId zone) {
231         assert(zone, "zone");
232         return this.zone == (zone) ? this : create(dateTime.toInstant(offset), zone);
233     }
234 
235     //-----------------------------------------------------------------------
236     override
237     public bool isSupported(TemporalField field) {
238         return cast(ChronoField)(field) !is null || (field !is null && field.isSupportedBy(this));
239     }
240 
241     //-----------------------------------------------------------------------
242     override
243     public ChronoZonedDateTime!(D) _with(TemporalField field, long newValue) {
244         if (cast(ChronoField)(field) !is null) {
245             ChronoField f = cast(ChronoField) field;
246             {
247                 if( f == ChronoField.INSTANT_SECONDS) return plus(newValue - toEpochSecond(), ChronoUnit.SECONDS);
248                 if( f == ChronoField.OFFSET_SECONDS) {
249                     ZoneOffset offset = ZoneOffset.ofTotalSeconds(f.checkValidIntValue(newValue));
250                     return create(dateTime.toInstant(offset), zone);
251                 }
252             }
253             return ofBest(dateTime._with(field, newValue), zone, offset);
254         }
255         return ChronoZonedDateTimeImpl!D.ensureValid!D(getChronology(), field.adjustInto(this, newValue));
256     }
257 
258     //-----------------------------------------------------------------------
259     override
260     public ChronoZonedDateTime!(D) plus(long amountToAdd, TemporalUnit unit) {
261         if (cast(ChronoUnit)(unit) !is null) {
262             return super_with(dateTime.plus(amountToAdd, unit));
263         }
264         return ChronoZonedDateTimeImpl!D.ensureValid!D(getChronology(), unit.addTo(this, amountToAdd));   /// TODO: Generics replacement Risk!
265     }
266      ChronoZonedDateTime!(D) super_with(TemporalAdjuster adjuster) {
267         return ChronoZonedDateTimeImpl!D.ensureValid!D(getChronology(), adjuster.adjustInto(this));
268     }
269 
270     //-----------------------------------------------------------------------
271     override
272     public long until(Temporal endExclusive, TemporalUnit unit) {
273         assert(endExclusive, "endExclusive");
274         /*@SuppressWarnings("unchecked")*/
275         ChronoZonedDateTime!(D) end = cast(ChronoZonedDateTime!(D)) getChronology().zonedDateTime(endExclusive);
276         if (cast(ChronoUnit)(unit) !is null) {
277             end = end.withZoneSameInstant(offset);
278             return dateTime.until(end.toLocalDateTime(), unit);
279         }
280         assert(unit, "unit");
281         return unit.between(this, end);
282     }
283 
284     //-----------------------------------------------------------------------
285     /**
286      * Writes the ChronoZonedDateTime using a
287      * <a href="{@docRoot}/serialized-form.html#hunt.time.chrono.Ser">dedicated serialized form</a>.
288      * @serialData
289      * !(pre)
290      *  _out.writeByte(3);                  // identifies a ChronoZonedDateTime
291      *  _out.writeObject(toLocalDateTime());
292      *  _out.writeObject(getOffset());
293      *  _out.writeObject(getZone());
294      * </pre>
295      *
296      * @return the instance of {@code Ser}, not null
297      */
298     private Object writeReplace() {
299         return new Ser(Ser.CHRONO_ZONE_DATE_TIME_TYPE, this);
300     }
301 
302     /**
303      * Defend against malicious streams.
304      *
305      * @param s the stream to read
306      * @throws InvalidObjectException always
307      */
308      ///@gxc
309     // private void readObject(ObjectInputStream s) /*throws InvalidObjectException*/ {
310     //     throw new InvalidObjectException("Deserialization via serialization delegate");
311     // }
312 
313     void writeExternal(ObjectOutput _out) /*throws IOException*/ {
314         _out.writeObject(dateTime);
315         _out.writeObject(offset);
316         _out.writeObject(zone);
317     }
318 
319     static ChronoZonedDateTime!(ChronoLocalDate) readExternal(ObjectInput _in) /*throws IOException, ClassNotFoundException */{
320         ChronoLocalDateTime!(ChronoLocalDate) dateTime = cast(ChronoLocalDateTime!(ChronoLocalDate)) _in.readObject();
321         ZoneOffset offset = cast(ZoneOffset) _in.readObject();
322         ZoneId zone = cast(ZoneId) _in.readObject();
323         return dateTime.atZone(offset).withZoneSameLocal(zone);
324         // TODO: ZDT uses ofLenient()
325     }
326 
327     //-------------------------------------------------------------------------
328     override
329     public bool opEquals(Object obj) {
330         if (this is obj) {
331             return true;
332         }
333         if (cast(ChronoZonedDateTime!D)(obj) !is null) {
334             return compareTo(cast(ChronoZonedDateTime!(D)) obj) == 0;
335         }
336         return false;
337     }
338 
339     override
340     public size_t toHash() @trusted nothrow {
341         try{
342             return toLocalDateTime().toHash() ^ getOffset().toHash() ^ Integer.rotateLeft(cast(int)(getZone().toHash()), 3);
343         }catch(Exception e){
344             return int.init;
345         }
346     }
347 
348     override
349     public string toString() {
350         string str = toLocalDateTime().toString() ~ getOffset().toString();
351         if (getOffset() != getZone()) {
352             str ~= '[' ~ getZone().toString() ~ ']';
353         }
354         return str;
355     }
356 
357     override
358      ValueRange range(TemporalField field) {
359         if (cast(ChronoField)(field) !is null) {
360             if (field == ChronoField.INSTANT_SECONDS || field == ChronoField.OFFSET_SECONDS) {
361                 return field.range();
362             }
363             return toLocalDateTime().range(field);
364         }
365         return field.rangeRefinedBy(this);
366     }
367 	
368 	override
369      int get(TemporalField field) {
370         if (cast(ChronoField)(field) !is null) {
371             auto f = cast(ChronoField) field;
372             {
373                 if( f == ChronoField.INSTANT_SECONDS)
374                     throw new UnsupportedTemporalTypeException("Invalid field 'InstantSeconds' for get() method, use getLong() instead");
375                 if( f == ChronoField.OFFSET_SECONDS)
376                     return getOffset().getTotalSeconds();
377             }
378             return toLocalDateTime().get(field);
379         }
380         return /* Temporal. */super_get(field);
381     }
382 
383      int super_get(TemporalField field) {
384         ValueRange range = range(field);
385         if (range.isIntValue() == false) {
386             throw new UnsupportedTemporalTypeException("Invalid field " ~ field.toString ~ " for get() method, use getLong() instead");
387         }
388         long value = getLong(field);
389         if (range.isValidValue(value) == false) {
390             throw new DateTimeException("Invalid value for " ~ field.toString ~ " (valid values " ~ range.toString ~ "): " ~ value.to!string);
391         }
392         return cast(int) value;
393     }
394 	
395 	override
396      long getLong(TemporalField field) {
397         if (cast(ChronoField)(field) !is null) {
398             auto f = cast(ChronoField) field;
399             {
400                 if ( f == ChronoField.INSTANT_SECONDS) return toEpochSecond();
401                 if ( f == ChronoField.OFFSET_SECONDS)return getOffset().getTotalSeconds();
402             }
403             return toLocalDateTime().getLong(field);
404         }
405         return field.getFrom(this);
406     }
407 	 override
408      bool isSupported(TemporalUnit unit) {
409         if (cast(ChronoUnit)(unit) !is null) {
410             return unit != ChronoUnit.FOREVER;
411         }
412         return unit !is null && unit.isSupportedBy(this);
413     }
414 	
415 	override
416      ChronoZonedDateTime!(D) _with(TemporalAdjuster adjuster) {
417         return ChronoZonedDateTimeImpl!D.ensureValid!D(getChronology(), /* Temporal. */adjuster.adjustInto(this));
418     }
419 	
420 	override
421      ChronoZonedDateTime!(D) plus(TemporalAmount amount) {
422         return ChronoZonedDateTimeImpl!D.ensureValid!D(getChronology(), /* Temporal. */amount.addTo(this));
423     }
424 	override
425      ChronoZonedDateTime!(D) minus(TemporalAmount amount) {
426         return ChronoZonedDateTimeImpl!D.ensureValid!D(getChronology(), /* Temporal. */amount.subtractFrom(this));
427     }
428 
429 	 override
430      ChronoZonedDateTime!(D) minus(long amountToSubtract, TemporalUnit unit) {
431         return ChronoZonedDateTimeImpl!D.ensureValid!D(getChronology(), /* Temporal. */(amountToSubtract == Long.MIN_VALUE ? plus(Long.MAX_VALUE, unit).plus(1, unit) : plus(-amountToSubtract, unit)));
432     }
433 
434     override
435      int compareTo(ChronoZonedDateTime!(ChronoLocalDate) other) {
436         int cmp = compare(toEpochSecond(), other.toEpochSecond());
437         if (cmp == 0) {
438             cmp = toLocalTime().getNano() - other.toLocalTime().getNano();
439             if (cmp == 0) {
440                 cmp = toLocalDateTime().compareTo(other.toLocalDateTime());
441                 if (cmp == 0) {
442                     cmp = getZone().getId().compare(other.getZone().getId());
443                     if (cmp == 0) {
444                         cmp = getChronology().compareTo(other.getChronology());
445                     }
446                 }
447             }
448         }
449         return cmp;
450     }
451 
452     override
453      int opCmp(ChronoZonedDateTime!(ChronoLocalDate) other) {
454         int cmp = compare(toEpochSecond(), other.toEpochSecond());
455         if (cmp == 0) {
456             cmp = toLocalTime().getNano() - other.toLocalTime().getNano();
457             if (cmp == 0) {
458                 cmp = toLocalDateTime().compareTo(other.toLocalDateTime());
459                 if (cmp == 0) {
460                     cmp = getZone().getId().compare(other.getZone().getId());
461                     if (cmp == 0) {
462                         cmp = getChronology().compareTo(other.getChronology());
463                     }
464                 }
465             }
466         }
467         return cmp;
468     }
469     
470     override
471 	 bool isBefore(ChronoZonedDateTime!(ChronoLocalDate) other) {
472         long thisEpochSec = toEpochSecond();
473         long otherEpochSec = other.toEpochSecond();
474         return thisEpochSec < otherEpochSec ||
475             (thisEpochSec == otherEpochSec && toLocalTime().getNano() < other.toLocalTime().getNano());
476     }
477 	
478     override
479 	 bool isAfter(ChronoZonedDateTime!(ChronoLocalDate) other) {
480         long thisEpochSec = toEpochSecond();
481         long otherEpochSec = other.toEpochSecond();
482         return thisEpochSec > otherEpochSec ||
483             (thisEpochSec == otherEpochSec && toLocalTime().getNano() > other.toLocalTime().getNano());
484     }
485 	
486     override
487 	 bool isEqual(ChronoZonedDateTime!(ChronoLocalDate) other) {
488         return toEpochSecond() == other.toEpochSecond() &&
489                 toLocalTime().getNano() == other.toLocalTime().getNano();
490     }
491 	
492     override
493 	 long toEpochSecond() {
494         long epochDay = toLocalDate().toEpochDay();
495         long secs = epochDay * 86400 + toLocalTime().toSecondOfDay();
496         secs -= getOffset().getTotalSeconds();
497         return secs;
498     }
499 	
500     override
501 	 Instant toInstant() {
502         return Instant.ofEpochSecond(toEpochSecond(), toLocalTime().getNano());
503     }
504 	
505     // override
506 	//  string format(DateTimeFormatter formatter) {
507     //     assert(formatter, "formatter");
508     //     return formatter.format(this);
509     // }
510 	
511     override
512 	 LocalTime toLocalTime() {
513         return toLocalDateTime().toLocalTime();
514     }
515 	
516     override
517 	 D toLocalDate() {
518         return toLocalDateTime().toLocalDate();
519     }
520 
521     override
522      Chronology getChronology() {
523         return toLocalDate().getChronology();
524     }
525 }