Home | History | Annotate | Download | only in format
      1 /*
      2  * Copyright (c) 2012, 2013, 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.temporal.ChronoField.AMPM_OF_DAY;
     65 import static java.time.temporal.ChronoField.CLOCK_HOUR_OF_AMPM;
     66 import static java.time.temporal.ChronoField.CLOCK_HOUR_OF_DAY;
     67 import static java.time.temporal.ChronoField.HOUR_OF_AMPM;
     68 import static java.time.temporal.ChronoField.HOUR_OF_DAY;
     69 import static java.time.temporal.ChronoField.INSTANT_SECONDS;
     70 import static java.time.temporal.ChronoField.MICRO_OF_DAY;
     71 import static java.time.temporal.ChronoField.MICRO_OF_SECOND;
     72 import static java.time.temporal.ChronoField.MILLI_OF_DAY;
     73 import static java.time.temporal.ChronoField.MILLI_OF_SECOND;
     74 import static java.time.temporal.ChronoField.MINUTE_OF_DAY;
     75 import static java.time.temporal.ChronoField.MINUTE_OF_HOUR;
     76 import static java.time.temporal.ChronoField.NANO_OF_DAY;
     77 import static java.time.temporal.ChronoField.NANO_OF_SECOND;
     78 import static java.time.temporal.ChronoField.OFFSET_SECONDS;
     79 import static java.time.temporal.ChronoField.SECOND_OF_DAY;
     80 import static java.time.temporal.ChronoField.SECOND_OF_MINUTE;
     81 
     82 import java.time.DateTimeException;
     83 import java.time.Instant;
     84 import java.time.LocalDate;
     85 import java.time.LocalTime;
     86 import java.time.Period;
     87 import java.time.ZoneId;
     88 import java.time.ZoneOffset;
     89 import java.time.chrono.ChronoLocalDate;
     90 import java.time.chrono.ChronoLocalDateTime;
     91 import java.time.chrono.ChronoZonedDateTime;
     92 import java.time.chrono.Chronology;
     93 import java.time.temporal.ChronoField;
     94 import java.time.temporal.TemporalAccessor;
     95 import java.time.temporal.TemporalField;
     96 import java.time.temporal.TemporalQueries;
     97 import java.time.temporal.TemporalQuery;
     98 import java.time.temporal.UnsupportedTemporalTypeException;
     99 import java.util.HashMap;
    100 import java.util.Iterator;
    101 import java.util.Map;
    102 import java.util.Map.Entry;
    103 import java.util.Objects;
    104 import java.util.Set;
    105 
    106 /**
    107  * A store of parsed data.
    108  * <p>
    109  * This class is used during parsing to collect the data. Part of the parsing process
    110  * involves handling optional blocks and multiple copies of the data get created to
    111  * support the necessary backtracking.
    112  * <p>
    113  * Once parsing is completed, this class can be used as the resultant {@code TemporalAccessor}.
    114  * In most cases, it is only exposed once the fields have been resolved.
    115  *
    116  * @implSpec
    117  * This class is a mutable context intended for use from a single thread.
    118  * Usage of the class is thread-safe within standard parsing as a new instance of this class
    119  * is automatically created for each parse and parsing is single-threaded
    120  *
    121  * @since 1.8
    122  */
    123 final class Parsed implements TemporalAccessor {
    124     // some fields are accessed using package scope from DateTimeParseContext
    125 
    126     /**
    127      * The parsed fields.
    128      */
    129     final Map<TemporalField, Long> fieldValues = new HashMap<>();
    130     /**
    131      * The parsed zone.
    132      */
    133     ZoneId zone;
    134     /**
    135      * The parsed chronology.
    136      */
    137     Chronology chrono;
    138     /**
    139      * Whether a leap-second is parsed.
    140      */
    141     boolean leapSecond;
    142     /**
    143      * The resolver style to use.
    144      */
    145     private ResolverStyle resolverStyle;
    146     /**
    147      * The resolved date.
    148      */
    149     private ChronoLocalDate date;
    150     /**
    151      * The resolved time.
    152      */
    153     private LocalTime time;
    154     /**
    155      * The excess period from time-only parsing.
    156      */
    157     Period excessDays = Period.ZERO;
    158 
    159     /**
    160      * Creates an instance.
    161      */
    162     Parsed() {
    163     }
    164 
    165     /**
    166      * Creates a copy.
    167      */
    168     Parsed copy() {
    169         // only copy fields used in parsing stage
    170         Parsed cloned = new Parsed();
    171         cloned.fieldValues.putAll(this.fieldValues);
    172         cloned.zone = this.zone;
    173         cloned.chrono = this.chrono;
    174         cloned.leapSecond = this.leapSecond;
    175         return cloned;
    176     }
    177 
    178     //-----------------------------------------------------------------------
    179     @Override
    180     public boolean isSupported(TemporalField field) {
    181         if (fieldValues.containsKey(field) ||
    182                 (date != null && date.isSupported(field)) ||
    183                 (time != null && time.isSupported(field))) {
    184             return true;
    185         }
    186         return field != null && (field instanceof ChronoField == false) && field.isSupportedBy(this);
    187     }
    188 
    189     @Override
    190     public long getLong(TemporalField field) {
    191         Objects.requireNonNull(field, "field");
    192         Long value = fieldValues.get(field);
    193         if (value != null) {
    194             return value;
    195         }
    196         if (date != null && date.isSupported(field)) {
    197             return date.getLong(field);
    198         }
    199         if (time != null && time.isSupported(field)) {
    200             return time.getLong(field);
    201         }
    202         if (field instanceof ChronoField) {
    203             throw new UnsupportedTemporalTypeException("Unsupported field: " + field);
    204         }
    205         return field.getFrom(this);
    206     }
    207 
    208     @SuppressWarnings("unchecked")
    209     @Override
    210     public <R> R query(TemporalQuery<R> query) {
    211         if (query == TemporalQueries.zoneId()) {
    212             return (R) zone;
    213         } else if (query == TemporalQueries.chronology()) {
    214             return (R) chrono;
    215         } else if (query == TemporalQueries.localDate()) {
    216             return (R) (date != null ? LocalDate.from(date) : null);
    217         } else if (query == TemporalQueries.localTime()) {
    218             return (R) time;
    219         } else if (query == TemporalQueries.zone() || query == TemporalQueries.offset()) {
    220             return query.queryFrom(this);
    221         } else if (query == TemporalQueries.precision()) {
    222             return null;  // not a complete date/time
    223         }
    224         // inline TemporalAccessor.super.query(query) as an optimization
    225         // non-JDK classes are not permitted to make this optimization
    226         return query.queryFrom(this);
    227     }
    228 
    229     //-----------------------------------------------------------------------
    230     /**
    231      * Resolves the fields in this context.
    232      *
    233      * @param resolverStyle  the resolver style, not null
    234      * @param resolverFields  the fields to use for resolving, null for all fields
    235      * @return this, for method chaining
    236      * @throws DateTimeException if resolving one field results in a value for
    237      *  another field that is in conflict
    238      */
    239     TemporalAccessor resolve(ResolverStyle resolverStyle, Set<TemporalField> resolverFields) {
    240         if (resolverFields != null) {
    241             fieldValues.keySet().retainAll(resolverFields);
    242         }
    243         this.resolverStyle = resolverStyle;
    244         resolveFields();
    245         resolveTimeLenient();
    246         crossCheck();
    247         resolvePeriod();
    248         resolveFractional();
    249         resolveInstant();
    250         return this;
    251     }
    252 
    253     //-----------------------------------------------------------------------
    254     private void resolveFields() {
    255         // resolve ChronoField
    256         resolveInstantFields();
    257         resolveDateFields();
    258         resolveTimeFields();
    259 
    260         // if any other fields, handle them
    261         // any lenient date resolution should return epoch-day
    262         if (fieldValues.size() > 0) {
    263             int changedCount = 0;
    264             outer:
    265             while (changedCount < 50) {
    266                 for (Map.Entry<TemporalField, Long> entry : fieldValues.entrySet()) {
    267                     TemporalField targetField = entry.getKey();
    268                     TemporalAccessor resolvedObject = targetField.resolve(fieldValues, this, resolverStyle);
    269                     if (resolvedObject != null) {
    270                         if (resolvedObject instanceof ChronoZonedDateTime) {
    271                             ChronoZonedDateTime<?> czdt = (ChronoZonedDateTime<?>) resolvedObject;
    272                             if (zone == null) {
    273                                 zone = czdt.getZone();
    274                             } else if (zone.equals(czdt.getZone()) == false) {
    275                                 throw new DateTimeException("ChronoZonedDateTime must use the effective parsed zone: " + zone);
    276                             }
    277                             resolvedObject = czdt.toLocalDateTime();
    278                         }
    279                         if (resolvedObject instanceof ChronoLocalDateTime) {
    280                             ChronoLocalDateTime<?> cldt = (ChronoLocalDateTime<?>) resolvedObject;
    281                             updateCheckConflict(cldt.toLocalTime(), Period.ZERO);
    282                             updateCheckConflict(cldt.toLocalDate());
    283                             changedCount++;
    284                             continue outer;  // have to restart to avoid concurrent modification
    285                         }
    286                         if (resolvedObject instanceof ChronoLocalDate) {
    287                             updateCheckConflict((ChronoLocalDate) resolvedObject);
    288                             changedCount++;
    289                             continue outer;  // have to restart to avoid concurrent modification
    290                         }
    291                         if (resolvedObject instanceof LocalTime) {
    292                             updateCheckConflict((LocalTime) resolvedObject, Period.ZERO);
    293                             changedCount++;
    294                             continue outer;  // have to restart to avoid concurrent modification
    295                         }
    296                         throw new DateTimeException("Method resolve() can only return ChronoZonedDateTime, " +
    297                                 "ChronoLocalDateTime, ChronoLocalDate or LocalTime");
    298                     } else if (fieldValues.containsKey(targetField) == false) {
    299                         changedCount++;
    300                         continue outer;  // have to restart to avoid concurrent modification
    301                     }
    302                 }
    303                 break;
    304             }
    305             if (changedCount == 50) {  // catch infinite loops
    306                 throw new DateTimeException("One of the parsed fields has an incorrectly implemented resolve method");
    307             }
    308             // if something changed then have to redo ChronoField resolve
    309             if (changedCount > 0) {
    310                 resolveInstantFields();
    311                 resolveDateFields();
    312                 resolveTimeFields();
    313             }
    314         }
    315     }
    316 
    317     private void updateCheckConflict(TemporalField targetField, TemporalField changeField, Long changeValue) {
    318         Long old = fieldValues.put(changeField, changeValue);
    319         if (old != null && old.longValue() != changeValue.longValue()) {
    320             throw new DateTimeException("Conflict found: " + changeField + " " + old +
    321                     " differs from " + changeField + " " + changeValue +
    322                     " while resolving  " + targetField);
    323         }
    324     }
    325 
    326     //-----------------------------------------------------------------------
    327     private void resolveInstantFields() {
    328         // resolve parsed instant seconds to date and time if zone available
    329         if (fieldValues.containsKey(INSTANT_SECONDS)) {
    330             if (zone != null) {
    331                 resolveInstantFields0(zone);
    332             } else {
    333                 Long offsetSecs = fieldValues.get(OFFSET_SECONDS);
    334                 if (offsetSecs != null) {
    335                     ZoneOffset offset = ZoneOffset.ofTotalSeconds(offsetSecs.intValue());
    336                     resolveInstantFields0(offset);
    337                 }
    338             }
    339         }
    340     }
    341 
    342     private void resolveInstantFields0(ZoneId selectedZone) {
    343         Instant instant = Instant.ofEpochSecond(fieldValues.remove(INSTANT_SECONDS));
    344         ChronoZonedDateTime<?> zdt = chrono.zonedDateTime(instant, selectedZone);
    345         updateCheckConflict(zdt.toLocalDate());
    346         updateCheckConflict(INSTANT_SECONDS, SECOND_OF_DAY, (long) zdt.toLocalTime().toSecondOfDay());
    347     }
    348 
    349     //-----------------------------------------------------------------------
    350     private void resolveDateFields() {
    351         updateCheckConflict(chrono.resolveDate(fieldValues, resolverStyle));
    352     }
    353 
    354     private void updateCheckConflict(ChronoLocalDate cld) {
    355         if (date != null) {
    356             if (cld != null && date.equals(cld) == false) {
    357                 throw new DateTimeException("Conflict found: Fields resolved to two different dates: " + date + " " + cld);
    358             }
    359         } else if (cld != null) {
    360             if (chrono.equals(cld.getChronology()) == false) {
    361                 throw new DateTimeException("ChronoLocalDate must use the effective parsed chronology: " + chrono);
    362             }
    363             date = cld;
    364         }
    365     }
    366 
    367     //-----------------------------------------------------------------------
    368     private void resolveTimeFields() {
    369         // simplify fields
    370         if (fieldValues.containsKey(CLOCK_HOUR_OF_DAY)) {
    371             // lenient allows anything, smart allows 0-24, strict allows 1-24
    372             long ch = fieldValues.remove(CLOCK_HOUR_OF_DAY);
    373             if (resolverStyle == ResolverStyle.STRICT || (resolverStyle == ResolverStyle.SMART && ch != 0)) {
    374                 CLOCK_HOUR_OF_DAY.checkValidValue(ch);
    375             }
    376             updateCheckConflict(CLOCK_HOUR_OF_DAY, HOUR_OF_DAY, ch == 24 ? 0 : ch);
    377         }
    378         if (fieldValues.containsKey(CLOCK_HOUR_OF_AMPM)) {
    379             // lenient allows anything, smart allows 0-12, strict allows 1-12
    380             long ch = fieldValues.remove(CLOCK_HOUR_OF_AMPM);
    381             if (resolverStyle == ResolverStyle.STRICT || (resolverStyle == ResolverStyle.SMART && ch != 0)) {
    382                 CLOCK_HOUR_OF_AMPM.checkValidValue(ch);
    383             }
    384             updateCheckConflict(CLOCK_HOUR_OF_AMPM, HOUR_OF_AMPM, ch == 12 ? 0 : ch);
    385         }
    386         if (fieldValues.containsKey(AMPM_OF_DAY) && fieldValues.containsKey(HOUR_OF_AMPM)) {
    387             long ap = fieldValues.remove(AMPM_OF_DAY);
    388             long hap = fieldValues.remove(HOUR_OF_AMPM);
    389             if (resolverStyle == ResolverStyle.LENIENT) {
    390                 updateCheckConflict(AMPM_OF_DAY, HOUR_OF_DAY, Math.addExact(Math.multiplyExact(ap, 12), hap));
    391             } else {  // STRICT or SMART
    392                 AMPM_OF_DAY.checkValidValue(ap);
    393                 HOUR_OF_AMPM.checkValidValue(ap);
    394                 updateCheckConflict(AMPM_OF_DAY, HOUR_OF_DAY, ap * 12 + hap);
    395             }
    396         }
    397         if (fieldValues.containsKey(NANO_OF_DAY)) {
    398             long nod = fieldValues.remove(NANO_OF_DAY);
    399             if (resolverStyle != ResolverStyle.LENIENT) {
    400                 NANO_OF_DAY.checkValidValue(nod);
    401             }
    402             updateCheckConflict(NANO_OF_DAY, HOUR_OF_DAY, nod / 3600_000_000_000L);
    403             updateCheckConflict(NANO_OF_DAY, MINUTE_OF_HOUR, (nod / 60_000_000_000L) % 60);
    404             updateCheckConflict(NANO_OF_DAY, SECOND_OF_MINUTE, (nod / 1_000_000_000L) % 60);
    405             updateCheckConflict(NANO_OF_DAY, NANO_OF_SECOND, nod % 1_000_000_000L);
    406         }
    407         if (fieldValues.containsKey(MICRO_OF_DAY)) {
    408             long cod = fieldValues.remove(MICRO_OF_DAY);
    409             if (resolverStyle != ResolverStyle.LENIENT) {
    410                 MICRO_OF_DAY.checkValidValue(cod);
    411             }
    412             updateCheckConflict(MICRO_OF_DAY, SECOND_OF_DAY, cod / 1_000_000L);
    413             updateCheckConflict(MICRO_OF_DAY, MICRO_OF_SECOND, cod % 1_000_000L);
    414         }
    415         if (fieldValues.containsKey(MILLI_OF_DAY)) {
    416             long lod = fieldValues.remove(MILLI_OF_DAY);
    417             if (resolverStyle != ResolverStyle.LENIENT) {
    418                 MILLI_OF_DAY.checkValidValue(lod);
    419             }
    420             updateCheckConflict(MILLI_OF_DAY, SECOND_OF_DAY, lod / 1_000);
    421             updateCheckConflict(MILLI_OF_DAY, MILLI_OF_SECOND, lod % 1_000);
    422         }
    423         if (fieldValues.containsKey(SECOND_OF_DAY)) {
    424             long sod = fieldValues.remove(SECOND_OF_DAY);
    425             if (resolverStyle != ResolverStyle.LENIENT) {
    426                 SECOND_OF_DAY.checkValidValue(sod);
    427             }
    428             updateCheckConflict(SECOND_OF_DAY, HOUR_OF_DAY, sod / 3600);
    429             updateCheckConflict(SECOND_OF_DAY, MINUTE_OF_HOUR, (sod / 60) % 60);
    430             updateCheckConflict(SECOND_OF_DAY, SECOND_OF_MINUTE, sod % 60);
    431         }
    432         if (fieldValues.containsKey(MINUTE_OF_DAY)) {
    433             long mod = fieldValues.remove(MINUTE_OF_DAY);
    434             if (resolverStyle != ResolverStyle.LENIENT) {
    435                 MINUTE_OF_DAY.checkValidValue(mod);
    436             }
    437             updateCheckConflict(MINUTE_OF_DAY, HOUR_OF_DAY, mod / 60);
    438             updateCheckConflict(MINUTE_OF_DAY, MINUTE_OF_HOUR, mod % 60);
    439         }
    440 
    441         // combine partial second fields strictly, leaving lenient expansion to later
    442         if (fieldValues.containsKey(NANO_OF_SECOND)) {
    443             long nos = fieldValues.get(NANO_OF_SECOND);
    444             if (resolverStyle != ResolverStyle.LENIENT) {
    445                 NANO_OF_SECOND.checkValidValue(nos);
    446             }
    447             if (fieldValues.containsKey(MICRO_OF_SECOND)) {
    448                 long cos = fieldValues.remove(MICRO_OF_SECOND);
    449                 if (resolverStyle != ResolverStyle.LENIENT) {
    450                     MICRO_OF_SECOND.checkValidValue(cos);
    451                 }
    452                 nos = cos * 1000 + (nos % 1000);
    453                 updateCheckConflict(MICRO_OF_SECOND, NANO_OF_SECOND, nos);
    454             }
    455             if (fieldValues.containsKey(MILLI_OF_SECOND)) {
    456                 long los = fieldValues.remove(MILLI_OF_SECOND);
    457                 if (resolverStyle != ResolverStyle.LENIENT) {
    458                     MILLI_OF_SECOND.checkValidValue(los);
    459                 }
    460                 updateCheckConflict(MILLI_OF_SECOND, NANO_OF_SECOND, los * 1_000_000L + (nos % 1_000_000L));
    461             }
    462         }
    463 
    464         // convert to time if all four fields available (optimization)
    465         if (fieldValues.containsKey(HOUR_OF_DAY) && fieldValues.containsKey(MINUTE_OF_HOUR) &&
    466                 fieldValues.containsKey(SECOND_OF_MINUTE) && fieldValues.containsKey(NANO_OF_SECOND)) {
    467             long hod = fieldValues.remove(HOUR_OF_DAY);
    468             long moh = fieldValues.remove(MINUTE_OF_HOUR);
    469             long som = fieldValues.remove(SECOND_OF_MINUTE);
    470             long nos = fieldValues.remove(NANO_OF_SECOND);
    471             resolveTime(hod, moh, som, nos);
    472         }
    473     }
    474 
    475     private void resolveTimeLenient() {
    476         // leniently create a time from incomplete information
    477         // done after everything else as it creates information from nothing
    478         // which would break updateCheckConflict(field)
    479 
    480         if (time == null) {
    481             // NANO_OF_SECOND merged with MILLI/MICRO above
    482             if (fieldValues.containsKey(MILLI_OF_SECOND)) {
    483                 long los = fieldValues.remove(MILLI_OF_SECOND);
    484                 if (fieldValues.containsKey(MICRO_OF_SECOND)) {
    485                     // merge milli-of-second and micro-of-second for better error message
    486                     long cos = los * 1_000 + (fieldValues.get(MICRO_OF_SECOND) % 1_000);
    487                     updateCheckConflict(MILLI_OF_SECOND, MICRO_OF_SECOND, cos);
    488                     fieldValues.remove(MICRO_OF_SECOND);
    489                     fieldValues.put(NANO_OF_SECOND, cos * 1_000L);
    490                 } else {
    491                     // convert milli-of-second to nano-of-second
    492                     fieldValues.put(NANO_OF_SECOND, los * 1_000_000L);
    493                 }
    494             } else if (fieldValues.containsKey(MICRO_OF_SECOND)) {
    495                 // convert micro-of-second to nano-of-second
    496                 long cos = fieldValues.remove(MICRO_OF_SECOND);
    497                 fieldValues.put(NANO_OF_SECOND, cos * 1_000L);
    498             }
    499 
    500             // merge hour/minute/second/nano leniently
    501             Long hod = fieldValues.get(HOUR_OF_DAY);
    502             if (hod != null) {
    503                 Long moh = fieldValues.get(MINUTE_OF_HOUR);
    504                 Long som = fieldValues.get(SECOND_OF_MINUTE);
    505                 Long nos = fieldValues.get(NANO_OF_SECOND);
    506 
    507                 // check for invalid combinations that cannot be defaulted
    508                 if ((moh == null && (som != null || nos != null)) ||
    509                         (moh != null && som == null && nos != null)) {
    510                     return;
    511                 }
    512 
    513                 // default as necessary and build time
    514                 long mohVal = (moh != null ? moh : 0);
    515                 long somVal = (som != null ? som : 0);
    516                 long nosVal = (nos != null ? nos : 0);
    517                 resolveTime(hod, mohVal, somVal, nosVal);
    518                 fieldValues.remove(HOUR_OF_DAY);
    519                 fieldValues.remove(MINUTE_OF_HOUR);
    520                 fieldValues.remove(SECOND_OF_MINUTE);
    521                 fieldValues.remove(NANO_OF_SECOND);
    522             }
    523         }
    524 
    525         // validate remaining
    526         if (resolverStyle != ResolverStyle.LENIENT && fieldValues.size() > 0) {
    527             for (Entry<TemporalField, Long> entry : fieldValues.entrySet()) {
    528                 TemporalField field = entry.getKey();
    529                 if (field instanceof ChronoField && field.isTimeBased()) {
    530                     ((ChronoField) field).checkValidValue(entry.getValue());
    531                 }
    532             }
    533         }
    534     }
    535 
    536     private void resolveTime(long hod, long moh, long som, long nos) {
    537         if (resolverStyle == ResolverStyle.LENIENT) {
    538             long totalNanos = Math.multiplyExact(hod, 3600_000_000_000L);
    539             totalNanos = Math.addExact(totalNanos, Math.multiplyExact(moh, 60_000_000_000L));
    540             totalNanos = Math.addExact(totalNanos, Math.multiplyExact(som, 1_000_000_000L));
    541             totalNanos = Math.addExact(totalNanos, nos);
    542             int excessDays = (int) Math.floorDiv(totalNanos, 86400_000_000_000L);  // safe int cast
    543             long nod = Math.floorMod(totalNanos, 86400_000_000_000L);
    544             updateCheckConflict(LocalTime.ofNanoOfDay(nod), Period.ofDays(excessDays));
    545         } else {  // STRICT or SMART
    546             int mohVal = MINUTE_OF_HOUR.checkValidIntValue(moh);
    547             int nosVal = NANO_OF_SECOND.checkValidIntValue(nos);
    548             // handle 24:00 end of day
    549             if (resolverStyle == ResolverStyle.SMART && hod == 24 && mohVal == 0 && som == 0 && nosVal == 0) {
    550                 updateCheckConflict(LocalTime.MIDNIGHT, Period.ofDays(1));
    551             } else {
    552                 int hodVal = HOUR_OF_DAY.checkValidIntValue(hod);
    553                 int somVal = SECOND_OF_MINUTE.checkValidIntValue(som);
    554                 updateCheckConflict(LocalTime.of(hodVal, mohVal, somVal, nosVal), Period.ZERO);
    555             }
    556         }
    557     }
    558 
    559     private void resolvePeriod() {
    560         // add whole days if we have both date and time
    561         if (date != null && time != null && excessDays.isZero() == false) {
    562             date = date.plus(excessDays);
    563             excessDays = Period.ZERO;
    564         }
    565     }
    566 
    567     private void resolveFractional() {
    568         // ensure fractional seconds available as ChronoField requires
    569         // resolveTimeLenient() will have merged MICRO_OF_SECOND/MILLI_OF_SECOND to NANO_OF_SECOND
    570         if (time == null &&
    571                 (fieldValues.containsKey(INSTANT_SECONDS) ||
    572                     fieldValues.containsKey(SECOND_OF_DAY) ||
    573                     fieldValues.containsKey(SECOND_OF_MINUTE))) {
    574             if (fieldValues.containsKey(NANO_OF_SECOND)) {
    575                 long nos = fieldValues.get(NANO_OF_SECOND);
    576                 fieldValues.put(MICRO_OF_SECOND, nos / 1000);
    577                 fieldValues.put(MILLI_OF_SECOND, nos / 1000000);
    578             } else {
    579                 fieldValues.put(NANO_OF_SECOND, 0L);
    580                 fieldValues.put(MICRO_OF_SECOND, 0L);
    581                 fieldValues.put(MILLI_OF_SECOND, 0L);
    582             }
    583         }
    584     }
    585 
    586     private void resolveInstant() {
    587         // add instant seconds if we have date, time and zone
    588         if (date != null && time != null) {
    589             if (zone != null) {
    590                 long instant = date.atTime(time).atZone(zone).getLong(ChronoField.INSTANT_SECONDS);
    591                 fieldValues.put(INSTANT_SECONDS, instant);
    592             } else {
    593                 Long offsetSecs = fieldValues.get(OFFSET_SECONDS);
    594                 if (offsetSecs != null) {
    595                     ZoneOffset offset = ZoneOffset.ofTotalSeconds(offsetSecs.intValue());
    596                     long instant = date.atTime(time).atZone(offset).getLong(ChronoField.INSTANT_SECONDS);
    597                     fieldValues.put(INSTANT_SECONDS, instant);
    598                 }
    599             }
    600         }
    601     }
    602 
    603     private void updateCheckConflict(LocalTime timeToSet, Period periodToSet) {
    604         if (time != null) {
    605             if (time.equals(timeToSet) == false) {
    606                 throw new DateTimeException("Conflict found: Fields resolved to different times: " + time + " " + timeToSet);
    607             }
    608             if (excessDays.isZero() == false && periodToSet.isZero() == false && excessDays.equals(periodToSet) == false) {
    609                 throw new DateTimeException("Conflict found: Fields resolved to different excess periods: " + excessDays + " " + periodToSet);
    610             } else {
    611                 excessDays = periodToSet;
    612             }
    613         } else {
    614             time = timeToSet;
    615             excessDays = periodToSet;
    616         }
    617     }
    618 
    619     //-----------------------------------------------------------------------
    620     private void crossCheck() {
    621         // only cross-check date, time and date-time
    622         // avoid object creation if possible
    623         if (date != null) {
    624             crossCheck(date);
    625         }
    626         if (time != null) {
    627             crossCheck(time);
    628             if (date != null && fieldValues.size() > 0) {
    629                 crossCheck(date.atTime(time));
    630             }
    631         }
    632     }
    633 
    634     private void crossCheck(TemporalAccessor target) {
    635         for (Iterator<Entry<TemporalField, Long>> it = fieldValues.entrySet().iterator(); it.hasNext(); ) {
    636             Entry<TemporalField, Long> entry = it.next();
    637             TemporalField field = entry.getKey();
    638             if (target.isSupported(field)) {
    639                 long val1;
    640                 try {
    641                     val1 = target.getLong(field);
    642                 } catch (RuntimeException ex) {
    643                     continue;
    644                 }
    645                 long val2 = entry.getValue();
    646                 if (val1 != val2) {
    647                     throw new DateTimeException("Conflict found: Field " + field + " " + val1 +
    648                             " differs from " + field + " " + val2 + " derived from " + target);
    649                 }
    650                 it.remove();
    651             }
    652         }
    653     }
    654 
    655     //-----------------------------------------------------------------------
    656     @Override
    657     public String toString() {
    658         StringBuilder buf = new StringBuilder(64);
    659         buf.append(fieldValues).append(',').append(chrono);
    660         if (zone != null) {
    661             buf.append(',').append(zone);
    662         }
    663         if (date != null || time != null) {
    664             buf.append(" resolved to ");
    665             if (date != null) {
    666                 buf.append(date);
    667                 if (time != null) {
    668                     buf.append('T').append(time);
    669                 }
    670             } else {
    671                 buf.append(time);
    672             }
    673         }
    674         return buf.toString();
    675     }
    676 
    677 }
    678