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