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.format.Parsed;
13 
14 import hunt.time.temporal.ChronoField;
15 
16 import hunt.time.Exceptions;
17 import hunt.time.Instant;
18 import hunt.time.LocalDate;
19 import hunt.time.LocalTime;
20 import hunt.time.Period;
21 import hunt.time.ZoneId;
22 import hunt.time.ZoneOffset;
23 import hunt.time.chrono.ChronoLocalDate;
24 import hunt.time.chrono.ChronoLocalDateTime;
25 import hunt.time.chrono.ChronoZonedDateTime;
26 import hunt.time.chrono.Chronology;
27 import hunt.time.temporal.ChronoField;
28 import hunt.time.temporal.TemporalAccessor;
29 import hunt.time.temporal.TemporalField;
30 import hunt.time.temporal.TemporalQueries;
31 import hunt.time.temporal.TemporalQuery;
32 import hunt.time.Exceptions;
33 import hunt.time.temporal.ValueRange;
34 
35 import hunt.collection.HashMap;
36 import hunt.collection.Iterator;
37 import hunt.collection.Map;
38 import std.conv;
39 import hunt.util.StringBuilder;
40 import hunt.collection.Set;
41 import hunt.Long;
42 import hunt.math.Helper;
43 import hunt.time.format.ResolverStyle;
44 
45 /**
46  * A store of parsed data.
47  * !(p)
48  * This class is used during parsing to collect the data. Part of the parsing process
49  * involves handling optional blocks and multiple copies of the data get created to
50  * support the necessary backtracking.
51  * !(p)
52  * Once parsing is completed, this class can be used as the resultant {@code TemporalAccessor}.
53  * In most cases, it is only exposed once the fields have been resolved.
54  *
55  * @implSpec
56  * This class is a mutable context intended for use from a single thread.
57  * Usage of the class is thread-safe within standard parsing as a new instance of this class
58  * is automatically created for each parse and parsing is single-threaded
59  *
60  * @since 1.8
61  */
62 final class Parsed : TemporalAccessor
63 {
64     // some fields are accessed using package scope from DateTimeParseContext
65 
66     /**
67      * The parsed fields.
68      */
69     Map!(TemporalField, Long) fieldValues;
70     /**
71      * The parsed zone.
72      */
73     ZoneId zone;
74     /**
75      * The parsed chronology.
76      */
77     Chronology chrono;
78     /**
79      * Whether a leap-second is parsed.
80      */
81     bool leapSecond;
82     /**
83      * The resolver style to use.
84      */
85     private ResolverStyle resolverStyle;
86     /**
87      * The resolved date.
88      */
89     private ChronoLocalDate date;
90     /**
91      * The resolved time.
92      */
93     private LocalTime time;
94     /**
95      * The excess period from time-only parsing.
96      */
97     Period excessDays;
98 
99     /**
100      * Creates an instance.
101      */
102     this()
103     {
104         fieldValues = new HashMap!(TemporalField, Long)();
105         excessDays = Period.ZERO;
106     }
107 
108     /**
109      * Creates a copy.
110      */
111     Parsed copy()
112     {
113         // only copy fields used _in parsing stage
114         Parsed cloned = new Parsed();
115         cloned.fieldValues.putAll(this.fieldValues);
116         cloned.zone = this.zone;
117         cloned.chrono = this.chrono;
118         cloned.leapSecond = this.leapSecond;
119         return cloned;
120     }
121 
122     //-----------------------------------------------------------------------
123     override public bool isSupported(TemporalField field)
124     {
125         if (fieldValues.containsKey(field) || (date !is null
126                 && date.isSupported(field)) || (time !is null && time.isSupported(field)))
127         {
128             return true;
129         }
130         return field !is null && ((cast(ChronoField)(field) !is null) == false)
131             && field.isSupportedBy(this);
132     }
133 
134     override public long getLong(TemporalField field)
135     {
136         assert(field, "field");
137         Long value = fieldValues.get(field);
138         if (value !is null)
139         {
140             return value.longValue();
141         }
142         if (date !is null && date.isSupported(field))
143         {
144             return date.getLong(field);
145         }
146         if (time !is null && time.isSupported(field))
147         {
148             return time.getLong(field);
149         }
150         if (cast(ChronoField)(field) !is null)
151         {
152             throw new UnsupportedTemporalTypeException("Unsupported field: " ~ typeid(field).name);
153         }
154         return field.getFrom(this);
155     }
156 
157     /*@SuppressWarnings("unchecked")*/
158     /* override */ public R query(R)(TemporalQuery!(R) query)
159     {
160         if (query == TemporalQueries.zoneId())
161         {
162             return cast(R) zone;
163         }
164         else if (query == TemporalQueries.chronology())
165         {
166             return cast(R) chrono;
167         }
168         else if (query == TemporalQueries.localDate())
169         {
170             return cast(R)(date !is null ? LocalDate.from(date) : null);
171         }
172         else if (query == TemporalQueries.localTime())
173         {
174             return cast(R) time;
175         }
176         else if (query == TemporalQueries.offset())
177         {
178             Long offsetSecs = fieldValues.get(ChronoField.OFFSET_SECONDS);
179             if (offsetSecs !is null)
180             {
181                 return cast(R) ZoneOffset.ofTotalSeconds(offsetSecs.intValue());
182             }
183             if (cast(ZoneOffset)(zone) !is null)
184             {
185                 return cast(R) zone;
186             }
187             return query.queryFrom(this);
188         }
189         else if (query == TemporalQueries.zone())
190         {
191             return query.queryFrom(this);
192         }
193         else if (query == TemporalQueries.precision())
194         {
195             return null; // not a complete date/time
196         }
197         // inline TemporalAccessor.super.query(query) as an optimization
198         // non-JDK classes are not permitted to make this optimization
199         return query.queryFrom(this);
200     }
201 
202     //-----------------------------------------------------------------------
203     /**
204      * Resolves the fields _in this context.
205      *
206      * @param resolverStyle  the resolver style, not null
207      * @param resolverFields  the fields to use for resolving, null for all fields
208      * @return this, for method chaining
209      * @throws DateTimeException if resolving one field results _in a value for
210      *  another field that is _in conflict
211      */
212     TemporalAccessor resolve(ResolverStyle resolverStyle, Set!(TemporalField) resolverFields)
213     {
214         if (resolverFields !is null)
215         {
216             // fieldValues.keySet().retainAll(resolverFields);
217             foreach (k; resolverFields)
218                 if (!fieldValues.containsKey(k))
219                     fieldValues.remove(k);
220         }
221         this.resolverStyle = resolverStyle;
222         resolveFields();
223         resolveTimeLenient();
224         crossCheck();
225         resolvePeriod();
226         resolveFractional();
227         resolveInstant();
228         return this;
229     }
230 
231     //-----------------------------------------------------------------------
232     private void resolveFields()
233     {
234         // resolve ChronoField
235         resolveInstantFields();
236         resolveDateFields();
237         resolveTimeFields();
238 
239         // if any other fields, handle them
240         // any lenient date resolution should return epoch-day
241         if (fieldValues.size() > 0)
242         {
243             int changedCount = 0;
244             outer: while (changedCount < 50)
245             {
246                 foreach (TemporalField k, Long v; fieldValues)
247                 {
248                     TemporalField targetField = k;
249                     TemporalAccessor resolvedObject = targetField.resolve(fieldValues,
250                             this, resolverStyle);
251                     if (resolvedObject !is null)
252                     {
253                         if (cast(ChronoZonedDateTime!ChronoLocalDate)(resolvedObject) !is null)
254                         {
255                             ChronoZonedDateTime!(ChronoLocalDate) czdt = cast(
256                                     ChronoZonedDateTime!(ChronoLocalDate)) resolvedObject;
257                             if (zone is null)
258                             {
259                                 zone = czdt.getZone();
260                             }
261                             else if ((zone == czdt.getZone()) == false)
262                             {
263                                 throw new DateTimeException(
264                                         "ChronoZonedDateTime must use the effective parsed zone: " ~ typeid(zone)
265                                         .name);
266                             }
267                             resolvedObject = czdt.toLocalDateTime();
268                         }
269                         if (cast(ChronoLocalDateTime!ChronoLocalDate)(resolvedObject) !is null)
270                         {
271                             ChronoLocalDateTime!(ChronoLocalDate) cldt = cast(
272                                     ChronoLocalDateTime!(ChronoLocalDate)) resolvedObject;
273                             updateCheckConflict(cldt.toLocalTime(), Period.ZERO);
274                             updateCheckConflict(cldt.toLocalDate());
275                             changedCount++;
276                             continue outer; // have to restart to avoid concurrent modification
277                         }
278                         if (cast(ChronoLocalDate)(resolvedObject) !is null)
279                         {
280                             updateCheckConflict(cast(ChronoLocalDate) resolvedObject);
281                             changedCount++;
282                             continue outer; // have to restart to avoid concurrent modification
283                         }
284                         if (cast(LocalTime)(resolvedObject) !is null)
285                         {
286                             updateCheckConflict(cast(LocalTime) resolvedObject, Period.ZERO);
287                             changedCount++;
288                             continue outer; // have to restart to avoid concurrent modification
289                         }
290                         throw new DateTimeException("Method resolve() can only return ChronoZonedDateTime, "
291                                 ~ "ChronoLocalDateTime, ChronoLocalDate or LocalTime");
292                     }
293                     else if (fieldValues.containsKey(targetField) == false)
294                     {
295                         changedCount++;
296                         continue outer; // have to restart to avoid concurrent modification
297                     }
298                 }
299                 break;
300             }
301             if (changedCount == 50)
302             { // catch infinite loops
303                 throw new DateTimeException(
304                         "One of the parsed fields has an incorrectly implemented resolve method");
305             }
306             // if something changed then have to redo ChronoField resolve
307             if (changedCount > 0)
308             {
309                 resolveInstantFields();
310                 resolveDateFields();
311                 resolveTimeFields();
312             }
313         }
314     }
315 
316     private void updateCheckConflict(TemporalField targetField,
317             TemporalField changeField, Long changeValue)
318     {
319         Long old = fieldValues.put(changeField, changeValue);
320         if (old !is null && old.longValue() != changeValue.longValue())
321         {
322             throw new DateTimeException("Conflict found: " ~ changeField.toString ~ " " ~ old.toString ~ " differs from "
323                     ~ changeField.toString ~ " " ~ changeValue.toString
324                     ~ " while resolving  " ~ targetField.toString);
325         }
326     }
327 
328     //-----------------------------------------------------------------------
329     private void resolveInstantFields()
330     {
331         // resolve parsed instant seconds to date and time if zone available
332         if (fieldValues.containsKey(ChronoField.INSTANT_SECONDS))
333         {
334             if (zone !is null)
335             {
336                 resolveInstantFields0(zone);
337             }
338             else
339             {
340                 Long offsetSecs = fieldValues.get(ChronoField.OFFSET_SECONDS);
341                 if (offsetSecs !is null)
342                 {
343                     ZoneOffset offset = ZoneOffset.ofTotalSeconds(offsetSecs.intValue());
344                     resolveInstantFields0(offset);
345                 }
346             }
347         }
348     }
349 
350     private void resolveInstantFields0(ZoneId selectedZone)
351     {
352         Instant instant = Instant.ofEpochSecond(
353                 fieldValues.remove(ChronoField.INSTANT_SECONDS).longValue());
354         ChronoZonedDateTime!(ChronoLocalDate) zdt = chrono.zonedDateTime(instant, selectedZone);
355         updateCheckConflict(zdt.toLocalDate());
356         updateCheckConflict(ChronoField.INSTANT_SECONDS, ChronoField.SECOND_OF_DAY,
357                 new Long(cast(long) zdt.toLocalTime().toSecondOfDay()));
358     }
359 
360     //-----------------------------------------------------------------------
361     private void resolveDateFields()
362     {
363         updateCheckConflict(chrono.resolveDate(fieldValues, resolverStyle));
364     }
365 
366     private void updateCheckConflict(ChronoLocalDate cld)
367     {
368         if (date !is null)
369         {
370             if (cld !is null && (date == cld) == false)
371             {
372                 throw new DateTimeException(
373                         "Conflict found: Fields resolved to two different dates: "
374                         ~ date.toString ~ " " ~ cld.toString);
375             }
376         }
377         else if (cld !is null)
378         {
379             if ((chrono == cld.getChronology()) == false)
380             {
381                 throw new DateTimeException(
382                         "ChronoLocalDate must use the effective parsed chronology: "
383                         ~ chrono.toString);
384             }
385             date = cld;
386         }
387     }
388 
389     //-----------------------------------------------------------------------
390     private void resolveTimeFields()
391     {
392         // simplify fields
393         if (fieldValues.containsKey(ChronoField.CLOCK_HOUR_OF_DAY))
394         {
395             // lenient allows anything, smart allows 0-24, strict allows 1-24
396             long ch = fieldValues.remove(ChronoField.CLOCK_HOUR_OF_DAY).longValue();
397             if (resolverStyle == ResolverStyle.STRICT
398                     || (resolverStyle == ResolverStyle.SMART && ch != 0))
399             {
400                 ChronoField.CLOCK_HOUR_OF_DAY.checkValidValue(ch);
401             }
402             updateCheckConflict(ChronoField.CLOCK_HOUR_OF_DAY,
403                     ChronoField.HOUR_OF_DAY, ch == 24 ? new Long(0) : new Long(ch));
404         }
405         if (fieldValues.containsKey(ChronoField.CLOCK_HOUR_OF_AMPM))
406         {
407             // lenient allows anything, smart allows 0-12, strict allows 1-12
408             long ch = fieldValues.remove(ChronoField.CLOCK_HOUR_OF_AMPM).longValue();
409             if (resolverStyle == ResolverStyle.STRICT
410                     || (resolverStyle == ResolverStyle.SMART && ch != 0))
411             {
412                 ChronoField.CLOCK_HOUR_OF_AMPM.checkValidValue(ch);
413             }
414             updateCheckConflict(ChronoField.CLOCK_HOUR_OF_AMPM,
415                     ChronoField.HOUR_OF_AMPM, ch == 12 ? new Long(0) : new Long(ch));
416         }
417         if (fieldValues.containsKey(ChronoField.AMPM_OF_DAY)
418                 && fieldValues.containsKey(ChronoField.HOUR_OF_AMPM))
419         {
420             long ap = fieldValues.remove(ChronoField.AMPM_OF_DAY).longValue();
421             long hap = fieldValues.remove(ChronoField.HOUR_OF_AMPM).longValue();
422             if (resolverStyle == ResolverStyle.LENIENT)
423             {
424                 updateCheckConflict(ChronoField.AMPM_OF_DAY, ChronoField.HOUR_OF_DAY,
425                         new Long(MathHelper.addExact(MathHelper.multiplyExact(ap, 12), hap)));
426             }
427             else
428             { // STRICT or SMART
429                 ChronoField.AMPM_OF_DAY.checkValidValue(ap);
430                 ChronoField.HOUR_OF_AMPM.checkValidValue(ap);
431                 updateCheckConflict(ChronoField.AMPM_OF_DAY,
432                         ChronoField.HOUR_OF_DAY, new Long(ap * 12 + hap));
433             }
434         }
435         if (fieldValues.containsKey(ChronoField.NANO_OF_DAY))
436         {
437             long nod = fieldValues.remove(ChronoField.NANO_OF_DAY).longValue();
438             if (resolverStyle != ResolverStyle.LENIENT)
439             {
440                 ChronoField.NANO_OF_DAY.checkValidValue(nod);
441             }
442             updateCheckConflict(ChronoField.NANO_OF_DAY,
443                     ChronoField.HOUR_OF_DAY, new Long(nod / 3600_000_000_000L));
444             updateCheckConflict(ChronoField.NANO_OF_DAY,
445                     ChronoField.MINUTE_OF_HOUR, new Long((nod / 60_000_000_000L) % 60));
446             updateCheckConflict(ChronoField.NANO_OF_DAY,
447                     ChronoField.SECOND_OF_MINUTE, new Long((nod / 1_000_000_000L) % 60));
448             updateCheckConflict(ChronoField.NANO_OF_DAY,
449                     ChronoField.NANO_OF_SECOND, new Long(nod % 1_000_000_000L));
450         }
451         if (fieldValues.containsKey(ChronoField.MICRO_OF_DAY))
452         {
453             long cod = fieldValues.remove(ChronoField.MICRO_OF_DAY).longValue();
454             if (resolverStyle != ResolverStyle.LENIENT)
455             {
456                 ChronoField.MICRO_OF_DAY.checkValidValue(cod);
457             }
458             updateCheckConflict(ChronoField.MICRO_OF_DAY,
459                     ChronoField.SECOND_OF_DAY, new Long(cod / 1_000_000L));
460             updateCheckConflict(ChronoField.MICRO_OF_DAY,
461                     ChronoField.MICRO_OF_SECOND, new Long(cod % 1_000_000L));
462         }
463         if (fieldValues.containsKey(ChronoField.MILLI_OF_DAY))
464         {
465             long lod = fieldValues.remove(ChronoField.MILLI_OF_DAY).longValue();
466             if (resolverStyle != ResolverStyle.LENIENT)
467             {
468                 ChronoField.MILLI_OF_DAY.checkValidValue(lod);
469             }
470             updateCheckConflict(ChronoField.MILLI_OF_DAY,
471                     ChronoField.SECOND_OF_DAY, new Long(lod / 1_000));
472             updateCheckConflict(ChronoField.MILLI_OF_DAY,
473                     ChronoField.MILLI_OF_SECOND, new Long(lod % 1_000));
474         }
475         if (fieldValues.containsKey(ChronoField.SECOND_OF_DAY))
476         {
477             long sod = fieldValues.remove(ChronoField.SECOND_OF_DAY).longValue();
478             if (resolverStyle != ResolverStyle.LENIENT)
479             {
480                 ChronoField.SECOND_OF_DAY.checkValidValue(sod);
481             }
482             updateCheckConflict(ChronoField.SECOND_OF_DAY,
483                     ChronoField.HOUR_OF_DAY, new Long(sod / 3600));
484             updateCheckConflict(ChronoField.SECOND_OF_DAY,
485                     ChronoField.MINUTE_OF_HOUR, new Long((sod / 60) % 60));
486             updateCheckConflict(ChronoField.SECOND_OF_DAY,
487                     ChronoField.SECOND_OF_MINUTE, new Long(sod % 60));
488         }
489         if (fieldValues.containsKey(ChronoField.MINUTE_OF_DAY))
490         {
491             long mod = fieldValues.remove(ChronoField.MINUTE_OF_DAY).longValue();
492             if (resolverStyle != ResolverStyle.LENIENT)
493             {
494                 ChronoField.MINUTE_OF_DAY.checkValidValue(mod);
495             }
496             updateCheckConflict(ChronoField.MINUTE_OF_DAY,
497                     ChronoField.HOUR_OF_DAY, new Long(mod / 60));
498             updateCheckConflict(ChronoField.MINUTE_OF_DAY,
499                     ChronoField.MINUTE_OF_HOUR, new Long(mod % 60));
500         }
501 
502         // combine partial second fields strictly, leaving lenient expansion to later
503         if (fieldValues.containsKey(ChronoField.NANO_OF_SECOND))
504         {
505             long nos = fieldValues.get(ChronoField.NANO_OF_SECOND).longValue();
506             if (resolverStyle != ResolverStyle.LENIENT)
507             {
508                 ChronoField.NANO_OF_SECOND.checkValidValue(nos);
509             }
510             if (fieldValues.containsKey(ChronoField.MICRO_OF_SECOND))
511             {
512                 long cos = fieldValues.remove(ChronoField.MICRO_OF_SECOND).longValue();
513                 if (resolverStyle != ResolverStyle.LENIENT)
514                 {
515                     ChronoField.MICRO_OF_SECOND.checkValidValue(cos);
516                 }
517                 nos = cos * 1000 + (nos % 1000);
518                 updateCheckConflict(ChronoField.MICRO_OF_SECOND,
519                         ChronoField.NANO_OF_SECOND, new Long(nos));
520             }
521             if (fieldValues.containsKey(ChronoField.MILLI_OF_SECOND))
522             {
523                 long los = fieldValues.remove(ChronoField.MILLI_OF_SECOND).longValue();
524                 if (resolverStyle != ResolverStyle.LENIENT)
525                 {
526                     ChronoField.MILLI_OF_SECOND.checkValidValue(los);
527                 }
528                 updateCheckConflict(ChronoField.MILLI_OF_SECOND,
529                         ChronoField.NANO_OF_SECOND, new Long(los * 1_000_000L + (nos % 1_000_000L)));
530             }
531         }
532 
533         // convert to time if all four fields available (optimization)
534         if (fieldValues.containsKey(ChronoField.HOUR_OF_DAY) && fieldValues.containsKey(ChronoField.MINUTE_OF_HOUR)
535                 && fieldValues.containsKey(ChronoField.SECOND_OF_MINUTE)
536                 && fieldValues.containsKey(ChronoField.NANO_OF_SECOND))
537         {
538             long hod = fieldValues.remove(ChronoField.HOUR_OF_DAY).longValue();
539             long moh = fieldValues.remove(ChronoField.MINUTE_OF_HOUR).longValue();
540             long som = fieldValues.remove(ChronoField.SECOND_OF_MINUTE).longValue();
541             long nos = fieldValues.remove(ChronoField.NANO_OF_SECOND).longValue();
542             resolveTime(hod, moh, som, nos);
543         }
544     }
545 
546     private void resolveTimeLenient()
547     {
548         // leniently create a time from incomplete information
549         // done after everything else as it creates information from nothing
550         // which would break updateCheckConflict(field)
551 
552         if (time is null)
553         {
554             // NANO_OF_SECOND merged with MILLI/MICRO above
555             if (fieldValues.containsKey(ChronoField.MILLI_OF_SECOND))
556             {
557                 long los = fieldValues.remove(ChronoField.MILLI_OF_SECOND).longValue();
558                 if (fieldValues.containsKey(ChronoField.MICRO_OF_SECOND))
559                 {
560                     // merge milli-of-second and micro-of-second for better error message
561                     long cos = los * 1_000 + (fieldValues.get(ChronoField.MICRO_OF_SECOND)
562                             .longValue() % 1_000);
563                     updateCheckConflict(ChronoField.MILLI_OF_SECOND,
564                             ChronoField.MICRO_OF_SECOND, new Long(cos));
565                     fieldValues.remove(ChronoField.MICRO_OF_SECOND);
566                     fieldValues.put(ChronoField.NANO_OF_SECOND, new Long(cos * 1_000L));
567                 }
568                 else
569                 {
570                     // convert milli-of-second to nano-of-second
571                     fieldValues.put(ChronoField.NANO_OF_SECOND, new Long(los * 1_000_000L));
572                 }
573             }
574             else if (fieldValues.containsKey(ChronoField.MICRO_OF_SECOND))
575             {
576                 // convert micro-of-second to nano-of-second
577                 long cos = fieldValues.remove(ChronoField.MICRO_OF_SECOND).longValue();
578                 fieldValues.put(ChronoField.NANO_OF_SECOND, new Long(cos * 1_000L));
579             }
580 
581             // merge hour/minute/second/nano leniently
582             Long hod = fieldValues.get(ChronoField.HOUR_OF_DAY);
583             if (hod !is null)
584             {
585                 Long moh = fieldValues.get(ChronoField.MINUTE_OF_HOUR);
586                 Long som = fieldValues.get(ChronoField.SECOND_OF_MINUTE);
587                 Long nos = fieldValues.get(ChronoField.NANO_OF_SECOND);
588 
589                 // check for invalid combinations that cannot be defaulted
590                 if ((moh is null && (som !is null || nos !is null))
591                         || (moh !is null && som is null && nos !is null))
592                 {
593                     return;
594                 }
595 
596                 // default as necessary and build time
597                 long mohVal = (moh !is null ? moh.longValue() : 0);
598                 long somVal = (som !is null ? som.longValue() : 0);
599                 long nosVal = (nos !is null ? nos.longValue() : 0);
600                 resolveTime(hod.longValue(), mohVal, somVal, nosVal);
601                 fieldValues.remove(ChronoField.HOUR_OF_DAY);
602                 fieldValues.remove(ChronoField.MINUTE_OF_HOUR);
603                 fieldValues.remove(ChronoField.SECOND_OF_MINUTE);
604                 fieldValues.remove(ChronoField.NANO_OF_SECOND);
605             }
606         }
607 
608         // validate remaining
609         if (resolverStyle != ResolverStyle.LENIENT && fieldValues.size() > 0)
610         {
611             foreach (TemporalField k, Long v; fieldValues)
612             {
613                 TemporalField field = k;
614                 if (cast(ChronoField)(field) !is null && field.isTimeBased())
615                 {
616                     (cast(ChronoField) field).checkValidValue(v.longValue());
617                 }
618             }
619         }
620     }
621 
622     private void resolveTime(long hod, long moh, long som, long nos)
623     {
624         if (resolverStyle == ResolverStyle.LENIENT)
625         {
626             long totalNanos = MathHelper.multiplyExact(hod, 3600_000_000_000L);
627             totalNanos = MathHelper.addExact(totalNanos, MathHelper.multiplyExact(moh, 60_000_000_000L));
628             totalNanos = MathHelper.addExact(totalNanos, MathHelper.multiplyExact(som, 1_000_000_000L));
629             totalNanos = MathHelper.addExact(totalNanos, nos);
630             int excessDays = cast(int) MathHelper.floorDiv(totalNanos, 86400_000_000_000L); // safe int cast
631             long nod = MathHelper.floorMod(totalNanos, 86400_000_000_000L);
632             updateCheckConflict(LocalTime.ofNanoOfDay(nod), Period.ofDays(excessDays));
633         }
634         else
635         { // STRICT or SMART
636             int mohVal = ChronoField.MINUTE_OF_HOUR.checkValidIntValue(moh);
637             int nosVal = ChronoField.NANO_OF_SECOND.checkValidIntValue(nos);
638             // handle 24:00 end of day
639             if (resolverStyle == ResolverStyle.SMART && hod == 24
640                     && mohVal == 0 && som == 0 && nosVal == 0)
641             {
642                 updateCheckConflict(LocalTime.MIDNIGHT, Period.ofDays(1));
643             }
644             else
645             {
646                 int hodVal = ChronoField.HOUR_OF_DAY.checkValidIntValue(hod);
647                 int somVal = ChronoField.SECOND_OF_MINUTE.checkValidIntValue(som);
648                 updateCheckConflict(LocalTime.of(hodVal, mohVal, somVal, nosVal), Period.ZERO);
649             }
650         }
651     }
652 
653     private void resolvePeriod()
654     {
655         // add whole days if we have both date and time
656         if (date !is null && time !is null && excessDays.isZero() == false)
657         {
658             date = date.plus(excessDays);
659             excessDays = Period.ZERO;
660         }
661     }
662 
663     private void resolveFractional()
664     {
665         // ensure fractional seconds available as ChronoField requires
666         // resolveTimeLenient() will have merged MICRO_OF_SECOND/MILLI_OF_SECOND to NANO_OF_SECOND
667         if (time is null && (fieldValues.containsKey(ChronoField.INSTANT_SECONDS)
668                 || fieldValues.containsKey(ChronoField.SECOND_OF_DAY)
669                 || fieldValues.containsKey(ChronoField.SECOND_OF_MINUTE)))
670         {
671             if (fieldValues.containsKey(ChronoField.NANO_OF_SECOND))
672             {
673                 long nos = fieldValues.get(ChronoField.NANO_OF_SECOND).longValue();
674                 fieldValues.put(ChronoField.MICRO_OF_SECOND, new Long(nos / 1000));
675                 fieldValues.put(ChronoField.MILLI_OF_SECOND, new Long(nos / 1000000));
676             }
677             else
678             {
679                 fieldValues.put(ChronoField.NANO_OF_SECOND, new Long(0L));
680                 fieldValues.put(ChronoField.MICRO_OF_SECOND, new Long(0L));
681                 fieldValues.put(ChronoField.MILLI_OF_SECOND, new Long(0L));
682             }
683         }
684     }
685 
686     private void resolveInstant()
687     {
688         // add instant seconds if we have date, time and zone
689         // Offset (if present) will be given priority over the zone.
690         if (date !is null && time !is null)
691         {
692             Long offsetSecs = fieldValues.get(ChronoField.OFFSET_SECONDS);
693             if (offsetSecs !is null)
694             {
695                 ZoneOffset offset = ZoneOffset.ofTotalSeconds(offsetSecs.intValue());
696                 long instant = date.atTime(time).atZone(offset).toEpochSecond();
697                 fieldValues.put(ChronoField.INSTANT_SECONDS, new Long(instant));
698             }
699             else
700             {
701                 if (zone !is null)
702                 {
703                     long instant = date.atTime(time).atZone(zone).toEpochSecond();
704                     fieldValues.put(ChronoField.INSTANT_SECONDS, new Long(instant));
705                 }
706             }
707         }
708     }
709 
710     private void updateCheckConflict(LocalTime timeToSet, Period periodToSet)
711     {
712         if (time !is null)
713         {
714             if ((time == timeToSet) == false)
715             {
716                 throw new DateTimeException(
717                         "Conflict found: Fields resolved to different times: "
718                         ~ time.toString ~ " " ~ timeToSet.toString);
719             }
720             if (excessDays.isZero() == false && periodToSet.isZero() == false
721                     && (excessDays == periodToSet) == false)
722             {
723                 throw new DateTimeException("Conflict found: Fields resolved to different excess periods: "
724                         ~ excessDays.toString ~ " " ~ periodToSet.toString);
725             }
726             else
727             {
728                 excessDays = periodToSet;
729             }
730         }
731         else
732         {
733             time = timeToSet;
734             excessDays = periodToSet;
735         }
736     }
737 
738     //-----------------------------------------------------------------------
739     private void crossCheck()
740     {
741         // only cross-check date, time and date-time
742         // avoid object creation if possible
743         if (date !is null)
744         {
745             crossCheck(date);
746         }
747         if (time !is null)
748         {
749             crossCheck(time);
750             if (date !is null && fieldValues.size() > 0)
751             {
752                 crossCheck(date.atTime(time));
753             }
754         }
755     }
756 
757     private void crossCheck(TemporalAccessor target)
758     {
759         // for (Iterator!(MapEntry!(TemporalField, Long)) it = fieldValues.entrySet().iterator(); it.hasNext(); ) {
760         //     Entry!(TemporalField, Long) entry = it.next();
761         //     TemporalField field = entry.getKey();
762         //     if (target.isSupported(field)) {
763         //         long val1;
764         //         try {
765         //             val1 = target.getLong(field);
766         //         } catch (Exception ex) {
767         //             continue;
768         //         }
769         //         long val2 = entry.getValue();
770         //         if (val1 != val2) {
771         //             throw new DateTimeException("Conflict found: Field " ~ field.toString ~ " " ~ val1.to!string +
772         //                     " differs from " ~ field.toString ~ " " ~ val2.to!string ~ " derived from " ~ target.toString);
773         //         }
774         //         it.remove();
775         //     }
776         // }
777 
778         foreach (TemporalField k, Long v; fieldValues)
779         {
780 
781             TemporalField field = k;
782             if (target.isSupported(field))
783             {
784                 long val1;
785                 try
786                 {
787                     val1 = target.getLong(field);
788                 }
789                 catch (Exception ex)
790                 {
791                     continue;
792                 }
793                 long val2 = v.longValue();
794                 if (val1 != val2)
795                 {
796                     throw new DateTimeException("Conflict found: Field " ~ field.toString ~ " " ~ val1.to!string
797                             ~ " differs from " ~ field.toString ~ " "
798                             ~ val2.to!string ~ " derived from " ~ target.toString);
799                 }
800                 fieldValues.remove(k);
801             }
802         }
803     }
804 
805     //-----------------------------------------------------------------------
806     override public string toString()
807     {
808         StringBuilder buf = new StringBuilder(64);
809         buf.append(fieldValues.toString).append(',').append(chrono.toString);
810         if (zone !is null)
811         {
812             buf.append(',').append(zone.toString);
813         }
814         if (date !is null || time !is null)
815         {
816             buf.append(" resolved to ");
817             if (date !is null)
818             {
819                 buf.append(date.toString);
820                 if (time !is null)
821                 {
822                     buf.append('T').append(time.toString);
823                 }
824             }
825             else
826             {
827                 buf.append(time.toString);
828             }
829         }
830         return buf.toString();
831     }
832 
833     override ValueRange range(TemporalField field)
834     {
835         if (cast(ChronoField)(field) !is null)
836         {
837             if (isSupported(field))
838             {
839                 return field.range();
840             }
841             throw new UnsupportedTemporalTypeException("Unsupported field: " ~ field.toString);
842         }
843         assert(field, "field");
844         return field.rangeRefinedBy(this);
845     }
846 
847     override int get(TemporalField field)
848     {
849         ValueRange range = range(field);
850         if (range.isIntValue() == false)
851         {
852             throw new UnsupportedTemporalTypeException(
853                     "Invalid field " ~ field.toString ~ " for get() method, use getLong() instead");
854         }
855         long value = getLong(field);
856         if (range.isValidValue(value) == false)
857         {
858             throw new DateTimeException(
859                     "Invalid value for " ~ field.toString ~ " (valid values "
860                     ~ range.toString ~ "): " ~ value.to!string);
861         }
862         return cast(int) value;
863     }
864 
865 }