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.ChronoPeriodImpl;
13 
14 import hunt.time.temporal.ChronoField;
15 import hunt.time.temporal.ChronoUnit;
16 
17 import hunt.stream.DataInput;
18 import hunt.stream.DataOutput;
19 import hunt.Exceptions;
20 import hunt.collection;
21 //import hunt.io.ObjectInputStream;
22 // import hunt.io.ObjectStreamException;
23 import hunt.stream.Common;
24 import hunt.time.Exceptions;
25 import hunt.time.temporal.ChronoUnit;
26 import hunt.time.temporal.Temporal;
27 import hunt.time.temporal.TemporalAccessor;
28 import hunt.time.temporal.TemporalAmount;
29 import hunt.time.temporal.TemporalQueries;
30 import hunt.time.temporal.TemporalUnit;
31 import hunt.time.Exceptions;
32 import hunt.time.temporal.ValueRange;
33 import hunt.collection.List;
34 import hunt.time.chrono.Chronology;
35 import hunt.time.chrono.ChronoPeriod;
36 import hunt.Integer;
37 import hunt.math.Helper;
38 import hunt.util.StringBuilder;
39 import hunt.time.chrono.Ser;
40 import hunt.time.util.QueryHelper;
41 
42 import hunt.util.Common;
43 // import hunt.serialization.JsonSerializer;
44 
45 
46 /**
47  * A period expressed _in terms of a standard year-month-day calendar system.
48  * !(p)
49  * This class is used by applications seeking to handle dates _in non-ISO calendar systems.
50  * For example, the Japanese, Minguo, Thai Buddhist and others.
51  *
52  * @implSpec
53  * This class is immutable nad thread-safe.
54  *
55  * @since 1.8
56  */
57 final class ChronoPeriodImpl
58         : ChronoPeriod { // , Serializable 
59     // this class is only used by JDK chronology implementations and makes assumptions based on that fact
60 
61 
62     /**
63      * The set of supported units.
64      */
65     __gshared List!(TemporalUnit) _SUPPORTED_UNITS;
66 
67     public static ref List!(TemporalUnit) SUPPORTED_UNITS()
68     {
69         if(_SUPPORTED_UNITS is null)
70         {
71             _SUPPORTED_UNITS = new ArrayList!TemporalUnit();
72             _SUPPORTED_UNITS.add(ChronoUnit.YEARS);
73             _SUPPORTED_UNITS.add(ChronoUnit.MONTHS);
74             _SUPPORTED_UNITS.add(ChronoUnit.DAYS);
75         }
76         return _SUPPORTED_UNITS;
77     }
78 
79     // shared static this()
80     // {
81     //     SUPPORTED_UNITS = new ArrayList!TemporalUnit();
82     //     SUPPORTED_UNITS.add(ChronoUnit.YEARS);
83     //     SUPPORTED_UNITS.add(ChronoUnit.MONTHS);
84     //     SUPPORTED_UNITS.add(ChronoUnit.DAYS);
85     // }
86     /**
87      * The chronology.
88      */
89     private  Chronology chrono;
90     /**
91      * The number of years.
92      */
93      int years;
94     /**
95      * The number of months.
96      */
97      int months;
98     /**
99      * The number of days.
100      */
101      int days;
102 
103     /**
104      * Creates an instance.
105      */
106     this(Chronology chrono, int years, int months, int days) {
107         assert(chrono, "chrono");
108         this.chrono = chrono;
109         this.years = years;
110         this.months = months;
111         this.days = days;
112     }
113 
114     //-----------------------------------------------------------------------
115     override
116     public long get(TemporalUnit unit) {
117         if (unit == ChronoUnit.YEARS) {
118             return years;
119         } else if (unit == ChronoUnit.MONTHS) {
120             return months;
121         } else if (unit == ChronoUnit.DAYS) {
122             return days;
123         } else {
124             throw new UnsupportedTemporalTypeException("Unsupported unit: " ~ typeid(unit).name);
125         }
126     }
127 
128     override
129     public List!(TemporalUnit) getUnits() {
130         return ChronoPeriodImpl.SUPPORTED_UNITS;
131     }
132 
133     override
134     public Chronology getChronology() {
135         return chrono;
136     }
137 
138     //-----------------------------------------------------------------------
139     override
140     public bool isZero() {
141         return years == 0 && months == 0 && days == 0;
142     }
143 
144     override
145     public bool isNegative() {
146         return years < 0 || months < 0 || days < 0;
147     }
148 
149     //-----------------------------------------------------------------------
150     override
151     public ChronoPeriod plus(TemporalAmount amountToAdd) {
152         ChronoPeriodImpl amount = validateAmount(amountToAdd);
153         return new ChronoPeriodImpl(
154                 chrono,
155                 MathHelper.addExact(years, amount.years),
156                 MathHelper.addExact(months, amount.months),
157                 MathHelper.addExact(days, amount.days));
158     }
159 
160     override
161     public ChronoPeriod minus(TemporalAmount amountToSubtract) {
162         ChronoPeriodImpl amount = validateAmount(amountToSubtract);
163         return new ChronoPeriodImpl(
164                 chrono,
165                 MathHelper.subtractExact(years, amount.years),
166                 MathHelper.subtractExact(months, amount.months),
167                 MathHelper.subtractExact(days, amount.days));
168     }
169 
170     /**
171      * Obtains an instance of {@code ChronoPeriodImpl} from a temporal amount.
172      *
173      * @param amount  the temporal amount to convert, not null
174      * @return the period, not null
175      */
176     private ChronoPeriodImpl validateAmount(TemporalAmount amount) {
177         assert(amount, "amount");
178         if ((cast(ChronoPeriodImpl)(amount) !is null) == false) {
179             throw new DateTimeException("Unable to obtain ChronoPeriod from TemporalAmount: " ~ typeid(amount).stringof);
180         }
181         ChronoPeriodImpl period = cast(ChronoPeriodImpl) amount;
182         if ((chrono == period.getChronology()) == false) {
183             throw new ClassCastException("Chronology mismatch, expected: " ~ chrono.getId() ~ ", actual: " ~ period.getChronology().getId());
184         }
185         return period;
186     }
187 
188     //-----------------------------------------------------------------------
189     override
190     public ChronoPeriod multipliedBy(int scalar) {
191         if (this.isZero() || scalar == 1) {
192             return this;
193         }
194         return new ChronoPeriodImpl(
195                 chrono,
196                 MathHelper.multiplyExact(years, scalar),
197                 MathHelper.multiplyExact(months, scalar),
198                 MathHelper.multiplyExact(days, scalar));
199     }
200 
201     //-----------------------------------------------------------------------
202     override
203     public ChronoPeriod normalized() {
204         long monthRange = monthRange();
205         if (monthRange > 0) {
206             long totalMonths = years * monthRange + months;
207             long splitYears = totalMonths / monthRange;
208             int splitMonths = cast(int) (totalMonths % monthRange);  // no overflow
209             if (splitYears == years && splitMonths == months) {
210                 return this;
211             }
212             return new ChronoPeriodImpl(chrono, MathHelper.toIntExact(splitYears), splitMonths, days);
213 
214         }
215         return this;
216     }
217 
218     override
219      ChronoPeriod negated() {
220         return multipliedBy(-1);
221     }
222     /**
223      * Calculates the range of months.
224      *
225      * @return the month range, -1 if not fixed range
226      */
227     private long monthRange() {
228         ValueRange startRange = chrono.range(ChronoField.MONTH_OF_YEAR);
229         if (startRange.isFixed() && startRange.isIntValue()) {
230             return startRange.getMaximum() - startRange.getMinimum() + 1;
231         }
232         return -1;
233     }
234 
235     //-------------------------------------------------------------------------
236     override
237     public Temporal addTo(Temporal temporal) {
238         validateChrono(temporal);
239         if (months == 0) {
240             if (years != 0) {
241                 temporal = temporal.plus(years, ChronoUnit.YEARS);
242             }
243         } else {
244             long monthRange = monthRange();
245             if (monthRange > 0) {
246                 temporal = temporal.plus(years * monthRange + months, ChronoUnit.MONTHS);
247             } else {
248                 if (years != 0) {
249                     temporal = temporal.plus(years, ChronoUnit.YEARS);
250                 }
251                 temporal = temporal.plus(months, ChronoUnit.MONTHS);
252             }
253         }
254         if (days != 0) {
255             temporal = temporal.plus(days, ChronoUnit.DAYS);
256         }
257         return temporal;
258     }
259 
260 
261 
262     override
263     public Temporal subtractFrom(Temporal temporal) {
264         validateChrono(temporal);
265         if (months == 0) {
266             if (years != 0) {
267                 temporal = temporal.minus(years, ChronoUnit.YEARS);
268             }
269         } else {
270             long monthRange = monthRange();
271             if (monthRange > 0) {
272                 temporal = temporal.minus(years * monthRange + months, ChronoUnit.MONTHS);
273             } else {
274                 if (years != 0) {
275                     temporal = temporal.minus(years, ChronoUnit.YEARS);
276                 }
277                 temporal = temporal.minus(months, ChronoUnit.MONTHS);
278             }
279         }
280         if (days != 0) {
281             temporal = temporal.minus(days, ChronoUnit.DAYS);
282         }
283         return temporal;
284     }
285 
286     /**
287      * Validates that the temporal has the correct chronology.
288      */
289     private void validateChrono(TemporalAccessor temporal) {
290         assert(temporal, "temporal");
291         Chronology temporalChrono = QueryHelper.query!Chronology(temporal , TemporalQueries.chronology());
292         if (temporalChrono !is null && (chrono == temporalChrono) == false) {
293             throw new DateTimeException("Chronology mismatch, expected: " ~ chrono.getId() ~ ", actual: " ~ temporalChrono.getId());
294         }
295     }
296 
297     //-----------------------------------------------------------------------
298     override
299     public bool opEquals(Object obj) {
300         if (this is obj) {
301             return true;
302         }
303         if (cast(ChronoPeriodImpl)(obj) !is null) {
304             ChronoPeriodImpl other = cast(ChronoPeriodImpl) obj;
305             return years == other.years && months == other.months &&
306                     days == other.days && chrono == (other.chrono);
307         }
308         return false;
309     }
310 
311     override
312     public size_t toHash() @trusted nothrow {
313         try{
314             return (years + Integer.rotateLeft(months, 8) + Integer.rotateLeft(days, 16)) ^ chrono.toHash();
315         }
316         catch(Exception e){}
317         return int.init;
318     }
319 
320     //-----------------------------------------------------------------------
321     override
322     public string toString() {
323         if (isZero()) {
324             return getChronology().toString() ~ " P0D";
325         } else {
326             StringBuilder buf = new StringBuilder();
327             buf.append(getChronology().toString()).append(' ').append('P');
328             if (years != 0) {
329                 buf.append(years).append('Y');
330             }
331             if (months != 0) {
332                 buf.append(months).append('M');
333             }
334             if (days != 0) {
335                 buf.append(days).append('D');
336             }
337             return buf.toString();
338         }
339     }
340 
341     //-----------------------------------------------------------------------
342     /**
343      * Writes the Chronology using a
344      * <a href="{@docRoot}/serialized-form.html#hunt.time.chrono.Ser">dedicated serialized form</a>.
345      * !(pre)
346      *  _out.writeByte(12);  // identifies this as a ChronoPeriodImpl
347      *  _out.writeUTF(getId());  // the chronology
348      *  _out.writeInt(years);
349      *  _out.writeInt(months);
350      *  _out.writeInt(days);
351      * </pre>
352      *
353      * @return the instance of {@code Ser}, not null
354      */
355     protected Object writeReplace() {
356         return new Ser(Ser.CHRONO_PERIOD_TYPE, this);
357     }
358 
359     /**
360      * Defend against malicious streams.
361      *
362      * @param s the stream to read
363      * @throws InvalidObjectException always
364      */
365      ///@gxc
366     // private void readObject(ObjectInputStream s) /*throws ObjectStreamException*/ {
367     //     throw new InvalidObjectException("Deserialization via serialization delegate");
368     // }
369 
370     void writeExternal(DataOutput _out) /*throws IOException*/ {
371         _out.writeUTF(chrono.getId());
372         _out.writeInt(years);
373         _out.writeInt(months);
374         _out.writeInt(days);
375     }
376 
377     static ChronoPeriodImpl readExternal(DataInput _in) /*throws IOException*/ {
378         Chronology chrono = Chronology.of(_in.readUTF());
379         int years = _in.readInt();
380         int months = _in.readInt();
381         int days = _in.readInt();
382         return new ChronoPeriodImpl(chrono, years, months, days);
383     }
384 
385     // mixin SerializationMember!(typeof(this));
386 }