1 /*
2  * Copyright (c) 2012, 2021, Oracle and/or its affiliates. All rights reserved.
3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4  *
5  * This code is free software; you can redistribute it and/or modify it
6  * under the terms of the GNU General Public License version 2 only, as
7  * published by the Free Software Foundation.  Oracle designates this
8  * particular file as subject to the "Classpath" exception as provided
9  * by Oracle in the LICENSE file that accompanied this code.
10  *
11  * This code is distributed in the hope that it will be useful, but WITHOUT
12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
14  * version 2 for more details (a copy is included in the LICENSE file that
15  * accompanied this code).
16  *
17  * You should have received a copy of the GNU General Public License version
18  * 2 along with this work; if not, write to the Free Software Foundation,
19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
20  *
21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
22  * or visit www.oracle.com if you need additional information or have any
23  * questions.
24  */
25 
26 /*
27  * This file is available under and governed by the GNU General Public
28  * License version 2 only, as published by the Free Software Foundation.
29  * However, the following notice accompanied the original version of this
30  * file:
31  *
32  * Copyright (c) 2008-2013, Stephen Colebourne & Michael Nascimento Santos
33  *
34  * All rights reserved.
35  *
36  * Redistribution and use in source and binary forms, with or without
37  * modification, are permitted provided that the following conditions are met:
38  *
39  *  * Redistributions of source code must retain the above copyright notice,
40  *    this list of conditions and the following disclaimer.
41  *
42  *  * Redistributions in binary form must reproduce the above copyright notice,
43  *    this list of conditions and the following disclaimer in the documentation
44  *    and/or other materials provided with the distribution.
45  *
46  *  * Neither the name of JSR-310 nor the names of its contributors
47  *    may be used to endorse or promote products derived from this software
48  *    without specific prior written permission.
49  *
50  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
51  * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
52  * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
53  * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
54  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
55  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
56  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
57  * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
58  * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
59  * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
60  * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
61  */
62 package java.time.format;
63 
64 import static java.time.format.DateTimeFormatterBuilder.DayPeriod;
65 import static java.time.temporal.ChronoField.AMPM_OF_DAY;
66 import static java.time.temporal.ChronoField.CLOCK_HOUR_OF_AMPM;
67 import static java.time.temporal.ChronoField.CLOCK_HOUR_OF_DAY;
68 import static java.time.temporal.ChronoField.HOUR_OF_AMPM;
69 import static java.time.temporal.ChronoField.HOUR_OF_DAY;
70 import static java.time.temporal.ChronoField.INSTANT_SECONDS;
71 import static java.time.temporal.ChronoField.MICRO_OF_DAY;
72 import static java.time.temporal.ChronoField.MICRO_OF_SECOND;
73 import static java.time.temporal.ChronoField.MILLI_OF_DAY;
74 import static java.time.temporal.ChronoField.MILLI_OF_SECOND;
75 import static java.time.temporal.ChronoField.MINUTE_OF_DAY;
76 import static java.time.temporal.ChronoField.MINUTE_OF_HOUR;
77 import static java.time.temporal.ChronoField.NANO_OF_DAY;
78 import static java.time.temporal.ChronoField.NANO_OF_SECOND;
79 import static java.time.temporal.ChronoField.OFFSET_SECONDS;
80 import static java.time.temporal.ChronoField.SECOND_OF_DAY;
81 import static java.time.temporal.ChronoField.SECOND_OF_MINUTE;
82 
83 import java.time.DateTimeException;
84 import java.time.Instant;
85 import java.time.LocalDate;
86 import java.time.LocalTime;
87 import java.time.Period;
88 import java.time.ZoneId;
89 import java.time.ZoneOffset;
90 import java.time.chrono.ChronoLocalDate;
91 import java.time.chrono.ChronoLocalDateTime;
92 import java.time.chrono.ChronoZonedDateTime;
93 import java.time.chrono.Chronology;
94 import java.time.temporal.ChronoField;
95 import java.time.temporal.TemporalAccessor;
96 import java.time.temporal.TemporalField;
97 import java.time.temporal.TemporalQueries;
98 import java.time.temporal.TemporalQuery;
99 import java.time.temporal.UnsupportedTemporalTypeException;
100 import java.util.HashMap;
101 import java.util.Iterator;
102 import java.util.Map;
103 import java.util.Map.Entry;
104 import java.util.Objects;
105 import java.util.Set;
106 
107 /**
108  * A store of parsed data.
109  * <p>
110  * This class is used during parsing to collect the data. Part of the parsing process
111  * involves handling optional blocks and multiple copies of the data get created to
112  * support the necessary backtracking.
113  * <p>
114  * Once parsing is completed, this class can be used as the resultant {@code TemporalAccessor}.
115  * In most cases, it is only exposed once the fields have been resolved.
116  *
117  * @implSpec
118  * This class is a mutable context intended for use from a single thread.
119  * Usage of the class is thread-safe within standard parsing as a new instance of this class
120  * is automatically created for each parse and parsing is single-threaded
121  *
122  * @since 1.8
123  */
124 final class Parsed implements TemporalAccessor {
125     // some fields are accessed using package scope from DateTimeParseContext
126 
127     /**
128      * The parsed fields.
129      */
130     final Map<TemporalField, Long> fieldValues = new HashMap<>();
131     /**
132      * The parsed zone.
133      */
134     ZoneId zone;
135     /**
136      * The parsed chronology.
137      */
138     Chronology chrono;
139     /**
140      * Whether a leap-second is parsed.
141      */
142     boolean leapSecond;
143     /**
144      * The resolver style to use.
145      */
146     private ResolverStyle resolverStyle;
147     /**
148      * The resolved date.
149      */
150     private ChronoLocalDate date;
151     /**
152      * The resolved time.
153      */
154     private LocalTime time;
155     /**
156      * The excess period from time-only parsing.
157      */
158     Period excessDays = Period.ZERO;
159     /**
160      * The parsed day period.
161      */
162     DayPeriod dayPeriod;
163 
164     /**
165      * Creates an instance.
166      */
Parsed()167     Parsed() {
168     }
169 
170     /**
171      * Creates a copy.
172      */
copy()173     Parsed copy() {
174         // only copy fields used in parsing stage
175         Parsed cloned = new Parsed();
176         cloned.fieldValues.putAll(this.fieldValues);
177         cloned.zone = this.zone;
178         cloned.chrono = this.chrono;
179         cloned.leapSecond = this.leapSecond;
180         cloned.dayPeriod = this.dayPeriod;
181         return cloned;
182     }
183 
184     //-----------------------------------------------------------------------
185     @Override
isSupported(TemporalField field)186     public boolean isSupported(TemporalField field) {
187         if (fieldValues.containsKey(field) ||
188                 (date != null && date.isSupported(field)) ||
189                 (time != null && time.isSupported(field))) {
190             return true;
191         }
192         return field != null && (!(field instanceof ChronoField)) && field.isSupportedBy(this);
193     }
194 
195     @Override
getLong(TemporalField field)196     public long getLong(TemporalField field) {
197         Objects.requireNonNull(field, "field");
198         Long value = fieldValues.get(field);
199         if (value != null) {
200             return value;
201         }
202         if (date != null && date.isSupported(field)) {
203             return date.getLong(field);
204         }
205         if (time != null && time.isSupported(field)) {
206             return time.getLong(field);
207         }
208         if (field instanceof ChronoField) {
209             throw new UnsupportedTemporalTypeException("Unsupported field: " + field);
210         }
211         return field.getFrom(this);
212     }
213 
214     @SuppressWarnings("unchecked")
215     @Override
query(TemporalQuery<R> query)216     public <R> R query(TemporalQuery<R> query) {
217         if (query == TemporalQueries.zoneId()) {
218             return (R) zone;
219         } else if (query == TemporalQueries.chronology()) {
220             return (R) chrono;
221         } else if (query == TemporalQueries.localDate()) {
222             return (R) (date != null ? LocalDate.from(date) : null);
223         } else if (query == TemporalQueries.localTime()) {
224             return (R) time;
225         } else if (query == TemporalQueries.offset()) {
226             Long offsetSecs = fieldValues.get(OFFSET_SECONDS);
227             if (offsetSecs != null) {
228                 return (R) ZoneOffset.ofTotalSeconds(offsetSecs.intValue());
229             }
230             if (zone instanceof ZoneOffset) {
231                 return (R)zone;
232             }
233             return query.queryFrom(this);
234         } else if (query == TemporalQueries.zone()) {
235             return query.queryFrom(this);
236         } else if (query == TemporalQueries.precision()) {
237             return null;  // not a complete date/time
238         }
239         // inline TemporalAccessor.super.query(query) as an optimization
240         // non-JDK classes are not permitted to make this optimization
241         return query.queryFrom(this);
242     }
243 
244     //-----------------------------------------------------------------------
245     /**
246      * Resolves the fields in this context.
247      *
248      * @param resolverStyle  the resolver style, not null
249      * @param resolverFields  the fields to use for resolving, null for all fields
250      * @return this, for method chaining
251      * @throws DateTimeException if resolving one field results in a value for
252      *  another field that is in conflict
253      */
resolve(ResolverStyle resolverStyle, Set<TemporalField> resolverFields)254     TemporalAccessor resolve(ResolverStyle resolverStyle, Set<TemporalField> resolverFields) {
255         if (resolverFields != null) {
256             fieldValues.keySet().retainAll(resolverFields);
257         }
258         this.resolverStyle = resolverStyle;
259         resolveFields();
260         resolveTimeLenient();
261         crossCheck();
262         resolvePeriod();
263         resolveFractional();
264         resolveInstant();
265         return this;
266     }
267 
268     //-----------------------------------------------------------------------
resolveFields()269     private void resolveFields() {
270         // resolve ChronoField
271         resolveInstantFields();
272         resolveDateFields();
273         resolveTimeFields();
274 
275         // if any other fields, handle them
276         // any lenient date resolution should return epoch-day
277         if (fieldValues.size() > 0) {
278             int changedCount = 0;
279             outer:
280             while (changedCount < 50) {
281                 for (Map.Entry<TemporalField, Long> entry : fieldValues.entrySet()) {
282                     TemporalField targetField = entry.getKey();
283                     TemporalAccessor resolvedObject = targetField.resolve(fieldValues, this, resolverStyle);
284                     if (resolvedObject != null) {
285                         if (resolvedObject instanceof ChronoZonedDateTime<?> czdt) {
286                             if (zone == null) {
287                                 zone = czdt.getZone();
288                             } else if (zone.equals(czdt.getZone()) == false) {
289                                 throw new DateTimeException("ChronoZonedDateTime must use the effective parsed zone: " + zone);
290                             }
291                             resolvedObject = czdt.toLocalDateTime();
292                         }
293                         if (resolvedObject instanceof ChronoLocalDateTime<?> cldt) {
294                             updateCheckConflict(cldt.toLocalTime(), Period.ZERO);
295                             updateCheckConflict(cldt.toLocalDate());
296                             changedCount++;
297                             continue outer;  // have to restart to avoid concurrent modification
298                         }
299                         if (resolvedObject instanceof ChronoLocalDate) {
300                             updateCheckConflict((ChronoLocalDate) resolvedObject);
301                             changedCount++;
302                             continue outer;  // have to restart to avoid concurrent modification
303                         }
304                         if (resolvedObject instanceof LocalTime) {
305                             updateCheckConflict((LocalTime) resolvedObject, Period.ZERO);
306                             changedCount++;
307                             continue outer;  // have to restart to avoid concurrent modification
308                         }
309                         throw new DateTimeException("Method resolve() can only return ChronoZonedDateTime, " +
310                                 "ChronoLocalDateTime, ChronoLocalDate or LocalTime");
311                     } else if (fieldValues.containsKey(targetField) == false) {
312                         changedCount++;
313                         continue outer;  // have to restart to avoid concurrent modification
314                     }
315                 }
316                 break;
317             }
318             if (changedCount == 50) {  // catch infinite loops
319                 throw new DateTimeException("One of the parsed fields has an incorrectly implemented resolve method");
320             }
321             // if something changed then have to redo ChronoField resolve
322             if (changedCount > 0) {
323                 resolveInstantFields();
324                 resolveDateFields();
325                 resolveTimeFields();
326             }
327         }
328     }
329 
updateCheckConflict(TemporalField targetField, TemporalField changeField, Long changeValue)330     private void updateCheckConflict(TemporalField targetField, TemporalField changeField, Long changeValue) {
331         Long old = fieldValues.put(changeField, changeValue);
332         if (old != null && old.longValue() != changeValue.longValue()) {
333             throw new DateTimeException("Conflict found: " + changeField + " " + old +
334                     " differs from " + changeField + " " + changeValue +
335                     " while resolving  " + targetField);
336         }
337     }
338 
339 
340 //-----------------------------------------------------------------------
resolveInstantFields()341     private void resolveInstantFields() {
342         // resolve parsed instant seconds to date and time if zone available
343         if (fieldValues.containsKey(INSTANT_SECONDS)) {
344             if (zone != null) {
345                 resolveInstantFields0(zone);
346             } else {
347                 Long offsetSecs = fieldValues.get(OFFSET_SECONDS);
348                 if (offsetSecs != null) {
349                     ZoneOffset offset = ZoneOffset.ofTotalSeconds(offsetSecs.intValue());
350                     resolveInstantFields0(offset);
351                 }
352             }
353         }
354     }
355 
resolveInstantFields0(ZoneId selectedZone)356     private void resolveInstantFields0(ZoneId selectedZone) {
357         Instant instant = Instant.ofEpochSecond(fieldValues.get(INSTANT_SECONDS));
358         ChronoZonedDateTime<?> zdt = chrono.zonedDateTime(instant, selectedZone);
359         updateCheckConflict(zdt.toLocalDate());
360         updateCheckConflict(INSTANT_SECONDS, SECOND_OF_DAY, (long) zdt.toLocalTime().toSecondOfDay());
361         updateCheckConflict(INSTANT_SECONDS, OFFSET_SECONDS, (long) zdt.getOffset().getTotalSeconds());
362     }
363 
364     //-----------------------------------------------------------------------
resolveDateFields()365     private void resolveDateFields() {
366         updateCheckConflict(chrono.resolveDate(fieldValues, resolverStyle));
367     }
368 
updateCheckConflict(ChronoLocalDate cld)369     private void updateCheckConflict(ChronoLocalDate cld) {
370         if (date != null) {
371             if (cld != null && date.equals(cld) == false) {
372                 throw new DateTimeException("Conflict found: Fields resolved to two different dates: " + date + " " + cld);
373             }
374         } else if (cld != null) {
375             if (chrono.equals(cld.getChronology()) == false) {
376                 throw new DateTimeException("ChronoLocalDate must use the effective parsed chronology: " + chrono);
377             }
378             date = cld;
379         }
380     }
381 
382     //-----------------------------------------------------------------------
resolveTimeFields()383     private void resolveTimeFields() {
384         // simplify fields
385         if (fieldValues.containsKey(CLOCK_HOUR_OF_DAY)) {
386             // lenient allows anything, smart allows 0-24, strict allows 1-24
387             long ch = fieldValues.remove(CLOCK_HOUR_OF_DAY);
388             if (resolverStyle == ResolverStyle.STRICT || (resolverStyle == ResolverStyle.SMART && ch != 0)) {
389                 CLOCK_HOUR_OF_DAY.checkValidValue(ch);
390             }
391             updateCheckConflict(CLOCK_HOUR_OF_DAY, HOUR_OF_DAY, ch == 24 ? 0 : ch);
392         }
393         if (fieldValues.containsKey(CLOCK_HOUR_OF_AMPM)) {
394             // lenient allows anything, smart allows 0-12, strict allows 1-12
395             long ch = fieldValues.remove(CLOCK_HOUR_OF_AMPM);
396             if (resolverStyle == ResolverStyle.STRICT || (resolverStyle == ResolverStyle.SMART && ch != 0)) {
397                 CLOCK_HOUR_OF_AMPM.checkValidValue(ch);
398             }
399             updateCheckConflict(CLOCK_HOUR_OF_AMPM, HOUR_OF_AMPM, ch == 12 ? 0 : ch);
400         }
401         if (fieldValues.containsKey(AMPM_OF_DAY) && fieldValues.containsKey(HOUR_OF_AMPM)) {
402             long ap = fieldValues.remove(AMPM_OF_DAY);
403             long hap = fieldValues.remove(HOUR_OF_AMPM);
404             if (resolverStyle == ResolverStyle.LENIENT) {
405                 updateCheckConflict(AMPM_OF_DAY, HOUR_OF_DAY, Math.addExact(Math.multiplyExact(ap, 12), hap));
406             } else {  // STRICT or SMART
407                 AMPM_OF_DAY.checkValidValue(ap);
408                 HOUR_OF_AMPM.checkValidValue(hap);
409                 updateCheckConflict(AMPM_OF_DAY, HOUR_OF_DAY, ap * 12 + hap);
410             }
411         }
412         if (fieldValues.containsKey(NANO_OF_DAY)) {
413             long nod = fieldValues.remove(NANO_OF_DAY);
414             if (resolverStyle != ResolverStyle.LENIENT) {
415                 NANO_OF_DAY.checkValidValue(nod);
416             }
417             updateCheckConflict(NANO_OF_DAY, HOUR_OF_DAY, nod / 3600_000_000_000L);
418             updateCheckConflict(NANO_OF_DAY, MINUTE_OF_HOUR, (nod / 60_000_000_000L) % 60);
419             updateCheckConflict(NANO_OF_DAY, SECOND_OF_MINUTE, (nod / 1_000_000_000L) % 60);
420             updateCheckConflict(NANO_OF_DAY, NANO_OF_SECOND, nod % 1_000_000_000L);
421         }
422         if (fieldValues.containsKey(MICRO_OF_DAY)) {
423             long cod = fieldValues.remove(MICRO_OF_DAY);
424             if (resolverStyle != ResolverStyle.LENIENT) {
425                 MICRO_OF_DAY.checkValidValue(cod);
426             }
427             updateCheckConflict(MICRO_OF_DAY, SECOND_OF_DAY, cod / 1_000_000L);
428             updateCheckConflict(MICRO_OF_DAY, MICRO_OF_SECOND, cod % 1_000_000L);
429         }
430         if (fieldValues.containsKey(MILLI_OF_DAY)) {
431             long lod = fieldValues.remove(MILLI_OF_DAY);
432             if (resolverStyle != ResolverStyle.LENIENT) {
433                 MILLI_OF_DAY.checkValidValue(lod);
434             }
435             updateCheckConflict(MILLI_OF_DAY, SECOND_OF_DAY, lod / 1_000);
436             updateCheckConflict(MILLI_OF_DAY, MILLI_OF_SECOND, lod % 1_000);
437         }
438         if (fieldValues.containsKey(SECOND_OF_DAY)) {
439             long sod = fieldValues.remove(SECOND_OF_DAY);
440             if (resolverStyle != ResolverStyle.LENIENT) {
441                 SECOND_OF_DAY.checkValidValue(sod);
442             }
443             updateCheckConflict(SECOND_OF_DAY, HOUR_OF_DAY, sod / 3600);
444             updateCheckConflict(SECOND_OF_DAY, MINUTE_OF_HOUR, (sod / 60) % 60);
445             updateCheckConflict(SECOND_OF_DAY, SECOND_OF_MINUTE, sod % 60);
446         }
447         if (fieldValues.containsKey(MINUTE_OF_DAY)) {
448             long mod = fieldValues.remove(MINUTE_OF_DAY);
449             if (resolverStyle != ResolverStyle.LENIENT) {
450                 MINUTE_OF_DAY.checkValidValue(mod);
451             }
452             updateCheckConflict(MINUTE_OF_DAY, HOUR_OF_DAY, mod / 60);
453             updateCheckConflict(MINUTE_OF_DAY, MINUTE_OF_HOUR, mod % 60);
454         }
455 
456         // combine partial second fields strictly, leaving lenient expansion to later
457         if (fieldValues.containsKey(NANO_OF_SECOND)) {
458             long nos = fieldValues.get(NANO_OF_SECOND);
459             if (resolverStyle != ResolverStyle.LENIENT) {
460                 NANO_OF_SECOND.checkValidValue(nos);
461             }
462             if (fieldValues.containsKey(MICRO_OF_SECOND)) {
463                 long cos = fieldValues.remove(MICRO_OF_SECOND);
464                 if (resolverStyle != ResolverStyle.LENIENT) {
465                     MICRO_OF_SECOND.checkValidValue(cos);
466                 }
467                 nos = cos * 1000 + (nos % 1000);
468                 updateCheckConflict(MICRO_OF_SECOND, NANO_OF_SECOND, nos);
469             }
470             if (fieldValues.containsKey(MILLI_OF_SECOND)) {
471                 long los = fieldValues.remove(MILLI_OF_SECOND);
472                 if (resolverStyle != ResolverStyle.LENIENT) {
473                     MILLI_OF_SECOND.checkValidValue(los);
474                 }
475                 updateCheckConflict(MILLI_OF_SECOND, NANO_OF_SECOND, los * 1_000_000L + (nos % 1_000_000L));
476             }
477         }
478 
479         if (dayPeriod != null && fieldValues.containsKey(HOUR_OF_AMPM)) {
480             long hoap = fieldValues.remove(HOUR_OF_AMPM);
481             if (resolverStyle != ResolverStyle.LENIENT) {
482                 HOUR_OF_AMPM.checkValidValue(hoap);
483             }
484             Long mohObj = fieldValues.get(MINUTE_OF_HOUR);
485             long moh = mohObj != null ? Math.floorMod(mohObj, 60) : 0;
486             long excessHours = dayPeriod.includes((Math.floorMod(hoap, 12) + 12) * 60 + moh) ? 12 : 0;
487             long hod = Math.addExact(hoap, excessHours);
488             updateCheckConflict(HOUR_OF_AMPM, HOUR_OF_DAY, hod);
489             dayPeriod = null;
490         }
491 
492         // convert to time if all four fields available (optimization)
493         if (fieldValues.containsKey(HOUR_OF_DAY) && fieldValues.containsKey(MINUTE_OF_HOUR) &&
494                 fieldValues.containsKey(SECOND_OF_MINUTE) && fieldValues.containsKey(NANO_OF_SECOND)) {
495             long hod = fieldValues.remove(HOUR_OF_DAY);
496             long moh = fieldValues.remove(MINUTE_OF_HOUR);
497             long som = fieldValues.remove(SECOND_OF_MINUTE);
498             long nos = fieldValues.remove(NANO_OF_SECOND);
499             resolveTime(hod, moh, som, nos);
500         }
501     }
502 
resolveTimeLenient()503     private void resolveTimeLenient() {
504         // leniently create a time from incomplete information
505         // done after everything else as it creates information from nothing
506         // which would break updateCheckConflict(field)
507 
508         if (time == null) {
509             // NANO_OF_SECOND merged with MILLI/MICRO above
510             if (fieldValues.containsKey(MILLI_OF_SECOND)) {
511                 long los = fieldValues.remove(MILLI_OF_SECOND);
512                 if (fieldValues.containsKey(MICRO_OF_SECOND)) {
513                     // merge milli-of-second and micro-of-second for better error message
514                     long cos = los * 1_000 + (fieldValues.get(MICRO_OF_SECOND) % 1_000);
515                     updateCheckConflict(MILLI_OF_SECOND, MICRO_OF_SECOND, cos);
516                     fieldValues.remove(MICRO_OF_SECOND);
517                     fieldValues.put(NANO_OF_SECOND, cos * 1_000L);
518                 } else {
519                     // convert milli-of-second to nano-of-second
520                     fieldValues.put(NANO_OF_SECOND, los * 1_000_000L);
521                 }
522             } else if (fieldValues.containsKey(MICRO_OF_SECOND)) {
523                 // convert micro-of-second to nano-of-second
524                 long cos = fieldValues.remove(MICRO_OF_SECOND);
525                 fieldValues.put(NANO_OF_SECOND, cos * 1_000L);
526             }
527 
528             // Set the hour-of-day, if not exist and not in STRICT, to the mid point of the day period or am/pm.
529             if (!fieldValues.containsKey(HOUR_OF_DAY) &&
530                     !fieldValues.containsKey(MINUTE_OF_HOUR) &&
531                     !fieldValues.containsKey(SECOND_OF_MINUTE) &&
532                     !fieldValues.containsKey(NANO_OF_SECOND) &&
533                     resolverStyle != ResolverStyle.STRICT) {
534                 if (dayPeriod != null) {
535                     long midpoint = dayPeriod.mid();
536                     resolveTime(midpoint / 60, midpoint % 60, 0, 0);
537                     dayPeriod = null;
538                 } else if (fieldValues.containsKey(AMPM_OF_DAY)) {
539                     long ap = fieldValues.remove(AMPM_OF_DAY);
540                     if (resolverStyle == ResolverStyle.LENIENT) {
541                         resolveTime(Math.addExact(Math.multiplyExact(ap, 12), 6), 0, 0, 0);
542                     } else {  // SMART
543                         AMPM_OF_DAY.checkValidValue(ap);
544                         resolveTime(ap * 12 + 6, 0, 0, 0);
545                     }
546                 }
547             }
548 
549             // merge hour/minute/second/nano leniently
550             Long hod = fieldValues.get(HOUR_OF_DAY);
551             if (hod != null) {
552                 Long moh = fieldValues.get(MINUTE_OF_HOUR);
553                 Long som = fieldValues.get(SECOND_OF_MINUTE);
554                 Long nos = fieldValues.get(NANO_OF_SECOND);
555 
556                 // check for invalid combinations that cannot be defaulted
557                 if ((moh == null && (som != null || nos != null)) ||
558                         (moh != null && som == null && nos != null)) {
559                     return;
560                 }
561 
562                 // default as necessary and build time
563                 long mohVal = (moh != null ? moh : 0);
564                 long somVal = (som != null ? som : 0);
565                 long nosVal = (nos != null ? nos : 0);
566 
567                 if (dayPeriod != null && resolverStyle != ResolverStyle.LENIENT) {
568                     // Check whether the hod/mohVal is within the day period
569                     if (!dayPeriod.includes(hod * 60 + mohVal)) {
570                         throw new DateTimeException("Conflict found: Resolved time %02d:%02d".formatted(hod, mohVal) +
571                                 " conflicts with " + dayPeriod);
572                     }
573                 }
574 
575                 resolveTime(hod, mohVal, somVal, nosVal);
576                 fieldValues.remove(HOUR_OF_DAY);
577                 fieldValues.remove(MINUTE_OF_HOUR);
578                 fieldValues.remove(SECOND_OF_MINUTE);
579                 fieldValues.remove(NANO_OF_SECOND);
580             }
581         }
582 
583         // validate remaining
584         if (resolverStyle != ResolverStyle.LENIENT && fieldValues.size() > 0) {
585             for (Entry<TemporalField, Long> entry : fieldValues.entrySet()) {
586                 TemporalField field = entry.getKey();
587                 if (field instanceof ChronoField && field.isTimeBased()) {
588                     ((ChronoField) field).checkValidValue(entry.getValue());
589                 }
590             }
591         }
592     }
593 
resolveTime(long hod, long moh, long som, long nos)594     private void resolveTime(long hod, long moh, long som, long nos) {
595         if (resolverStyle == ResolverStyle.LENIENT) {
596             long totalNanos = Math.multiplyExact(hod, 3600_000_000_000L);
597             totalNanos = Math.addExact(totalNanos, Math.multiplyExact(moh, 60_000_000_000L));
598             totalNanos = Math.addExact(totalNanos, Math.multiplyExact(som, 1_000_000_000L));
599             totalNanos = Math.addExact(totalNanos, nos);
600             int excessDays = (int) Math.floorDiv(totalNanos, 86400_000_000_000L);  // safe int cast
601             long nod = Math.floorMod(totalNanos, 86400_000_000_000L);
602             updateCheckConflict(LocalTime.ofNanoOfDay(nod), Period.ofDays(excessDays));
603         } else {  // STRICT or SMART
604             int mohVal = MINUTE_OF_HOUR.checkValidIntValue(moh);
605             int nosVal = NANO_OF_SECOND.checkValidIntValue(nos);
606             // handle 24:00 end of day
607             if (resolverStyle == ResolverStyle.SMART && hod == 24 && mohVal == 0 && som == 0 && nosVal == 0) {
608                 updateCheckConflict(LocalTime.MIDNIGHT, Period.ofDays(1));
609             } else {
610                 int hodVal = HOUR_OF_DAY.checkValidIntValue(hod);
611                 int somVal = SECOND_OF_MINUTE.checkValidIntValue(som);
612                 updateCheckConflict(LocalTime.of(hodVal, mohVal, somVal, nosVal), Period.ZERO);
613             }
614         }
615     }
616 
resolvePeriod()617     private void resolvePeriod() {
618         // add whole days if we have both date and time
619         if (date != null && time != null && excessDays.isZero() == false) {
620             date = date.plus(excessDays);
621             excessDays = Period.ZERO;
622         }
623     }
624 
resolveFractional()625     private void resolveFractional() {
626         // ensure fractional seconds available as ChronoField requires
627         // resolveTimeLenient() will have merged MICRO_OF_SECOND/MILLI_OF_SECOND to NANO_OF_SECOND
628         if (time == null &&
629                 (fieldValues.containsKey(INSTANT_SECONDS) ||
630                     fieldValues.containsKey(SECOND_OF_DAY) ||
631                     fieldValues.containsKey(SECOND_OF_MINUTE))) {
632             if (fieldValues.containsKey(NANO_OF_SECOND)) {
633                 long nos = fieldValues.get(NANO_OF_SECOND);
634                 fieldValues.put(MICRO_OF_SECOND, nos / 1000);
635                 fieldValues.put(MILLI_OF_SECOND, nos / 1000000);
636             } else {
637                 fieldValues.put(NANO_OF_SECOND, 0L);
638                 fieldValues.put(MICRO_OF_SECOND, 0L);
639                 fieldValues.put(MILLI_OF_SECOND, 0L);
640             }
641         }
642     }
643 
resolveInstant()644     private void resolveInstant() {
645         // add instant seconds (if not present) if we have date, time and zone
646         // Offset (if present) will be given priority over the zone.
647         if (!fieldValues.containsKey(INSTANT_SECONDS) && date != null && time != null) {
648             Long offsetSecs = fieldValues.get(OFFSET_SECONDS);
649             if (offsetSecs != null) {
650                 ZoneOffset offset = ZoneOffset.ofTotalSeconds(offsetSecs.intValue());
651                 long instant = date.atTime(time).atZone(offset).toEpochSecond();
652                 fieldValues.put(INSTANT_SECONDS, instant);
653             } else {
654                 if (zone != null) {
655                     long instant = date.atTime(time).atZone(zone).toEpochSecond();
656                     fieldValues.put(INSTANT_SECONDS, instant);
657                 }
658             }
659         }
660     }
661 
updateCheckConflict(LocalTime timeToSet, Period periodToSet)662     private void updateCheckConflict(LocalTime timeToSet, Period periodToSet) {
663         if (time != null) {
664             if (time.equals(timeToSet) == false) {
665                 throw new DateTimeException("Conflict found: Fields resolved to different times: " + time + " " + timeToSet);
666             }
667             if (excessDays.isZero() == false && periodToSet.isZero() == false && excessDays.equals(periodToSet) == false) {
668                 throw new DateTimeException("Conflict found: Fields resolved to different excess periods: " + excessDays + " " + periodToSet);
669             } else {
670                 excessDays = periodToSet;
671             }
672         } else {
673             time = timeToSet;
674             excessDays = periodToSet;
675         }
676     }
677 
678     //-----------------------------------------------------------------------
crossCheck()679     private void crossCheck() {
680         // only cross-check date, time and date-time
681         // avoid object creation if possible
682         if (date != null) {
683             crossCheck(date);
684         }
685         if (time != null) {
686             crossCheck(time);
687             if (date != null && fieldValues.size() > 0) {
688                 crossCheck(date.atTime(time));
689             }
690         }
691     }
692 
crossCheck(TemporalAccessor target)693     private void crossCheck(TemporalAccessor target) {
694         for (Iterator<Entry<TemporalField, Long>> it = fieldValues.entrySet().iterator(); it.hasNext(); ) {
695             Entry<TemporalField, Long> entry = it.next();
696             TemporalField field = entry.getKey();
697             if (target.isSupported(field)) {
698                 long val1;
699                 try {
700                     val1 = target.getLong(field);
701                 } catch (RuntimeException ex) {
702                     continue;
703                 }
704                 long val2 = entry.getValue();
705                 if (val1 != val2) {
706                     throw new DateTimeException("Conflict found: Field " + field + " " + val1 +
707                             " differs from " + field + " " + val2 + " derived from " + target);
708                 }
709                 it.remove();
710             }
711         }
712     }
713 
714     //-----------------------------------------------------------------------
715     @Override
toString()716     public String toString() {
717         StringBuilder buf = new StringBuilder(64);
718         buf.append(fieldValues).append(',').append(chrono);
719         if (zone != null) {
720             buf.append(',').append(zone);
721         }
722         if (date != null || time != null) {
723             buf.append(" resolved to ");
724             if (date != null) {
725                 buf.append(date);
726                 if (time != null) {
727                     buf.append('T').append(time);
728                 }
729             } else {
730                 buf.append(time);
731             }
732         }
733         return buf.toString();
734     }
735 
736 }
737