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.Period; 13 14 import hunt.time.temporal.ChronoUnit; 15 16 import hunt.stream.DataInput; 17 import hunt.stream.DataOutput; 18 import hunt.Exceptions; 19 20 //import hunt.io.ObjectInputStream; 21 import hunt.stream.Common; 22 import hunt.time.chrono.ChronoLocalDate; 23 import hunt.time.chrono.ChronoPeriod; 24 import hunt.time.chrono.Chronology; 25 import hunt.time.chrono.IsoChronology; 26 import hunt.time.format.DateTimeParseException; 27 import hunt.time.temporal.ChronoUnit; 28 import hunt.time.temporal.Temporal; 29 import hunt.time.temporal.TemporalAccessor; 30 import hunt.time.temporal.TemporalAmount; 31 import hunt.time.temporal.TemporalQueries; 32 import hunt.time.temporal.TemporalUnit; 33 import hunt.time.Exceptions; 34 import hunt.collection.List; 35 import hunt.time.LocalDate; 36 import hunt.util.Comparator; 37 import hunt.time.Exceptions; 38 import hunt.Integer; 39 import hunt.Long; 40 import hunt.math.Helper; 41 import hunt.collection; 42 import hunt.time.Ser; 43 import hunt.text.Common; 44 import std.conv; 45 import std.regex; 46 import hunt.util.StringBuilder; 47 import hunt.time.util.QueryHelper; 48 import hunt.time.util.Common; 49 // import hunt.util.regex.Matcher; 50 // import hunt.util.regex.Pattern; 51 52 /** 53 * A date-based amount of time _in the ISO-8601 calendar system, 54 * such as '2 years, 3 months and 4 days'. 55 * !(p) 56 * This class models a quantity or amount of time _in terms of years, months and days. 57 * See {@link Duration} for the time-based equivalent to this class. 58 * !(p) 59 * Durations and periods differ _in their treatment of daylight savings time 60 * when added to {@link ZonedDateTime}. A {@code Duration} will add an exact 61 * number of seconds, thus a duration of one day is always exactly 24 hours. 62 * By contrast, a {@code Period} will add a conceptual day, trying to maintain 63 * the local time. 64 * !(p) 65 * For example, consider adding a period of one day and a duration of one day to 66 * 18:00 on the evening before a daylight savings gap. The {@code Period} will add 67 * the conceptual day and result _in a {@code ZonedDateTime} at 18:00 the following day. 68 * By contrast, the {@code Duration} will add exactly 24 hours, resulting _in a 69 * {@code ZonedDateTime} at 19:00 the following day (assuming a one hour DST gap). 70 * !(p) 71 * The supported units of a period are {@link ChronoUnit#YEARS YEARS}, 72 * {@link ChronoUnit#MONTHS MONTHS} and {@link ChronoUnit#DAYS DAYS}. 73 * All three fields are always present, but may be set to zero. 74 * !(p) 75 * The ISO-8601 calendar system is the modern civil calendar system used today 76 * _in most of the world. It is equivalent to the proleptic Gregorian calendar 77 * system, _in which today's rules for leap years are applied for all time. 78 * !(p) 79 * The period is modeled as a directed amount of time, meaning that individual parts of the 80 * period may be negative. 81 * 82 * !(p) 83 * This is a <a href="{@docRoot}/java.base/java/lang/doc-files/ValueBased.html">value-based</a> 84 * class; use of identity-sensitive operations (including reference equality 85 * ({@code ==}), identity hash code, or synchronization) on instances of 86 * {@code Period} may have unpredictable results and should be avoided. 87 * The {@code equals} method should be used for comparisons. 88 * 89 * @implSpec 90 * This class is immutable and thread-safe. 91 * 92 * @since 1.8 93 */ 94 public final class Period 95 : ChronoPeriod { // , Serializable 96 97 /** 98 * A constant for a period of zero. 99 */ 100 // public __gshared Period ZERO; 101 /** 102 * Serialization version. 103 */ 104 private enum long serialVersionUID = -3587258372562876L; 105 /** 106 * The pattern for parsing. 107 */ 108 private enum PATTERN = 109 "([-+]?)P(?:([-+]?[0-9]+)Y)?(?:([-+]?[0-9]+)M)?(?:([-+]?[0-9]+)W)?(?:([-+]?[0-9]+)D)?"; 110 111 /** 112 * The set of supported units. 113 */ 114 __gshared List!(TemporalUnit) _SUPPORTED_UNITS; 115 116 /** 117 * The number of years. 118 */ 119 private int years; 120 /** 121 * The number of months. 122 */ 123 private int months; 124 /** 125 * The number of days. 126 */ 127 private int days; 128 129 public static ref List!(TemporalUnit) SUPPORTED_UNITS() 130 { 131 if(_SUPPORTED_UNITS is null) 132 { 133 _SUPPORTED_UNITS = new ArrayList!TemporalUnit(); 134 _SUPPORTED_UNITS.add(ChronoUnit.YEARS); 135 _SUPPORTED_UNITS.add(ChronoUnit.MONTHS); 136 _SUPPORTED_UNITS.add(ChronoUnit.DAYS); 137 } 138 return _SUPPORTED_UNITS; 139 } 140 // shared static this() 141 // { 142 // ZERO = new Period(0, 0, 0); 143 mixin(MakeGlobalVar!(Period)("ZERO",`new Period(0, 0, 0)`)); 144 145 // } 146 147 //----------------------------------------------------------------------- 148 /** 149 * Obtains a {@code Period} representing a number of years. 150 * !(p) 151 * The resulting period will have the specified years. 152 * The months and days units will be zero. 153 * 154 * @param years the number of years, positive or negative 155 * @return the period of years, not null 156 */ 157 public static Period ofYears(int years) { 158 return create(years, 0, 0); 159 } 160 161 /** 162 * Obtains a {@code Period} representing a number of months. 163 * !(p) 164 * The resulting period will have the specified months. 165 * The years and days units will be zero. 166 * 167 * @param months the number of months, positive or negative 168 * @return the period of months, not null 169 */ 170 public static Period ofMonths(int months) { 171 return create(0, months, 0); 172 } 173 174 /** 175 * Obtains a {@code Period} representing a number of weeks. 176 * !(p) 177 * The resulting period will be day-based, with the amount of days 178 * equal to the number of weeks multiplied by 7. 179 * The years and months units will be zero. 180 * 181 * @param weeks the number of weeks, positive or negative 182 * @return the period, with the input weeks converted to days, not null 183 */ 184 public static Period ofWeeks(int weeks) { 185 return create(0, 0, MathHelper.multiplyExact(weeks, 7)); 186 } 187 188 /** 189 * Obtains a {@code Period} representing a number of days. 190 * !(p) 191 * The resulting period will have the specified days. 192 * The years and months units will be zero. 193 * 194 * @param days the number of days, positive or negative 195 * @return the period of days, not null 196 */ 197 public static Period ofDays(int days) { 198 return create(0, 0, days); 199 } 200 201 //----------------------------------------------------------------------- 202 /** 203 * Obtains a {@code Period} representing a number of years, months and days. 204 * !(p) 205 * This creates an instance based on years, months and days. 206 * 207 * @param years the amount of years, may be negative 208 * @param months the amount of months, may be negative 209 * @param days the amount of days, may be negative 210 * @return the period of years, months and days, not null 211 */ 212 public static Period of(int years, int months, int days) { 213 return create(years, months, days); 214 } 215 216 //----------------------------------------------------------------------- 217 /** 218 * Obtains an instance of {@code Period} from a temporal amount. 219 * !(p) 220 * This obtains a period based on the specified amount. 221 * A {@code TemporalAmount} represents an amount of time, which may be 222 * date-based or time-based, which this factory extracts to a {@code Period}. 223 * !(p) 224 * The conversion loops around the set of units from the amount and uses 225 * the {@link ChronoUnit#YEARS YEARS}, {@link ChronoUnit#MONTHS MONTHS} 226 * and {@link ChronoUnit#DAYS DAYS} units to create a period. 227 * If any other units are found then an exception is thrown. 228 * !(p) 229 * If the amount is a {@code ChronoPeriod} then it must use the ISO chronology. 230 * 231 * @param amount the temporal amount to convert, not null 232 * @return the equivalent period, not null 233 * @throws DateTimeException if unable to convert to a {@code Period} 234 * @throws ArithmeticException if the amount of years, months or days exceeds an int 235 */ 236 public static Period from(TemporalAmount amount) { 237 if (cast(Period)(amount) !is null) { 238 return cast(Period) amount; 239 } 240 if (cast(ChronoPeriod)(amount) !is null) { 241 if ((IsoChronology.INSTANCE == (cast(ChronoPeriod) amount).getChronology()) == false) { 242 throw new DateTimeException("Period requires ISO chronology: " ~ typeid(amount).name); 243 } 244 } 245 assert(amount, "amount"); 246 int years = 0; 247 int months = 0; 248 int days = 0; 249 foreach(TemporalUnit unit ; amount.getUnits()) { 250 long unitAmount = amount.get(unit); 251 if (unit == ChronoUnit.YEARS) { 252 years = MathHelper.toIntExact(unitAmount); 253 } else if (unit == ChronoUnit.MONTHS) { 254 months = MathHelper.toIntExact(unitAmount); 255 } else if (unit == ChronoUnit.DAYS) { 256 days = MathHelper.toIntExact(unitAmount); 257 } else { 258 throw new DateTimeException("Unit must be Years, Months or Days, but was " ~ typeid(unit).name); 259 } 260 } 261 return create(years, months, days); 262 } 263 264 //----------------------------------------------------------------------- 265 /** 266 * Obtains a {@code Period} from a text string such as {@code PnYnMnD}. 267 * !(p) 268 * This will parse the string produced by {@code toString()} which is 269 * based on the ISO-8601 period formats {@code PnYnMnD} and {@code PnW}. 270 * !(p) 271 * The string starts with an optional sign, denoted by the ASCII negative 272 * or positive symbol. If negative, the whole period is negated. 273 * The ASCII letter "P" is next _in upper or lower case. 274 * There are then four sections, each consisting of a number and a suffix. 275 * At least one of the four sections must be present. 276 * The sections have suffixes _in ASCII of "Y", "M", "W" and "D" for 277 * years, months, weeks and days, accepted _in upper or lower case. 278 * The suffixes must occur _in order. 279 * The number part of each section must consist of ASCII digits. 280 * The number may be prefixed by the ASCII negative or positive symbol. 281 * The number must parse to an {@code int}. 282 * !(p) 283 * The leading plus/minus sign, and negative values for other units are 284 * not part of the ISO-8601 standard. In addition, ISO-8601 does not 285 * permit mixing between the {@code PnYnMnD} and {@code PnW} formats. 286 * Any week-based input is multiplied by 7 and treated as a number of days. 287 * !(p) 288 * For example, the following are valid inputs: 289 * !(pre) 290 * "P2Y" -- Period.ofYears(2) 291 * "P3M" -- Period.ofMonths(3) 292 * "P4W" -- Period.ofWeeks(4) 293 * "P5D" -- Period.ofDays(5) 294 * "P1Y2M3D" -- Period.of(1, 2, 3) 295 * "P1Y2M3W4D" -- Period.of(1, 2, 25) 296 * "P-1Y2M" -- Period.of(-1, 2, 0) 297 * "-P1Y2M" -- Period.of(-1, -2, 0) 298 * </pre> 299 * 300 * @param text the text to parse, not null 301 * @return the parsed period, not null 302 * @throws DateTimeParseException if the text cannot be parsed to a period 303 */ 304 public static Period parse(string text) { 305 assert(text, "text"); 306 auto matchers = matchAll(text,PATTERN); 307 if (!matchers.empty()) { 308 auto matcher = matchers.front(); 309 310 int negate = (charMatch(text, matcher.captures[0], '-') ? -1 : 1); 311 string yearStart = matcher.captures[1]; 312 string monthStart = matcher.captures[2]; 313 string weekStart = matcher.captures[3]; 314 string dayStart = matcher.captures[4]; 315 if (yearStart.length >= 0 || monthStart.length >= 0 || weekStart.length >= 0 || dayStart.length >= 0) { 316 try { 317 int years = parseNumber(text, yearStart, negate); 318 int months = parseNumber(text, monthStart, negate); 319 int weeks = parseNumber(text, weekStart, negate); 320 int days = parseNumber(text, dayStart, negate); 321 days = MathHelper.addExact(days, MathHelper.multiplyExact(weeks, 7)); 322 return create(years, months, days); 323 } catch (NumberFormatException ex) { 324 throw new DateTimeParseException("Text cannot be parsed to a Period", text, 0, ex); 325 } 326 } 327 } 328 throw new DateTimeParseException("Text cannot be parsed to a Period", text, 0); 329 } 330 331 private static bool charMatch(string text, string data, char c) { 332 return (data.length == 1 && data[0] == c); 333 } 334 335 private static int parseNumber(string text, string data ,int negate) { 336 import std.string : isNumeric; 337 if (!isNumeric(data) || data.length == 0) { 338 return 0; 339 } 340 int val = to!int(data); 341 try { 342 return MathHelper.multiplyExact(val, negate); 343 } catch (ArithmeticException ex) { 344 throw new DateTimeParseException("Text cannot be parsed to a Period", text, 0, ex); 345 } 346 } 347 348 //----------------------------------------------------------------------- 349 /** 350 * Obtains a {@code Period} consisting of the number of years, months, 351 * and days between two dates. 352 * !(p) 353 * The start date is included, but the end date is not. 354 * The period is calculated by removing complete months, then calculating 355 * the remaining number of days, adjusting to ensure that both have the same sign. 356 * The number of months is then split into years and months based on a 12 month year. 357 * A month is considered if the end day-of-month is greater than or equal to the start day-of-month. 358 * For example, from {@code 2010-01-15} to {@code 2011-03-18} is one year, two months and three days. 359 * !(p) 360 * The result of this method can be a negative period if the end is before the start. 361 * The negative sign will be the same _in each of year, month and day. 362 * 363 * @param startDateInclusive the start date, inclusive, not null 364 * @param endDateExclusive the end date, exclusive, not null 365 * @return the period between this date and the end date, not null 366 * @see ChronoLocalDate#until(ChronoLocalDate) 367 */ 368 public static Period between(LocalDate startDateInclusive, LocalDate endDateExclusive) { 369 return startDateInclusive.until(endDateExclusive); 370 } 371 372 //----------------------------------------------------------------------- 373 /** 374 * Creates an instance. 375 * 376 * @param years the amount 377 * @param months the amount 378 * @param days the amount 379 */ 380 private static Period create(int years, int months, int days) { 381 if ((years | months | days) == 0) { 382 return ZERO; 383 } 384 return new Period(years, months, days); 385 } 386 387 /** 388 * Constructor. 389 * 390 * @param years the amount 391 * @param months the amount 392 * @param days the amount 393 */ 394 this(int years, int months, int days) { 395 this.years = years; 396 this.months = months; 397 this.days = days; 398 } 399 400 //----------------------------------------------------------------------- 401 /** 402 * Gets the value of the requested unit. 403 * !(p) 404 * This returns a value for each of the three supported units, 405 * {@link ChronoUnit#YEARS YEARS}, {@link ChronoUnit#MONTHS MONTHS} and 406 * {@link ChronoUnit#DAYS DAYS}. 407 * All other units throw an exception. 408 * 409 * @param unit the {@code TemporalUnit} for which to return the value 410 * @return the long value of the unit 411 * @throws DateTimeException if the unit is not supported 412 * @throws UnsupportedTemporalTypeException if the unit is not supported 413 */ 414 override 415 public long get(TemporalUnit unit) { 416 if (unit == ChronoUnit.YEARS) { 417 return getYears(); 418 } else if (unit == ChronoUnit.MONTHS) { 419 return getMonths(); 420 } else if (unit == ChronoUnit.DAYS) { 421 return getDays(); 422 } else { 423 throw new UnsupportedTemporalTypeException("Unsupported unit: " ~ typeid(unit).name); 424 } 425 } 426 427 /** 428 * Gets the set of units supported by this period. 429 * !(p) 430 * The supported units are {@link ChronoUnit#YEARS YEARS}, 431 * {@link ChronoUnit#MONTHS MONTHS} and {@link ChronoUnit#DAYS DAYS}. 432 * They are returned _in the order years, months, days. 433 * !(p) 434 * This set can be used _in conjunction with {@link #get(TemporalUnit)} 435 * to access the entire state of the period. 436 * 437 * @return a list containing the years, months and days units, not null 438 */ 439 override 440 public List!(TemporalUnit) getUnits() { 441 return SUPPORTED_UNITS; 442 } 443 444 /** 445 * Gets the chronology of this period, which is the ISO calendar system. 446 * !(p) 447 * The {@code Chronology} represents the calendar system _in use. 448 * The ISO-8601 calendar system is the modern civil calendar system used today 449 * _in most of the world. It is equivalent to the proleptic Gregorian calendar 450 * system, _in which today's rules for leap years are applied for all time. 451 * 452 * @return the ISO chronology, not null 453 */ 454 override 455 public IsoChronology getChronology() { 456 return IsoChronology.INSTANCE; 457 } 458 459 //----------------------------------------------------------------------- 460 /** 461 * Checks if all three units of this period are zero. 462 * !(p) 463 * A zero period has the value zero for the years, months and days units. 464 * 465 * @return true if this period is zero-length 466 */ 467 public bool isZero() { 468 return (this == ZERO); 469 } 470 471 /** 472 * Checks if any of the three units of this period are negative. 473 * !(p) 474 * This checks whether the years, months or days units are less than zero. 475 * 476 * @return true if any unit of this period is negative 477 */ 478 public bool isNegative() { 479 return years < 0 || months < 0 || days < 0; 480 } 481 482 //----------------------------------------------------------------------- 483 /** 484 * Gets the amount of years of this period. 485 * !(p) 486 * This returns the years unit. 487 * !(p) 488 * The months unit is not automatically normalized with the years unit. 489 * This means that a period of "15 months" is different to a period 490 * of "1 year and 3 months". 491 * 492 * @return the amount of years of this period, may be negative 493 */ 494 public int getYears() { 495 return years; 496 } 497 498 /** 499 * Gets the amount of months of this period. 500 * !(p) 501 * This returns the months unit. 502 * !(p) 503 * The months unit is not automatically normalized with the years unit. 504 * This means that a period of "15 months" is different to a period 505 * of "1 year and 3 months". 506 * 507 * @return the amount of months of this period, may be negative 508 */ 509 public int getMonths() { 510 return months; 511 } 512 513 /** 514 * Gets the amount of days of this period. 515 * !(p) 516 * This returns the days unit. 517 * 518 * @return the amount of days of this period, may be negative 519 */ 520 public int getDays() { 521 return days; 522 } 523 524 //----------------------------------------------------------------------- 525 /** 526 * Returns a copy of this period with the specified amount of years. 527 * !(p) 528 * This sets the amount of the years unit _in a copy of this period. 529 * The months and days units are unaffected. 530 * !(p) 531 * The months unit is not automatically normalized with the years unit. 532 * This means that a period of "15 months" is different to a period 533 * of "1 year and 3 months". 534 * !(p) 535 * This instance is immutable and unaffected by this method call. 536 * 537 * @param years the years to represent, may be negative 538 * @return a {@code Period} based on this period with the requested years, not null 539 */ 540 public Period withYears(int years) { 541 if (years == this.years) { 542 return this; 543 } 544 return create(years, months, days); 545 } 546 547 /** 548 * Returns a copy of this period with the specified amount of months. 549 * !(p) 550 * This sets the amount of the months unit _in a copy of this period. 551 * The years and days units are unaffected. 552 * !(p) 553 * The months unit is not automatically normalized with the years unit. 554 * This means that a period of "15 months" is different to a period 555 * of "1 year and 3 months". 556 * !(p) 557 * This instance is immutable and unaffected by this method call. 558 * 559 * @param months the months to represent, may be negative 560 * @return a {@code Period} based on this period with the requested months, not null 561 */ 562 public Period withMonths(int months) { 563 if (months == this.months) { 564 return this; 565 } 566 return create(years, months, days); 567 } 568 569 /** 570 * Returns a copy of this period with the specified amount of days. 571 * !(p) 572 * This sets the amount of the days unit _in a copy of this period. 573 * The years and months units are unaffected. 574 * !(p) 575 * This instance is immutable and unaffected by this method call. 576 * 577 * @param days the days to represent, may be negative 578 * @return a {@code Period} based on this period with the requested days, not null 579 */ 580 public Period withDays(int days) { 581 if (days == this.days) { 582 return this; 583 } 584 return create(years, months, days); 585 } 586 587 //----------------------------------------------------------------------- 588 /** 589 * Returns a copy of this period with the specified period added. 590 * !(p) 591 * This operates separately on the years, months and days. 592 * No normalization is performed. 593 * !(p) 594 * For example, "1 year, 6 months and 3 days" plus "2 years, 2 months and 2 days" 595 * returns "3 years, 8 months and 5 days". 596 * !(p) 597 * The specified amount is typically an instance of {@code Period}. 598 * Other types are interpreted using {@link Period#from(TemporalAmount)}. 599 * !(p) 600 * This instance is immutable and unaffected by this method call. 601 * 602 * @param amountToAdd the amount to add, not null 603 * @return a {@code Period} based on this period with the requested period added, not null 604 * @throws DateTimeException if the specified amount has a non-ISO chronology or 605 * contains an invalid unit 606 * @throws ArithmeticException if numeric overflow occurs 607 */ 608 public Period plus(TemporalAmount amountToAdd) { 609 Period isoAmount = Period.from(amountToAdd); 610 return create( 611 MathHelper.addExact(years, isoAmount.years), 612 MathHelper.addExact(months, isoAmount.months), 613 MathHelper.addExact(days, isoAmount.days)); 614 } 615 616 /** 617 * Returns a copy of this period with the specified years added. 618 * !(p) 619 * This adds the amount to the years unit _in a copy of this period. 620 * The months and days units are unaffected. 621 * For example, "1 year, 6 months and 3 days" plus 2 years returns "3 years, 6 months and 3 days". 622 * !(p) 623 * This instance is immutable and unaffected by this method call. 624 * 625 * @param yearsToAdd the years to add, positive or negative 626 * @return a {@code Period} based on this period with the specified years added, not null 627 * @throws ArithmeticException if numeric overflow occurs 628 */ 629 public Period plusYears(long yearsToAdd) { 630 if (yearsToAdd == 0) { 631 return this; 632 } 633 return create(MathHelper.toIntExact(MathHelper.addExact(years, yearsToAdd)), months, days); 634 } 635 636 /** 637 * Returns a copy of this period with the specified months added. 638 * !(p) 639 * This adds the amount to the months unit _in a copy of this period. 640 * The years and days units are unaffected. 641 * For example, "1 year, 6 months and 3 days" plus 2 months returns "1 year, 8 months and 3 days". 642 * !(p) 643 * This instance is immutable and unaffected by this method call. 644 * 645 * @param monthsToAdd the months to add, positive or negative 646 * @return a {@code Period} based on this period with the specified months added, not null 647 * @throws ArithmeticException if numeric overflow occurs 648 */ 649 public Period plusMonths(long monthsToAdd) { 650 if (monthsToAdd == 0) { 651 return this; 652 } 653 return create(years, MathHelper.toIntExact(MathHelper.addExact(months, monthsToAdd)), days); 654 } 655 656 /** 657 * Returns a copy of this period with the specified days added. 658 * !(p) 659 * This adds the amount to the days unit _in a copy of this period. 660 * The years and months units are unaffected. 661 * For example, "1 year, 6 months and 3 days" plus 2 days returns "1 year, 6 months and 5 days". 662 * !(p) 663 * This instance is immutable and unaffected by this method call. 664 * 665 * @param daysToAdd the days to add, positive or negative 666 * @return a {@code Period} based on this period with the specified days added, not null 667 * @throws ArithmeticException if numeric overflow occurs 668 */ 669 public Period plusDays(long daysToAdd) { 670 if (daysToAdd == 0) { 671 return this; 672 } 673 return create(years, months, MathHelper.toIntExact(MathHelper.addExact(days, daysToAdd))); 674 } 675 676 //----------------------------------------------------------------------- 677 /** 678 * Returns a copy of this period with the specified period subtracted. 679 * !(p) 680 * This operates separately on the years, months and days. 681 * No normalization is performed. 682 * !(p) 683 * For example, "1 year, 6 months and 3 days" minus "2 years, 2 months and 2 days" 684 * returns "-1 years, 4 months and 1 day". 685 * !(p) 686 * The specified amount is typically an instance of {@code Period}. 687 * Other types are interpreted using {@link Period#from(TemporalAmount)}. 688 * !(p) 689 * This instance is immutable and unaffected by this method call. 690 * 691 * @param amountToSubtract the amount to subtract, not null 692 * @return a {@code Period} based on this period with the requested period subtracted, not null 693 * @throws DateTimeException if the specified amount has a non-ISO chronology or 694 * contains an invalid unit 695 * @throws ArithmeticException if numeric overflow occurs 696 */ 697 public Period minus(TemporalAmount amountToSubtract) { 698 Period isoAmount = Period.from(amountToSubtract); 699 return create( 700 MathHelper.subtractExact(years, isoAmount.years), 701 MathHelper.subtractExact(months, isoAmount.months), 702 MathHelper.subtractExact(days, isoAmount.days)); 703 } 704 705 /** 706 * Returns a copy of this period with the specified years subtracted. 707 * !(p) 708 * This subtracts the amount from the years unit _in a copy of this period. 709 * The months and days units are unaffected. 710 * For example, "1 year, 6 months and 3 days" minus 2 years returns "-1 years, 6 months and 3 days". 711 * !(p) 712 * This instance is immutable and unaffected by this method call. 713 * 714 * @param yearsToSubtract the years to subtract, positive or negative 715 * @return a {@code Period} based on this period with the specified years subtracted, not null 716 * @throws ArithmeticException if numeric overflow occurs 717 */ 718 public Period minusYears(long yearsToSubtract) { 719 return (yearsToSubtract == Long.MIN_VALUE ? plusYears(Long.MAX_VALUE).plusYears(1) : plusYears(-yearsToSubtract)); 720 } 721 722 /** 723 * Returns a copy of this period with the specified months subtracted. 724 * !(p) 725 * This subtracts the amount from the months unit _in a copy of this period. 726 * The years and days units are unaffected. 727 * For example, "1 year, 6 months and 3 days" minus 2 months returns "1 year, 4 months and 3 days". 728 * !(p) 729 * This instance is immutable and unaffected by this method call. 730 * 731 * @param monthsToSubtract the years to subtract, positive or negative 732 * @return a {@code Period} based on this period with the specified months subtracted, not null 733 * @throws ArithmeticException if numeric overflow occurs 734 */ 735 public Period minusMonths(long monthsToSubtract) { 736 return (monthsToSubtract == Long.MIN_VALUE ? plusMonths(Long.MAX_VALUE).plusMonths(1) : plusMonths(-monthsToSubtract)); 737 } 738 739 /** 740 * Returns a copy of this period with the specified days subtracted. 741 * !(p) 742 * This subtracts the amount from the days unit _in a copy of this period. 743 * The years and months units are unaffected. 744 * For example, "1 year, 6 months and 3 days" minus 2 days returns "1 year, 6 months and 1 day". 745 * !(p) 746 * This instance is immutable and unaffected by this method call. 747 * 748 * @param daysToSubtract the months to subtract, positive or negative 749 * @return a {@code Period} based on this period with the specified days subtracted, not null 750 * @throws ArithmeticException if numeric overflow occurs 751 */ 752 public Period minusDays(long daysToSubtract) { 753 return (daysToSubtract == Long.MIN_VALUE ? plusDays(Long.MAX_VALUE).plusDays(1) : plusDays(-daysToSubtract)); 754 } 755 756 //----------------------------------------------------------------------- 757 /** 758 * Returns a new instance with each element _in this period multiplied 759 * by the specified scalar. 760 * !(p) 761 * This returns a period with each of the years, months and days units 762 * individually multiplied. 763 * For example, a period of "2 years, -3 months and 4 days" multiplied by 764 * 3 will return "6 years, -9 months and 12 days". 765 * No normalization is performed. 766 * 767 * @param scalar the scalar to multiply by, not null 768 * @return a {@code Period} based on this period with the amounts multiplied by the scalar, not null 769 * @throws ArithmeticException if numeric overflow occurs 770 */ 771 public Period multipliedBy(int scalar) { 772 if (this == ZERO || scalar == 1) { 773 return this; 774 } 775 return create( 776 MathHelper.multiplyExact(years, scalar), 777 MathHelper.multiplyExact(months, scalar), 778 MathHelper.multiplyExact(days, scalar)); 779 } 780 781 /** 782 * Returns a new instance with each amount _in this period negated. 783 * !(p) 784 * This returns a period with each of the years, months and days units 785 * individually negated. 786 * For example, a period of "2 years, -3 months and 4 days" will be 787 * negated to "-2 years, 3 months and -4 days". 788 * No normalization is performed. 789 * 790 * @return a {@code Period} based on this period with the amounts negated, not null 791 * @throws ArithmeticException if numeric overflow occurs, which only happens if 792 * one of the units has the value {@code Long.MIN_VALUE} 793 */ 794 public Period negated() { 795 return multipliedBy(-1); 796 } 797 798 //----------------------------------------------------------------------- 799 /** 800 * Returns a copy of this period with the years and months normalized. 801 * !(p) 802 * This normalizes the years and months units, leaving the days unit unchanged. 803 * The months unit is adjusted to have an absolute value less than 12, 804 * with the years unit being adjusted to compensate. For example, a period of 805 * "1 Year and 15 months" will be normalized to "2 years and 3 months". 806 * !(p) 807 * The sign of the years and months units will be the same after normalization. 808 * For example, a period of "1 year and -25 months" will be normalized to 809 * "-1 year and -1 month". 810 * !(p) 811 * This instance is immutable and unaffected by this method call. 812 * 813 * @return a {@code Period} based on this period with excess months normalized to years, not null 814 * @throws ArithmeticException if numeric overflow occurs 815 */ 816 public Period normalized() { 817 long totalMonths = toTotalMonths(); 818 long splitYears = totalMonths / 12; 819 int splitMonths = cast(int) (totalMonths % 12); // no overflow 820 if (splitYears == years && splitMonths == months) { 821 return this; 822 } 823 return create(MathHelper.toIntExact(splitYears), splitMonths, days); 824 } 825 826 /** 827 * Gets the total number of months _in this period. 828 * !(p) 829 * This returns the total number of months _in the period by multiplying the 830 * number of years by 12 and adding the number of months. 831 * !(p) 832 * This instance is immutable and unaffected by this method call. 833 * 834 * @return the total number of months _in the period, may be negative 835 */ 836 public long toTotalMonths() { 837 return years * 12L + months; // no overflow 838 } 839 840 //------------------------------------------------------------------------- 841 /** 842 * Adds this period to the specified temporal object. 843 * !(p) 844 * This returns a temporal object of the same observable type as the input 845 * with this period added. 846 * If the temporal has a chronology, it must be the ISO chronology. 847 * !(p) 848 * In most cases, it is clearer to reverse the calling pattern by using 849 * {@link Temporal#plus(TemporalAmount)}. 850 * !(pre) 851 * // these two lines are equivalent, but the second approach is recommended 852 * dateTime = thisPeriod.addTo(dateTime); 853 * dateTime = dateTime.plus(thisPeriod); 854 * </pre> 855 * !(p) 856 * The calculation operates as follows. 857 * First, the chronology of the temporal is checked to ensure it is ISO chronology or null. 858 * Second, if the months are zero, the years are added if non-zero, otherwise 859 * the combination of years and months is added if non-zero. 860 * Finally, any days are added. 861 * !(p) 862 * This approach ensures that a partial period can be added to a partial date. 863 * For example, a period of years and/or months can be added to a {@code YearMonth}, 864 * but a period including days cannot. 865 * The approach also adds years and months together when necessary, which ensures 866 * correct behaviour at the end of the month. 867 * !(p) 868 * This instance is immutable and unaffected by this method call. 869 * 870 * @param temporal the temporal object to adjust, not null 871 * @return an object of the same type with the adjustment made, not null 872 * @throws DateTimeException if unable to add 873 * @throws ArithmeticException if numeric overflow occurs 874 */ 875 override 876 public Temporal addTo(Temporal temporal) { 877 validateChrono(temporal); 878 if (months == 0) { 879 if (years != 0) { 880 temporal = temporal.plus(years, ChronoUnit.YEARS); 881 } 882 } else { 883 long totalMonths = toTotalMonths(); 884 if (totalMonths != 0) { 885 temporal = temporal.plus(totalMonths, ChronoUnit.MONTHS); 886 } 887 } 888 if (days != 0) { 889 temporal = temporal.plus(days, ChronoUnit.DAYS); 890 } 891 return temporal; 892 } 893 894 /** 895 * Subtracts this period from the specified temporal object. 896 * !(p) 897 * This returns a temporal object of the same observable type as the input 898 * with this period subtracted. 899 * If the temporal has a chronology, it must be the ISO chronology. 900 * !(p) 901 * In most cases, it is clearer to reverse the calling pattern by using 902 * {@link Temporal#minus(TemporalAmount)}. 903 * !(pre) 904 * // these two lines are equivalent, but the second approach is recommended 905 * dateTime = thisPeriod.subtractFrom(dateTime); 906 * dateTime = dateTime.minus(thisPeriod); 907 * </pre> 908 * !(p) 909 * The calculation operates as follows. 910 * First, the chronology of the temporal is checked to ensure it is ISO chronology or null. 911 * Second, if the months are zero, the years are subtracted if non-zero, otherwise 912 * the combination of years and months is subtracted if non-zero. 913 * Finally, any days are subtracted. 914 * !(p) 915 * This approach ensures that a partial period can be subtracted from a partial date. 916 * For example, a period of years and/or months can be subtracted from a {@code YearMonth}, 917 * but a period including days cannot. 918 * The approach also subtracts years and months together when necessary, which ensures 919 * correct behaviour at the end of the month. 920 * !(p) 921 * This instance is immutable and unaffected by this method call. 922 * 923 * @param temporal the temporal object to adjust, not null 924 * @return an object of the same type with the adjustment made, not null 925 * @throws DateTimeException if unable to subtract 926 * @throws ArithmeticException if numeric overflow occurs 927 */ 928 override 929 public Temporal subtractFrom(Temporal temporal) { 930 validateChrono(temporal); 931 if (months == 0) { 932 if (years != 0) { 933 temporal = temporal.minus(years, ChronoUnit.YEARS); 934 } 935 } else { 936 long totalMonths = toTotalMonths(); 937 if (totalMonths != 0) { 938 temporal = temporal.minus(totalMonths, ChronoUnit.MONTHS); 939 } 940 } 941 if (days != 0) { 942 temporal = temporal.minus(days, ChronoUnit.DAYS); 943 } 944 return temporal; 945 } 946 947 /** 948 * Validates that the temporal has the correct chronology. 949 */ 950 private void validateChrono(TemporalAccessor temporal) { 951 assert(temporal, "temporal"); 952 Chronology temporalChrono = QueryHelper.query!Chronology(temporal ,TemporalQueries.chronology()); 953 if (temporalChrono !is null && (IsoChronology.INSTANCE == temporalChrono) == false) { 954 throw new DateTimeException("Chronology mismatch, expected: ISO, actual: " ~ temporalChrono.getId()); 955 } 956 } 957 958 //----------------------------------------------------------------------- 959 /** 960 * Checks if this period is equal to another period. 961 * !(p) 962 * The comparison is based on the type {@code Period} and each of the three amounts. 963 * To be equal, the years, months and days units must be individually equal. 964 * Note that this means that a period of "15 Months" is not equal to a period 965 * of "1 Year and 3 Months". 966 * 967 * @param obj the object to check, null returns false 968 * @return true if this is equal to the other period 969 */ 970 override 971 public bool opEquals(Object obj) { 972 if (this is obj) { 973 return true; 974 } 975 if (cast(Period)(obj) !is null) { 976 Period other = cast(Period) obj; 977 return years == other.years && 978 months == other.months && 979 days == other.days; 980 } 981 return false; 982 } 983 984 /** 985 * A hash code for this period. 986 * 987 * @return a suitable hash code 988 */ 989 override 990 public size_t toHash() @trusted nothrow { 991 try 992 { 993 return years + Integer.rotateLeft(months, 8) + Integer.rotateLeft(days, 16); 994 } 995 catch(Exception e){} 996 return int.init; 997 } 998 999 //----------------------------------------------------------------------- 1000 /** 1001 * Outputs this period as a {@code string}, such as {@code P6Y3M1D}. 1002 * !(p) 1003 * The output will be _in the ISO-8601 period format. 1004 * A zero period will be represented as zero days, 'P0D'. 1005 * 1006 * @return a string representation of this period, not null 1007 */ 1008 override 1009 public string toString() { 1010 if (this == ZERO) { 1011 return "P0D"; 1012 } else { 1013 StringBuilder buf = new StringBuilder(); 1014 buf.append('P'); 1015 if (years != 0) { 1016 buf.append(years).append('Y'); 1017 } 1018 if (months != 0) { 1019 buf.append(months).append('M'); 1020 } 1021 if (days != 0) { 1022 buf.append(days).append('D'); 1023 } 1024 return buf.toString(); 1025 } 1026 } 1027 1028 //----------------------------------------------------------------------- 1029 /** 1030 * Writes the object using a 1031 * <a href="{@docRoot}/serialized-form.html#hunt.time.Ser">dedicated serialized form</a>. 1032 * @serialData 1033 * !(pre) 1034 * _out.writeByte(14); // identifies a Period 1035 * _out.writeInt(years); 1036 * _out.writeInt(months); 1037 * _out.writeInt(days); 1038 * </pre> 1039 * 1040 * @return the instance of {@code Ser}, not null 1041 */ 1042 private Object writeReplace() { 1043 return new Ser(Ser.PERIOD_TYPE, this); 1044 } 1045 1046 /** 1047 * Defend against malicious streams. 1048 * 1049 * @param s the stream to read 1050 * @throws java.io.InvalidObjectException always 1051 */ 1052 ///@gxc 1053 // private void readObject(ObjectInputStream s) /*throws InvalidObjectException*/ { 1054 // throw new InvalidObjectException("Deserialization via serialization delegate"); 1055 // } 1056 1057 void writeExternal(DataOutput _out) /*throws IOException*/ { 1058 _out.writeInt(years); 1059 _out.writeInt(months); 1060 _out.writeInt(days); 1061 } 1062 1063 static Period readExternal(DataInput _in) /*throws IOException*/ { 1064 int years = _in.readInt(); 1065 int months = _in.readInt(); 1066 int days = _in.readInt(); 1067 return Period.of(years, months, days); 1068 } 1069 1070 }