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.ChronoLocalDateImpl;
13 
14 import hunt.time.temporal.ChronoField;
15 import hunt.Exceptions;
16 import hunt.stream.Common;
17 import hunt.time.Exceptions;
18 import hunt.time.temporal.ChronoUnit;
19 import hunt.time.temporal.Temporal;
20 import hunt.time.temporal.TemporalAdjuster;
21 import hunt.time.temporal.TemporalAmount;
22 import hunt.time.temporal.TemporalField;
23 import hunt.time.temporal.TemporalUnit;
24 import hunt.time.Exceptions;
25 import hunt.time.temporal.ValueRange;
26 import hunt.time.chrono.ChronoLocalDate;
27 import hunt.time.chrono.Chronology;
28 import hunt.Long;
29 import hunt.math.Helper;
30 import std.conv;
31 import hunt.util.StringBuilder;
32 /**
33  * A date expressed _in terms of a standard year-month-day calendar system.
34  * !(p)
35  * This class is used by applications seeking to handle dates _in non-ISO calendar systems.
36  * For example, the Japanese, Minguo, Thai Buddhist and others.
37  * !(p)
38  * {@code ChronoLocalDate} is built on the generic concepts of year, month and day.
39  * The calendar system, represented by a {@link hunt.time.chrono.Chronology}, expresses the relationship between
40  * the fields and this class allows the resulting date to be manipulated.
41  * !(p)
42  * Note that not all calendar systems are suitable for use with this class.
43  * For example, the Mayan calendar uses a system that bears no relation to years, months and days.
44  * !(p)
45  * The API design encourages the use of {@code LocalDate} for the majority of the application.
46  * This includes code to read and write from a persistent data store, such as a database,
47  * and to send dates and times across a network. The {@code ChronoLocalDate} instance is then used
48  * at the user interface level to deal with localized input/output.
49  *
50  * !(P)Example: </p>
51  * !(pre)
52  *        System._out.printf("Example()%n");
53  *        // Enumerate the list of available calendars and print today for each
54  *        Set&lt;Chronology&gt; chronos = Chronology.getAvailableChronologies();
55  *        foreach(Chronology chrono ; chronos) {
56  *            ChronoLocalDate date = chrono.dateNow();
57  *            System._out.printf("   %20s: %s%n", chrono.getID(), date.toString());
58  *        }
59  *
60  *        // Print the Hijrah date and calendar
61  *        ChronoLocalDate date = Chronology.of("Hijrah").dateNow();
62  *        int day = date.get(ChronoField.DAY_OF_MONTH);
63  *        int dow = date.get(ChronoField.DAY_OF_WEEK);
64  *        int month = date.get(ChronoField.MONTH_OF_YEAR);
65  *        int year = date.get(ChronoField.YEAR);
66  *        System._out.printf("  Today is %s %s %d-%s-%d%n", date.getChronology().getID(),
67  *                dow, day, month, year);
68  *
69  *        // Print today's date and the last day of the year
70  *        ChronoLocalDate now1 = Chronology.of("Hijrah").dateNow();
71  *        ChronoLocalDate first = now1._with(ChronoField.DAY_OF_MONTH, 1)
72  *                ._with(ChronoField.MONTH_OF_YEAR, 1);
73  *        ChronoLocalDate last = first.plus(1, ChronoUnit.YEARS)
74  *                .minus(1, ChronoUnit.DAYS);
75  *        System._out.printf("  Today is %s: start: %s; end: %s%n", last.getChronology().getID(),
76  *                first, last);
77  * </pre>
78  *
79  * !(h3)Adding Calendars</h3>
80  * !(p) The set of calendars is extensible by defining a subclass of {@link ChronoLocalDate}
81  * to represent a date instance and an implementation of {@code Chronology}
82  * to be the factory for the ChronoLocalDate subclass.
83  * </p>
84  * !(p) To permit the discovery of the additional calendar types the implementation of
85  * {@code Chronology} must be registered as a Service implementing the {@code Chronology} interface
86  * _in the {@code META-INF/Services} file as per the specification of {@link java.util.ServiceLoader}.
87  * The subclass must function according to the {@code Chronology} class description and must provide its
88  * {@link hunt.time.chrono.Chronology#getId() chronlogy ID} and {@link Chronology#getCalendarType() calendar type}. </p>
89  *
90  * @implSpec
91  * This abstract class must be implemented with care to ensure other classes operate correctly.
92  * All implementations that can be instantiated must be final, immutable and thread-safe.
93  * Subclasses should be Serializable wherever possible.
94  *
95  * @param !(D) the ChronoLocalDate of this date-time
96  * @since 1.8
97  */
98 abstract class ChronoLocalDateImpl(D = ChronoLocalDate) if(is(D : ChronoLocalDate))
99         : ChronoLocalDate, Temporal, TemporalAdjuster { //, Serializable
100 
101 
102     /**
103      * Casts the {@code Temporal} to {@code ChronoLocalDate} ensuring it bas the specified chronology.
104      *
105      * @param chrono  the chronology to check for, not null
106      * @param temporal  a date-time to cast, not null
107      * @return the date-time checked and cast to {@code ChronoLocalDate}, not null
108      * @throws ClassCastException if the date-time cannot be cast to ChronoLocalDate
109      *  or the chronology is not equal this Chronology
110      */
111     static  D ensureValid(D)(Chronology chrono, Temporal temporal) {
112         /*@SuppressWarnings("unchecked")*/
113         D other =  cast(D)temporal;
114         if ((chrono == other.getChronology()) == false) {
115             throw new ClassCastException("Chronology mismatch, expected: " ~ chrono.getId() ~ ", actual: " ~ other.getChronology().getId());
116         }
117         return other;
118     }
119 
120     //-----------------------------------------------------------------------
121     /**
122      * Creates an instance.
123      */
124     this() {
125     }
126 
127     override
128     /*@SuppressWarnings("unchecked")*/
129     public D _with(TemporalAdjuster adjuster) {
130         return cast(D) /* ChronoLocalDate. super.*/super_with(adjuster);
131     }
132      ChronoLocalDate super_with(TemporalAdjuster adjuster) {
133         return ChronoLocalDateImpl!D.ensureValid!D(getChronology(), adjuster.adjustInto(this));
134     }
135 
136     override
137     /*@SuppressWarnings("unchecked")*/
138     public D _with(TemporalField field, long value) {
139         return cast(D) /* ChronoLocalDate. super.*/super_with(field, value);
140     }
141      ChronoLocalDate super_with(TemporalField field, long newValue) {
142         if (cast(ChronoField)(field) !is null) {
143             throw new UnsupportedTemporalTypeException("Unsupported field: " ~ typeid(field).name);
144         }
145         return ChronoLocalDateImpl!D.ensureValid!D(getChronology(), field.adjustInto(this, newValue));
146     }
147 
148     //-----------------------------------------------------------------------
149     override
150     /*@SuppressWarnings("unchecked")*/
151     public D plus(TemporalAmount amount) {
152         return cast(D) /* ChronoLocalDate.super. */super_plus(amount);
153     }
154      ChronoLocalDate super_plus(TemporalAmount amount) {
155         return ChronoLocalDateImpl!D.ensureValid!D(getChronology(),amount.addTo(this));
156     }
157     //-----------------------------------------------------------------------
158     override
159     /*@SuppressWarnings("unchecked")*/
160     public D plus(long amountToAdd, TemporalUnit unit) {
161         if (cast(ChronoUnit)(unit) !is null) {
162             ChronoUnit f = cast(ChronoUnit) unit;
163             {
164                 if( f == ChronoUnit.DAYS) return plusDays(amountToAdd);
165                 if( f == ChronoUnit.WEEKS) return plusDays(MathHelper.multiplyExact(amountToAdd, 7));
166                 if( f == ChronoUnit.MONTHS) return plusMonths(amountToAdd);
167                 if( f == ChronoUnit.YEARS) return plusYears(amountToAdd);
168                 if( f == ChronoUnit.DECADES) return plusYears(MathHelper.multiplyExact(amountToAdd, 10));
169                 if( f == ChronoUnit.CENTURIES) return plusYears(MathHelper.multiplyExact(amountToAdd, 100));
170                 if( f == ChronoUnit.MILLENNIA) return plusYears(MathHelper.multiplyExact(amountToAdd, 1000));
171                 if( f == ChronoUnit.ERAS) return _with(ChronoField.ERA, MathHelper.addExact(getLong(ChronoField.ERA), amountToAdd));
172             }
173             throw new UnsupportedTemporalTypeException("Unsupported unit: " ~ f.toString);
174         }
175         return cast(D) /* ChronoLocalDate. super.*/super_plus(amountToAdd, unit);
176     }
177      ChronoLocalDate super_plus(long amountToAdd, TemporalUnit unit) {
178         if (cast(ChronoUnit)(unit) !is null) {
179             throw new UnsupportedTemporalTypeException("Unsupported unit: " ~ typeid(unit).name);
180         }
181         return ChronoLocalDateImpl!D.ensureValid!D(getChronology(), unit.addTo(this, amountToAdd));
182     }
183 
184     override
185     /*@SuppressWarnings("unchecked")*/
186     public D minus(TemporalAmount amount) {
187         return cast(D) /* ChronoLocalDate. */super_minus(amount);
188     }
189 
190      ChronoLocalDate super_minus(TemporalAmount amount) {
191         return ChronoLocalDateImpl!D.ensureValid!D(getChronology(), amount.subtractFrom(this));
192     }
193 
194     override
195     /*@SuppressWarnings("unchecked")*/
196     public D minus(long amountToSubtract, TemporalUnit unit) {
197         return cast(D) /* ChronoLocalDate. */super_minus(amountToSubtract, unit);
198     }
199      ChronoLocalDate super_minus(long amountToSubtract, TemporalUnit unit) {
200         return ChronoLocalDateImpl!D.ensureValid!D(getChronology(), (amountToSubtract == Long.MIN_VALUE ? plus(Long.MAX_VALUE, unit).plus(1, unit) : plus(-amountToSubtract, unit)));
201     }
202 
203     //-----------------------------------------------------------------------
204     /**
205      * Returns a copy of this date with the specified number of years added.
206      * !(p)
207      * This adds the specified period _in years to the date.
208      * In some cases, adding years can cause the resulting date to become invalid.
209      * If this occurs, then other fields, typically the day-of-month, will be adjusted to ensure
210      * that the result is valid. Typically this will select the last valid day of the month.
211      * !(p)
212      * This instance is immutable and unaffected by this method call.
213      *
214      * @param yearsToAdd  the years to add, may be negative
215      * @return a date based on this one with the years added, not null
216      * @throws DateTimeException if the result exceeds the supported date range
217      */
218     abstract D plusYears(long yearsToAdd);
219 
220     /**
221      * Returns a copy of this date with the specified number of months added.
222      * !(p)
223      * This adds the specified period _in months to the date.
224      * In some cases, adding months can cause the resulting date to become invalid.
225      * If this occurs, then other fields, typically the day-of-month, will be adjusted to ensure
226      * that the result is valid. Typically this will select the last valid day of the month.
227      * !(p)
228      * This instance is immutable and unaffected by this method call.
229      *
230      * @param monthsToAdd  the months to add, may be negative
231      * @return a date based on this one with the months added, not null
232      * @throws DateTimeException if the result exceeds the supported date range
233      */
234     abstract D plusMonths(long monthsToAdd);
235 
236     /**
237      * Returns a copy of this date with the specified number of weeks added.
238      * !(p)
239      * This adds the specified period _in weeks to the date.
240      * In some cases, adding weeks can cause the resulting date to become invalid.
241      * If this occurs, then other fields will be adjusted to ensure that the result is valid.
242      * !(p)
243      * The default implementation uses {@link #plusDays(long)} using a 7 day week.
244      * !(p)
245      * This instance is immutable and unaffected by this method call.
246      *
247      * @param weeksToAdd  the weeks to add, may be negative
248      * @return a date based on this one with the weeks added, not null
249      * @throws DateTimeException if the result exceeds the supported date range
250      */
251     D plusWeeks(long weeksToAdd) {
252         return plusDays(MathHelper.multiplyExact(weeksToAdd, 7));
253     }
254 
255     /**
256      * Returns a copy of this date with the specified number of days added.
257      * !(p)
258      * This adds the specified period _in days to the date.
259      * !(p)
260      * This instance is immutable and unaffected by this method call.
261      *
262      * @param daysToAdd  the days to add, may be negative
263      * @return a date based on this one with the days added, not null
264      * @throws DateTimeException if the result exceeds the supported date range
265      */
266     abstract D plusDays(long daysToAdd);
267 
268     //-----------------------------------------------------------------------
269     /**
270      * Returns a copy of this date with the specified number of years subtracted.
271      * !(p)
272      * This subtracts the specified period _in years to the date.
273      * In some cases, subtracting years can cause the resulting date to become invalid.
274      * If this occurs, then other fields, typically the day-of-month, will be adjusted to ensure
275      * that the result is valid. Typically this will select the last valid day of the month.
276      * !(p)
277      * The default implementation uses {@link #plusYears(long)}.
278      * !(p)
279      * This instance is immutable and unaffected by this method call.
280      *
281      * @param yearsToSubtract  the years to subtract, may be negative
282      * @return a date based on this one with the years subtracted, not null
283      * @throws DateTimeException if the result exceeds the supported date range
284      */
285     /*@SuppressWarnings("unchecked")*/
286     D minusYears(long yearsToSubtract) {
287         return (yearsToSubtract == Long.MIN_VALUE ? (cast(ChronoLocalDateImpl!(D))plusYears(Long.MAX_VALUE)).plusYears(1) : plusYears(-yearsToSubtract));
288     }
289 
290     /**
291      * Returns a copy of this date with the specified number of months subtracted.
292      * !(p)
293      * This subtracts the specified period _in months to the date.
294      * In some cases, subtracting months can cause the resulting date to become invalid.
295      * If this occurs, then other fields, typically the day-of-month, will be adjusted to ensure
296      * that the result is valid. Typically this will select the last valid day of the month.
297      * !(p)
298      * The default implementation uses {@link #plusMonths(long)}.
299      * !(p)
300      * This instance is immutable and unaffected by this method call.
301      *
302      * @param monthsToSubtract  the months to subtract, may be negative
303      * @return a date based on this one with the months subtracted, not null
304      * @throws DateTimeException if the result exceeds the supported date range
305      */
306     /*@SuppressWarnings("unchecked")*/
307     D minusMonths(long monthsToSubtract) {
308         return (monthsToSubtract == Long.MIN_VALUE ? (cast(ChronoLocalDateImpl!(D))plusMonths(Long.MAX_VALUE)).plusMonths(1) : plusMonths(-monthsToSubtract));
309     }
310 
311     /**
312      * Returns a copy of this date with the specified number of weeks subtracted.
313      * !(p)
314      * This subtracts the specified period _in weeks to the date.
315      * In some cases, subtracting weeks can cause the resulting date to become invalid.
316      * If this occurs, then other fields will be adjusted to ensure that the result is valid.
317      * !(p)
318      * The default implementation uses {@link #plusWeeks(long)}.
319      * !(p)
320      * This instance is immutable and unaffected by this method call.
321      *
322      * @param weeksToSubtract  the weeks to subtract, may be negative
323      * @return a date based on this one with the weeks subtracted, not null
324      * @throws DateTimeException if the result exceeds the supported date range
325      */
326     /*@SuppressWarnings("unchecked")*/
327     D minusWeeks(long weeksToSubtract) {
328         return (weeksToSubtract == Long.MIN_VALUE ? (cast(ChronoLocalDateImpl!(D))plusWeeks(Long.MAX_VALUE)).plusWeeks(1) : plusWeeks(-weeksToSubtract));
329     }
330 
331     /**
332      * Returns a copy of this date with the specified number of days subtracted.
333      * !(p)
334      * This subtracts the specified period _in days to the date.
335      * !(p)
336      * The default implementation uses {@link #plusDays(long)}.
337      * !(p)
338      * This instance is immutable and unaffected by this method call.
339      *
340      * @param daysToSubtract  the days to subtract, may be negative
341      * @return a date based on this one with the days subtracted, not null
342      * @throws DateTimeException if the result exceeds the supported date range
343      */
344     /*@SuppressWarnings("unchecked")*/
345     D minusDays(long daysToSubtract) {
346         return (daysToSubtract == Long.MIN_VALUE ? (cast(ChronoLocalDateImpl!(D))plusDays(Long.MAX_VALUE)).plusDays(1) : plusDays(-daysToSubtract));
347     }
348 
349     //-----------------------------------------------------------------------
350     override
351     public long until(Temporal endExclusive, TemporalUnit unit) {
352         assert(endExclusive, "endExclusive");
353         ChronoLocalDate end = getChronology().date(endExclusive);
354         if (cast(ChronoUnit)(unit) !is null) {
355             auto f = cast(ChronoUnit) unit;
356             {
357                 if( f == ChronoUnit.DAYS) return daysUntil(end);
358                 if( f == ChronoUnit.WEEKS) return daysUntil(end) / 7;
359                 if( f == ChronoUnit.MONTHS) return monthsUntil(end);
360                 if( f == ChronoUnit.YEARS) return monthsUntil(end) / 12;
361                 if( f == ChronoUnit.DECADES) return monthsUntil(end) / 120;
362                 if( f == ChronoUnit.CENTURIES) return monthsUntil(end) / 1200;
363                 if( f == ChronoUnit.MILLENNIA) return monthsUntil(end) / 12000;
364                 if( f == ChronoUnit.ERAS) return end.getLong(ChronoField.ERA) - getLong(ChronoField.ERA);
365             }
366             throw new UnsupportedTemporalTypeException("Unsupported unit: " ~ f.toString);
367         }
368         assert(unit, "unit");
369         return unit.between(this, end);
370     }
371 
372     private long daysUntil(ChronoLocalDate end) {
373         return end.toEpochDay() - toEpochDay();  // no overflow
374     }
375 
376     private long monthsUntil(ChronoLocalDate end) {
377         ValueRange range = getChronology().range(ChronoField.MONTH_OF_YEAR);
378         if (range.getMaximum() != 12) {
379             throw new IllegalStateException("ChronoLocalDateImpl only supports Chronologies with 12 months per year");
380         }
381         long packed1 = getLong(ChronoField.PROLEPTIC_MONTH) * 32L + get(ChronoField.DAY_OF_MONTH);  // no overflow
382         long packed2 = end.getLong(ChronoField.PROLEPTIC_MONTH) * 32L + end.get(ChronoField.DAY_OF_MONTH);  // no overflow
383         return (packed2 - packed1) / 32;
384     }
385 
386     override
387     public bool opEquals(Object obj) {
388         if (this is obj) {
389             return true;
390         }
391         if (cast(ChronoLocalDate)(obj) !is null) {
392             return compareTo(cast(ChronoLocalDate) obj) == 0;
393         }
394         return false;
395     }
396 
397     override
398     public size_t toHash() @trusted nothrow {
399         try{
400             long epDay = toEpochDay();
401             return getChronology().toHash() ^ (cast(int) (epDay ^ (epDay >>> 32)));
402         }catch(Exception e){}
403         return int.init;
404     }
405 
406     override
407     public string toString() {
408         // getLong() reduces chances of exceptions _in toString()
409         long yoe = getLong(ChronoField.YEAR_OF_ERA);
410         long moy = getLong(ChronoField.MONTH_OF_YEAR);
411         long dom = getLong(ChronoField.DAY_OF_MONTH);
412         StringBuilder buf = new StringBuilder(30);
413         buf.append(getChronology().toString())
414                 .append(" ")
415                 .append(typeid(getEra()).name) ///@gxc
416                 .append(" ")
417                 .append(yoe)
418                 .append(moy < 10 ? "-0" : "-").append(moy)
419                 .append(dom < 10 ? "-0" : "-").append(dom);
420         return buf.toString();
421     }
422 
423 }