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