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.AbstractChronology;
13 
14 import hunt.time.temporal.ChronoField;
15 import hunt.time.temporal.ChronoUnit;
16 import hunt.time.temporal.TemporalAdjusters;
17 
18 import hunt.stream.DataInput;
19 import hunt.stream.DataOutput;
20 
21 //import hunt.io.ObjectInputStream;
22 // import hunt.io.ObjectStreamException;
23 import hunt.stream.Common;
24 import hunt.time.Exceptions;
25 import hunt.time.DayOfWeek;
26 import hunt.time.format.ResolverStyle;
27 import hunt.time.temporal.ChronoField;
28 import hunt.time.temporal.TemporalAdjusters;
29 import hunt.time.temporal.TemporalField;
30 import hunt.time.temporal.ValueRange;
31 import hunt.time.util;
32 
33 import hunt.time.chrono.IsoChronology;
34 import hunt.time.chrono.Era;
35 import hunt.time.chrono.Ser;
36 import hunt.time.util.Common;
37 import hunt.time.util.ServiceLoader;
38 import hunt.time.chrono.Chronology;
39 import hunt.time.chrono.ChronoLocalDate;
40 
41 import hunt.collection.HashSet;
42 import hunt.collection.List;
43 import hunt.collection.Map;
44 import hunt.collection.Set;
45 import hunt.collection.HashMap;
46 import hunt.Exceptions;
47 import hunt.Long;
48 import hunt.logging;
49 import hunt.math.Helper;
50 import hunt.util.Comparator;
51 import hunt.util.Locale;
52 
53 import std.conv;
54 import std.concurrency : initOnce;
55 
56 public abstract class AbstractChronology : Chronology {
57 
58     /**
59      * Map of available calendars by ID.
60      */
61     private static HashMap!(string, Chronology) CHRONOS_BY_ID() {
62         __gshared HashMap!(string, Chronology) _d;
63         return initOnce!(_d)(new HashMap!(string, Chronology)()); // = new ConcurrentHashMap!(string, Chronology)();
64     }   
65 
66     /**
67      * Map of available calendars by calendar type.
68      */
69     static HashMap!(string, Chronology) CHRONOS_BY_TYPE() {
70         __gshared HashMap!(string, Chronology) _d;
71         return initOnce!(_d)(new HashMap!(string, Chronology)()); // = new ConcurrentHashMap!(string, Chronology)();
72     }   
73 
74     /**
75      * Register a Chronology by its ID and type for lookup by {@link #of(string)}.
76      * Chronologies must not be registered until they are completely constructed.
77      * Specifically, not _in the constructor of Chronology.
78      *
79      * @param chrono the chronology to register; not null
80      * @return the already registered Chronology if any, may be null
81      */
82     static Chronology registerChrono(Chronology chrono) {
83         return registerChrono(chrono, chrono.getId());
84     }
85 
86     /**
87      * Register a Chronology by ID and type for lookup by {@link #of(string)}.
88      * Chronos must not be registered until they are completely constructed.
89      * Specifically, not _in the constructor of Chronology.
90      *
91      * @param chrono the chronology to register; not null
92      * @param id the ID to register the chronology; not null
93      * @return the already registered Chronology if any, may be null
94      */
95     static Chronology registerChrono(Chronology chrono, string id) {
96         Chronology prev = CHRONOS_BY_ID.putIfAbsent(id, chrono);
97         if (prev is null) {
98             string type = chrono.getCalendarType();
99             if (type !is null) {
100                 CHRONOS_BY_TYPE.putIfAbsent(type, chrono);
101             }
102         }
103         return prev;
104     }
105 
106     /**
107      * Initialization of the maps from id and type to Chronology.
108      * The ServiceLoader is used to find and register any implementations
109      * of {@link hunt.time.chrono.AbstractChronology} found _in the bootclass loader.
110      * The built-_in chronologies are registered explicitly.
111      * Calendars configured via the Thread's context classloader are local
112      * to that thread and are ignored.
113      * <p>
114      * The initialization is done only once using the registration
115      * of the IsoChronology as the test and the final step.
116      * Multiple threads may perform the initialization concurrently.
117      * Only the first registration of each Chronology is retained by the
118      * ConcurrentHashMap.
119      * @return true if the cache was initialized
120      */
121     private static bool initCache() {
122         if (CHRONOS_BY_ID.get("ISO") is null) {
123             // Initialization is incomplete
124 
125             // Register built-_in Chronologies
126             ///@gxc
127             // registerChrono(HijrahChronology.INSTANCE);
128             // registerChrono(JapaneseChronology.INSTANCE);
129             // registerChrono(MinguoChronology.INSTANCE);
130             // registerChrono(ThaiBuddhistChronology.INSTANCE);
131 
132             // Register Chronologies from the ServiceLoader
133             // @SuppressWarnings("rawtypes")
134             ServiceLoader!(AbstractChronology) loader;
135             foreach( obj ; loader.objs) {
136                 AbstractChronology chrono = obj.ctor();
137                 string id = chrono.getId();
138                 if (id == ("ISO") || registerChrono(chrono) !is null) {
139                     // Log the attempt to replace an existing Chronology
140                     // PlatformLogger logger = PlatformLogger.getLogger("hunt.time.chrono");
141                     version(HUNT_DEBUG) trace("Ignoring duplicate Chronology, from ServiceLoader configuration "  ~ id);
142                 }
143             }
144 
145             // finally, register IsoChronology to mark initialization is complete
146             registerChrono(IsoChronology.INSTANCE);
147             return true;
148         }
149         return false;
150     }
151 
152     //-----------------------------------------------------------------------
153     /**
154      * Obtains an instance of {@code Chronology} from a locale.
155      * !(p)
156      * See {@link Chronology#ofLocale(Locale)}.
157      *
158      * @param locale  the locale to use to obtain the calendar system, not null
159      * @return the calendar system associated with the locale, not null
160      * @throws hunt.time.Exceptions if the locale-specified calendar cannot be found
161      */
162     static Chronology ofLocale(Locale locale) {
163         assert(locale, "locale");
164         string type = locale.getUnicodeLocaleType("ca");
165         if (type is null || "iso" == (type) || "iso8601" == (type)) {
166             return IsoChronology.INSTANCE;
167         }
168         // Not pre-defined; lookup by the type
169         do {
170             Chronology chrono = CHRONOS_BY_TYPE.get(type);
171             if (chrono !is null) {
172                 return chrono;
173             }
174             // If not found, do the initialization (once) and repeat the lookup
175         } while (initCache());
176 
177         // Look for a Chronology using ServiceLoader of the Thread's ContextClassLoader
178         // Application provided Chronologies must not be cached
179         // @SuppressWarnings("rawtypes")
180         ServiceLoader!(AbstractChronology) loader;
181         foreach( obj ; loader.objs) {
182             Chronology chrono  = obj.ctor();
183             if (type == (chrono.getCalendarType())) {
184                 return chrono;
185             }
186         }
187         throw new DateTimeException("Unknown calendar system: " ~ type);
188     }
189 
190     //-----------------------------------------------------------------------
191     /**
192      * Obtains an instance of {@code Chronology} from a chronology ID or
193      * calendar system type.
194      * !(p)
195      * See {@link Chronology#of(string)}.
196      *
197      * @param id  the chronology ID or calendar system type, not null
198      * @return the chronology with the identifier requested, not null
199      * @throws hunt.time.Exceptions if the chronology cannot be found
200      */
201     static Chronology of(string id) {
202         assert(id, "id");
203         do {
204             Chronology chrono = of0(id);
205             if (chrono !is null) {
206                 return chrono;
207             }
208             // If not found, do the initialization (once) and repeat the lookup
209         } while (initCache());
210 
211         // Look for a Chronology using ServiceLoader of the Thread's ContextClassLoader
212         // Application provided Chronologies must not be cached
213         // @SuppressWarnings("rawtypes")
214         ServiceLoader!(AbstractChronology) loader;
215         foreach(  obj ; loader.objs) {
216             Chronology chrono = cast(Chronology)(obj.ctor());
217             if (id == (chrono.getId()) || id == (chrono.getCalendarType())) {
218                 return chrono;
219             }
220         }
221         throw new DateTimeException("Unknown chronology: " ~ id);
222     }
223 
224     /**
225      * Obtains an instance of {@code Chronology} from a chronology ID or
226      * calendar system type.
227      *
228      * @param id  the chronology ID or calendar system type, not null
229      * @return the chronology with the identifier requested, or {@code null} if not found
230      */
231     private static Chronology of0(string id) {
232         Chronology chrono = CHRONOS_BY_ID.get(id);
233         if (chrono is null) {
234             chrono = CHRONOS_BY_TYPE.get(id);
235         }
236         return chrono;
237     }
238 
239     /**
240      * Returns the available chronologies.
241      * !(p)
242      * Each returned {@code Chronology} is available for use _in the system.
243      * The set of chronologies includes the system chronologies and
244      * any chronologies provided by the application via ServiceLoader
245      * configuration.
246      *
247      * @return the independent, modifiable set of the available chronology IDs, not null
248      */
249     static Set!(Chronology) getAvailableChronologies() {
250         initCache();       // force initialization
251         HashSet!(Chronology) chronos = new HashSet!(Chronology)();
252         foreach( value ;CHRONOS_BY_ID.values())
253         {
254             chronos.add(value);
255         }
256 
257         /// Add _in Chronologies from the ServiceLoader configuration
258         // @SuppressWarnings("rawtypes")
259         ServiceLoader!(AbstractChronology) loader;
260         foreach(  obj ; loader.objs) {
261             Chronology chrono = obj.ctor();
262             chronos.add(chrono);
263         }
264         return chronos;
265     }
266 
267     //-----------------------------------------------------------------------
268     /**
269      * Creates an instance.
270      */
271     protected this() {
272     }
273 
274     //-----------------------------------------------------------------------
275     /**
276      * Resolves parsed {@code ChronoField} values into a date during parsing.
277      * <p>
278      * Most {@code TemporalField} implementations are resolved using the
279      * resolve method on the field. By contrast, the {@code ChronoField} class
280      * defines fields that only have meaning relative to the chronology.
281      * As such, {@code ChronoField} date fields are resolved here _in the
282      * context of a specific chronology.
283      * <p>
284      * {@code ChronoField} instances are resolved by this method, which may
285      * be overridden _in subclasses.
286      * <ul>
287      * <li>{@code EPOCH_DAY} - If present, this is converted to a date and
288      *  all other date fields are then cross-checked against the date.
289      * <li>{@code PROLEPTIC_MONTH} - If present, then it is split into the
290      *  {@code YEAR} and {@code MONTH_OF_YEAR}. If the mode is strict or smart
291      *  then the field is validated.
292      * <li>{@code YEAR_OF_ERA} and {@code ERA} - If both are present, then they
293      *  are combined to form a {@code YEAR}. In lenient mode, the {@code YEAR_OF_ERA}
294      *  range is not validated, _in smart and strict mode it is. The {@code ERA} is
295      *  validated for range _in all three modes. If only the {@code YEAR_OF_ERA} is
296      *  present, and the mode is smart or lenient, then the last available era
297      *  is assumed. In strict mode, no era is assumed and the {@code YEAR_OF_ERA} is
298      *  left untouched. If only the {@code ERA} is present, then it is left untouched.
299      * <li>{@code YEAR}, {@code MONTH_OF_YEAR} and {@code DAY_OF_MONTH} -
300      *  If all three are present, then they are combined to form a date.
301      *  In all three modes, the {@code YEAR} is validated.
302      *  If the mode is smart or strict, then the month and day are validated.
303      *  If the mode is lenient, then the date is combined _in a manner equivalent to
304      *  creating a date on the first day of the first month _in the requested year,
305      *  then adding the difference _in months, then the difference _in days.
306      *  If the mode is smart, and the day-of-month is greater than the maximum for
307      *  the year-month, then the day-of-month is adjusted to the last day-of-month.
308      *  If the mode is strict, then the three fields must form a valid date.
309      * <li>{@code YEAR} and {@code DAY_OF_YEAR} -
310      *  If both are present, then they are combined to form a date.
311      *  In all three modes, the {@code YEAR} is validated.
312      *  If the mode is lenient, then the date is combined _in a manner equivalent to
313      *  creating a date on the first day of the requested year, then adding
314      *  the difference _in days.
315      *  If the mode is smart or strict, then the two fields must form a valid date.
316      * <li>{@code YEAR}, {@code MONTH_OF_YEAR}, {@code ALIGNED_WEEK_OF_MONTH} and
317      *  {@code ALIGNED_DAY_OF_WEEK_IN_MONTH} -
318      *  If all four are present, then they are combined to form a date.
319      *  In all three modes, the {@code YEAR} is validated.
320      *  If the mode is lenient, then the date is combined _in a manner equivalent to
321      *  creating a date on the first day of the first month _in the requested year, then adding
322      *  the difference _in months, then the difference _in weeks, then _in days.
323      *  If the mode is smart or strict, then the all four fields are validated to
324      *  their outer ranges. The date is then combined _in a manner equivalent to
325      *  creating a date on the first day of the requested year and month, then adding
326      *  the amount _in weeks and days to reach their values. If the mode is strict,
327      *  the date is additionally validated to check that the day and week adjustment
328      *  did not change the month.
329      * <li>{@code YEAR}, {@code MONTH_OF_YEAR}, {@code ALIGNED_WEEK_OF_MONTH} and
330      *  {@code DAY_OF_WEEK} - If all four are present, then they are combined to
331      *  form a date. The approach is the same as described above for
332      *  years, months and weeks _in {@code ALIGNED_DAY_OF_WEEK_IN_MONTH}.
333      *  The day-of-week is adjusted as the next or same matching day-of-week once
334      *  the years, months and weeks have been handled.
335      * <li>{@code YEAR}, {@code ALIGNED_WEEK_OF_YEAR} and {@code ALIGNED_DAY_OF_WEEK_IN_YEAR} -
336      *  If all three are present, then they are combined to form a date.
337      *  In all three modes, the {@code YEAR} is validated.
338      *  If the mode is lenient, then the date is combined _in a manner equivalent to
339      *  creating a date on the first day of the requested year, then adding
340      *  the difference _in weeks, then _in days.
341      *  If the mode is smart or strict, then the all three fields are validated to
342      *  their outer ranges. The date is then combined _in a manner equivalent to
343      *  creating a date on the first day of the requested year, then adding
344      *  the amount _in weeks and days to reach their values. If the mode is strict,
345      *  the date is additionally validated to check that the day and week adjustment
346      *  did not change the year.
347      * <li>{@code YEAR}, {@code ALIGNED_WEEK_OF_YEAR} and {@code DAY_OF_WEEK} -
348      *  If all three are present, then they are combined to form a date.
349      *  The approach is the same as described above for years and weeks _in
350      *  {@code ALIGNED_DAY_OF_WEEK_IN_YEAR}. The day-of-week is adjusted as the
351      *  next or same matching day-of-week once the years and weeks have been handled.
352      * </ul>
353      * <p>
354      * The default implementation is suitable for most calendar systems.
355      * If {@link hunt.time.temporal.ChronoField#YEAR_OF_ERA} is found without an {@link hunt.time.temporal.ChronoField#ERA}
356      * then the last era _in {@link #eras()} is used.
357      * The implementation assumes a 7 day week, that the first day-of-month
358      * has the value 1, that first day-of-year has the value 1, and that the
359      * first of the month and year always exists.
360      *
361      * @param fieldValues  the map of fields to values, which can be updated, not null
362      * @param resolverStyle  the requested type of resolve, not null
363      * @return the resolved date, null if insufficient information to create a date
364      * @throws hunt.time.Exceptions if the date cannot be resolved, typically
365      *  because of a conflict _in the input data
366      */
367     override
368     public ChronoLocalDate resolveDate(Map!(TemporalField, Long) fieldValues, ResolverStyle resolverStyle) {
369         // check epoch-day before inventing era
370         if (fieldValues.containsKey(ChronoField.EPOCH_DAY)) {
371             return dateEpochDay(fieldValues.remove(ChronoField.EPOCH_DAY).longValue());
372         }
373 
374         // fix proleptic month before inventing era
375         resolveProlepticMonth(fieldValues, resolverStyle);
376 
377         // invent era if necessary to resolve year-of-era
378         ChronoLocalDate resolved = resolveYearOfEra(fieldValues, resolverStyle);
379         if (resolved !is null) {
380             return resolved;
381         }
382 
383         // build date
384         if (fieldValues.containsKey(ChronoField.YEAR)) {
385             if (fieldValues.containsKey(ChronoField.MONTH_OF_YEAR)) {
386                 if (fieldValues.containsKey(ChronoField.DAY_OF_MONTH)) {
387                     return resolveYMD(fieldValues, resolverStyle);
388                 }
389                 if (fieldValues.containsKey(ChronoField.ALIGNED_WEEK_OF_MONTH)) {
390                     if (fieldValues.containsKey(ChronoField.ALIGNED_DAY_OF_WEEK_IN_MONTH)) {
391                         return resolveYMAA(fieldValues, resolverStyle);
392                     }
393                     if (fieldValues.containsKey(ChronoField.DAY_OF_WEEK)) {
394                         return resolveYMAD(fieldValues, resolverStyle);
395                     }
396                 }
397             }
398             if (fieldValues.containsKey(ChronoField.DAY_OF_YEAR)) {
399                 return resolveYD(fieldValues, resolverStyle);
400             }
401             if (fieldValues.containsKey(ChronoField.ALIGNED_WEEK_OF_YEAR)) {
402                 if (fieldValues.containsKey(ChronoField.ALIGNED_DAY_OF_WEEK_IN_YEAR)) {
403                     return resolveYAA(fieldValues, resolverStyle);
404                 }
405                 if (fieldValues.containsKey(ChronoField.DAY_OF_WEEK)) {
406                     return resolveYAD(fieldValues, resolverStyle);
407                 }
408             }
409         }
410         return null;
411     }
412 
413     void resolveProlepticMonth(Map!(TemporalField, Long) fieldValues, ResolverStyle resolverStyle) {
414         Long pMonth = fieldValues.remove(ChronoField.PROLEPTIC_MONTH);
415         if (pMonth !is null) {
416             if (resolverStyle != ResolverStyle.LENIENT) {
417                 ChronoField.PROLEPTIC_MONTH.checkValidValue(pMonth.longValue());
418             }
419             // first day-of-month is likely to be safest for setting proleptic-month
420             // cannot add to year zero, as not all chronologies have a year zero
421             ChronoLocalDate chronoDate = dateNow()
422                     ._with(ChronoField.DAY_OF_MONTH, 1)._with(ChronoField.PROLEPTIC_MONTH, pMonth.longValue());
423             addFieldValue(fieldValues, ChronoField.MONTH_OF_YEAR, chronoDate.get(ChronoField.MONTH_OF_YEAR));
424             addFieldValue(fieldValues, ChronoField.YEAR, chronoDate.get(ChronoField.YEAR));
425         }
426     }
427 
428     ChronoLocalDate resolveYearOfEra(Map!(TemporalField, Long) fieldValues, ResolverStyle resolverStyle) {
429         Long yoeLong = fieldValues.remove(ChronoField.YEAR_OF_ERA);
430         if (yoeLong !is null) {
431             Long eraLong = fieldValues.remove(ChronoField.ERA);
432             int yoe;
433             if (resolverStyle != ResolverStyle.LENIENT) {
434                 yoe = range(ChronoField.YEAR_OF_ERA).checkValidIntValue(yoeLong.longValue(), ChronoField.YEAR_OF_ERA);
435             } else {
436                 yoe = MathHelper.toIntExact(yoeLong.longValue());
437             }
438             if (eraLong !is null) {
439                 Era eraObj = eraOf(range(ChronoField.ERA).checkValidIntValue(eraLong.longValue(), ChronoField.ERA));
440                 addFieldValue(fieldValues, ChronoField.YEAR, prolepticYear(eraObj, yoe));
441             } else {
442                 if (fieldValues.containsKey(ChronoField.YEAR)) {
443                     int year = range(ChronoField.YEAR).checkValidIntValue(fieldValues.get(ChronoField.YEAR).longValue(), ChronoField.YEAR);
444                     ChronoLocalDate chronoDate = dateYearDay(year, 1);
445                     addFieldValue(fieldValues, ChronoField.YEAR, prolepticYear(chronoDate.getEra(), yoe));
446                 } else if (resolverStyle == ResolverStyle.STRICT) {
447                     // do not invent era if strict
448                     // reinstate the field removed earlier, no cross-check issues
449                     fieldValues.put(ChronoField.YEAR_OF_ERA, yoeLong);
450                 } else {
451                     List!(Era) eras = eras();
452                     if (eras.isEmpty()) {
453                         addFieldValue(fieldValues, ChronoField.YEAR, yoe);
454                     } else {
455                         Era eraObj = eras.get(eras.size() - 1);
456                         addFieldValue(fieldValues, ChronoField.YEAR, prolepticYear(eraObj, yoe));
457                     }
458                 }
459             }
460         } else if (fieldValues.containsKey(ChronoField.ERA)) {
461             range(ChronoField.ERA).checkValidValue(fieldValues.get(ChronoField.ERA).longValue(), ChronoField.ERA);  // always validated
462         }
463         return null;
464     }
465 
466     ChronoLocalDate resolveYMD(Map!(TemporalField, Long) fieldValues, ResolverStyle resolverStyle) {
467         int y = range(ChronoField.YEAR).checkValidIntValue(fieldValues.remove(ChronoField.YEAR).longValue(), ChronoField.YEAR);
468         if (resolverStyle == ResolverStyle.LENIENT) {
469             long months = MathHelper.subtractExact(fieldValues.remove(ChronoField.MONTH_OF_YEAR).longValue(), 1);
470             long days = MathHelper.subtractExact(fieldValues.remove(ChronoField.DAY_OF_MONTH).longValue(), 1);
471             return date(y, 1, 1).plus(months, ChronoUnit.MONTHS).plus(days, ChronoUnit.DAYS);
472         }
473         int moy = range(ChronoField.MONTH_OF_YEAR).checkValidIntValue(fieldValues.remove(ChronoField.MONTH_OF_YEAR).longValue(), ChronoField.MONTH_OF_YEAR);
474         ValueRange domRange = range(ChronoField.DAY_OF_MONTH);
475         int dom = domRange.checkValidIntValue(fieldValues.remove(ChronoField.DAY_OF_MONTH).longValue(), ChronoField.DAY_OF_MONTH);
476         if (resolverStyle == ResolverStyle.SMART) {  // previous valid
477             try {
478                 return date(y, moy, dom);
479             } catch (DateTimeException ex) {
480                 return date(y, moy, 1)._with(TemporalAdjusters.lastDayOfMonth());
481             }
482         }
483         return date(y, moy, dom);
484     }
485 
486     ChronoLocalDate resolveYD(Map!(TemporalField, Long) fieldValues, ResolverStyle resolverStyle) {
487         int y = range(ChronoField.YEAR).checkValidIntValue(fieldValues.remove(ChronoField.YEAR).longValue(), ChronoField.YEAR);
488         if (resolverStyle == ResolverStyle.LENIENT) {
489             long days = MathHelper.subtractExact(fieldValues.remove(ChronoField.DAY_OF_YEAR).longValue(), 1);
490             return dateYearDay(y, 1).plus(days, ChronoUnit.DAYS);
491         }
492         int doy = range(ChronoField.DAY_OF_YEAR).checkValidIntValue(fieldValues.remove(ChronoField.DAY_OF_YEAR).longValue(), ChronoField.DAY_OF_YEAR);
493         return dateYearDay(y, doy);  // smart is same as strict
494     }
495 
496     ChronoLocalDate resolveYMAA(Map!(TemporalField, Long) fieldValues, ResolverStyle resolverStyle) {
497         int y = range(ChronoField.YEAR).checkValidIntValue(fieldValues.remove(ChronoField.YEAR).longValue(), ChronoField.YEAR);
498         if (resolverStyle == ResolverStyle.LENIENT) {
499             long months = MathHelper.subtractExact(fieldValues.remove(ChronoField.MONTH_OF_YEAR).longValue(), 1);
500             long weeks = MathHelper.subtractExact(fieldValues.remove(ChronoField.ALIGNED_WEEK_OF_MONTH).longValue(), 1);
501             long days = MathHelper.subtractExact(fieldValues.remove(ChronoField.ALIGNED_DAY_OF_WEEK_IN_MONTH).longValue(), 1);
502             return date(y, 1, 1).plus(months, ChronoUnit.MONTHS).plus(weeks, ChronoUnit.WEEKS).plus(days, ChronoUnit.DAYS);
503         }
504         int moy = range(ChronoField.MONTH_OF_YEAR).checkValidIntValue(fieldValues.remove(ChronoField.MONTH_OF_YEAR).longValue(), ChronoField.MONTH_OF_YEAR);
505         int aw = range(ChronoField.ALIGNED_WEEK_OF_MONTH).checkValidIntValue(fieldValues.remove(ChronoField.ALIGNED_WEEK_OF_MONTH).longValue(), ChronoField.ALIGNED_WEEK_OF_MONTH);
506         int ad = range(ChronoField.ALIGNED_DAY_OF_WEEK_IN_MONTH).checkValidIntValue(fieldValues.remove(ChronoField.ALIGNED_DAY_OF_WEEK_IN_MONTH).longValue(), ChronoField.ALIGNED_DAY_OF_WEEK_IN_MONTH);
507         ChronoLocalDate date = date(y, moy, 1).plus((aw - 1) * 7 + (ad - 1), ChronoUnit.DAYS);
508         if (resolverStyle == ResolverStyle.STRICT && date.get(ChronoField.MONTH_OF_YEAR) != moy) {
509             throw new DateTimeException("Strict mode rejected resolved date as it is _in a different month");
510         }
511         return date;
512     }
513 
514     ChronoLocalDate resolveYMAD(Map!(TemporalField, Long) fieldValues, ResolverStyle resolverStyle) {
515         int y = range(ChronoField.YEAR).checkValidIntValue(fieldValues.remove(ChronoField.YEAR).longValue(), ChronoField.YEAR);
516         if (resolverStyle == ResolverStyle.LENIENT) {
517             long months = MathHelper.subtractExact(fieldValues.remove(ChronoField.MONTH_OF_YEAR).longValue(), 1);
518             long weeks = MathHelper.subtractExact(fieldValues.remove(ChronoField.ALIGNED_WEEK_OF_MONTH).longValue(), 1);
519             long dow = MathHelper.subtractExact(fieldValues.remove(ChronoField.DAY_OF_WEEK).longValue(), 1);
520             return resolveAligned(date(y, 1, 1), months, weeks, dow);
521         }
522         int moy = range(ChronoField.MONTH_OF_YEAR).checkValidIntValue(fieldValues.remove(ChronoField.MONTH_OF_YEAR).longValue(), ChronoField.MONTH_OF_YEAR);
523         int aw = range(ChronoField.ALIGNED_WEEK_OF_MONTH).checkValidIntValue(fieldValues.remove(ChronoField.ALIGNED_WEEK_OF_MONTH).longValue(), ChronoField.ALIGNED_WEEK_OF_MONTH);
524         int dow = range(ChronoField.DAY_OF_WEEK).checkValidIntValue(fieldValues.remove(ChronoField.DAY_OF_WEEK).longValue(), ChronoField.DAY_OF_WEEK);
525         ChronoLocalDate date = date(y, moy, 1).plus((aw - 1) * 7, ChronoUnit.DAYS)._with(TemporalAdjusters.nextOrSame(DayOfWeek.of(dow)));
526         if (resolverStyle == ResolverStyle.STRICT && date.get(ChronoField.MONTH_OF_YEAR) != moy) {
527             throw new DateTimeException("Strict mode rejected resolved date as it is _in a different month");
528         }
529         return date;
530     }
531 
532     ChronoLocalDate resolveYAA(Map!(TemporalField, Long) fieldValues, ResolverStyle resolverStyle) {
533         int y = range(ChronoField.YEAR).checkValidIntValue(fieldValues.remove(ChronoField.YEAR).longValue(), ChronoField.YEAR);
534         if (resolverStyle == ResolverStyle.LENIENT) {
535             long weeks = MathHelper.subtractExact(fieldValues.remove(ChronoField.ALIGNED_WEEK_OF_YEAR).longValue(), 1);
536             long days = MathHelper.subtractExact(fieldValues.remove(ChronoField.ALIGNED_DAY_OF_WEEK_IN_YEAR).longValue(), 1);
537             return dateYearDay(y, 1).plus(weeks, ChronoUnit.WEEKS).plus(days, ChronoUnit.DAYS);
538         }
539         int aw = range(ChronoField.ALIGNED_WEEK_OF_YEAR).checkValidIntValue(fieldValues.remove(ChronoField.ALIGNED_WEEK_OF_YEAR).longValue(), ChronoField.ALIGNED_WEEK_OF_YEAR);
540         int ad = range(ChronoField.ALIGNED_DAY_OF_WEEK_IN_YEAR).checkValidIntValue(fieldValues.remove(ChronoField.ALIGNED_DAY_OF_WEEK_IN_YEAR).longValue(), ChronoField.ALIGNED_DAY_OF_WEEK_IN_YEAR);
541         ChronoLocalDate date = dateYearDay(y, 1).plus((aw - 1) * 7 + (ad - 1), ChronoUnit.DAYS);
542         if (resolverStyle == ResolverStyle.STRICT && date.get(ChronoField.YEAR) != y) {
543             throw new DateTimeException("Strict mode rejected resolved date as it is _in a different year");
544         }
545         return date;
546     }
547 
548     ChronoLocalDate resolveYAD(Map!(TemporalField, Long) fieldValues, ResolverStyle resolverStyle) {
549         int y = range(ChronoField.YEAR).checkValidIntValue(fieldValues.remove(ChronoField.YEAR).longValue(), ChronoField.YEAR);
550         if (resolverStyle == ResolverStyle.LENIENT) {
551             long weeks = MathHelper.subtractExact(fieldValues.remove(ChronoField.ALIGNED_WEEK_OF_YEAR).longValue(), 1);
552             long dow = MathHelper.subtractExact(fieldValues.remove(ChronoField.DAY_OF_WEEK).longValue(), 1);
553             return resolveAligned(dateYearDay(y, 1), 0, weeks, dow);
554         }
555         int aw = range(ChronoField.ALIGNED_WEEK_OF_YEAR).checkValidIntValue(fieldValues.remove(ChronoField.ALIGNED_WEEK_OF_YEAR).longValue(), ChronoField.ALIGNED_WEEK_OF_YEAR);
556         int dow = range(ChronoField.DAY_OF_WEEK).checkValidIntValue(fieldValues.remove(ChronoField.DAY_OF_WEEK).longValue(), ChronoField.DAY_OF_WEEK);
557         ChronoLocalDate date = dateYearDay(y, 1).plus((aw - 1) * 7, ChronoUnit.DAYS)._with(TemporalAdjusters.nextOrSame(DayOfWeek.of(dow)));
558         if (resolverStyle == ResolverStyle.STRICT && date.get(ChronoField.YEAR) != y) {
559             throw new DateTimeException("Strict mode rejected resolved date as it is _in a different year");
560         }
561         return date;
562     }
563 
564     ChronoLocalDate resolveAligned(ChronoLocalDate base, long months, long weeks, long dow) {
565         ChronoLocalDate date = base.plus(months, ChronoUnit.MONTHS).plus(weeks, ChronoUnit.WEEKS);
566         if (dow > 7) {
567             date = date.plus((dow - 1) / 7, ChronoUnit.WEEKS);
568             dow = ((dow - 1) % 7) + 1;
569         } else if (dow < 1) {
570             date = date.plus(MathHelper.subtractExact(dow,  7) / 7, ChronoUnit.WEEKS);
571             dow = ((dow + 6) % 7) + 1;
572         }
573         return date._with(TemporalAdjusters.nextOrSame(DayOfWeek.of(cast(int) dow)));
574     }
575 
576     /**
577      * Adds a field-value pair to the map, checking for conflicts.
578      * !(p)
579      * If the field is not already present, then the field-value pair is added to the map.
580      * If the field is already present and it has the same value as that specified, no action occurs.
581      * If the field is already present and it has a different value to that specified, then
582      * an exception is thrown.
583      *
584      * @param field  the field to add, not null
585      * @param value  the value to add, not null
586      * @throws hunt.time.Exceptions if the field is already present with a different value
587      */
588     void addFieldValue(Map!(TemporalField, Long) fieldValues, ChronoField field, long value) {
589         Long old = fieldValues.get(field);  // check first for better error message
590         if (old !is null && old.longValue() != value) {
591             throw new DateTimeException("Conflict found: " ~ typeid(field).name ~ " " ~ old.to!string ~ " differs from " ~ typeid(field).name ~ " " ~ value.to!string);
592         }
593         fieldValues.put(field, new Long(value));
594     }
595 
596     //-----------------------------------------------------------------------
597     /**
598      * Compares this chronology to another chronology.
599      * !(p)
600      * The comparison order first by the chronology ID string, then by any
601      * additional information specific to the subclass.
602      * It is "consistent with equals", as defined by {@link Comparable}.
603      *
604      * @implSpec
605      * This implementation compares the chronology ID.
606      * Subclasses must compare any additional state that they store.
607      *
608      * @param other  the other chronology to compare to, not null
609      * @return the comparator value, negative if less, positive if greater
610      */
611     override
612     public int compareTo(Chronology other) {
613         return getId().compare(other.getId());
614     }
615 
616     /**
617      * Checks if this chronology is equal to another chronology.
618      * <p>
619      * The comparison is based on the entire state of the object.
620      *
621      * @implSpec
622      * This implementation checks the type and calls
623      * {@link #compareTo(hunt.time.chrono.Chronology)}.
624      *
625      * @param obj  the object to check, null returns false
626      * @return true if this is equal to the other chronology
627      */
628     override
629     public bool opEquals(Object obj) {
630         if (this is obj) {
631            return true;
632         }
633         if (cast(AbstractChronology)(obj) !is null) {
634             return compareTo(cast(AbstractChronology) obj) == 0;
635         }
636         return false;
637     }
638     
639 
640     /**
641      * A hash code for this chronology.
642      * <p>
643      * The hash code should be based on the entire state of the object.
644      *
645      * @implSpec
646      * This implementation is based on the chronology ID and class.
647      * Subclasses should add any additional state that they store.
648      *
649      * @return a suitable hash code
650      */
651     override
652     public size_t toHash() @trusted nothrow {
653         try
654         {   
655             return hashOf(typeid(this).name) ^ hashOf(getId());
656         }
657         catch(Exception e){}
658         return int.init;
659     }
660 
661     //-----------------------------------------------------------------------
662     /**
663      * Outputs this chronology as a {@code string}, using the chronology ID.
664      *
665      * @return a string representation of this chronology, not null
666      */
667     override
668     public string toString() {
669         return getId();
670     }
671 
672     //-----------------------------------------------------------------------
673     /**
674      * Writes the Chronology using a
675      * <a href="{@docRoot}/serialized-form.html#hunt.time.chrono.Ser">dedicated serialized form</a>.
676      * <pre>
677      *  _out.writeByte(1);  // identifies this as a Chronology
678      *  _out.writeUTF(getId());
679      * </pre>
680      *
681      * @return the instance of {@code Ser}, not null
682      */
683     Object writeReplace() {
684         return new Ser(Ser.CHRONO_TYPE, this);
685     }
686 
687     /**
688      * Defend against malicious streams.
689      *
690      * @param s the stream to read
691      * @throws java.io.InvalidObjectException always
692      */
693      ///@gxc
694     // private void readObject(ObjectInputStream s) /*throws ObjectStreamException*/ {
695     //     throw new InvalidObjectException("Deserialization via serialization delegate");
696     // }
697 
698     void writeExternal(DataOutput _out) /*throws IOException*/ {
699         _out.writeUTF(getId());
700     }
701 
702     static Chronology readExternal(DataInput _in) /*throws IOException*/ {
703         string id = _in.readUTF();
704         return Chronology.of(id);
705     }
706 
707 }