• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ExpediaGroup / beekeeper / #814

07 Apr 2026 07:33PM UTC coverage: 87.706% (-0.07%) from 87.774%
#814

Pull #201

ninhomilton
Clean up
Pull Request #201: Upgrade to Java 21 and Spring Boot 3.2.12

10 of 12 new or added lines in 3 files covered. (83.33%)

115 existing lines in 33 files now uncovered.

1541 of 1757 relevant lines covered (87.71%)

0.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

73.39
/beekeeper-core/src/main/java/com/expediagroup/beekeeper/core/model/PeriodDuration.java
1
/*
2
 * Copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  * Redistributions of source code must retain the above copyright notice,
10
 *    this list of conditions and the following disclaimer.
11
 *
12
 *  * Redistributions in binary form must reproduce the above copyright notice,
13
 *    this list of conditions and the following disclaimer in the documentation
14
 *    and/or other materials provided with the distribution.
15
 *
16
 *  * Neither the name of JSR-310 nor the names of its contributors
17
 *    may be used to endorse or promote products derived from this software
18
 *    without specific prior written permission.
19
 *
20
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
24
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
25
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
26
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
27
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
28
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
29
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
30
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
31
 */
32
package com.expediagroup.beekeeper.core.model;
33

34
import static java.time.temporal.ChronoUnit.DAYS;
35
import static java.time.temporal.ChronoUnit.MONTHS;
36
import static java.time.temporal.ChronoUnit.NANOS;
37
import static java.time.temporal.ChronoUnit.SECONDS;
38
import static java.time.temporal.ChronoUnit.YEARS;
39

40
import java.io.Serializable;
41
import java.time.DateTimeException;
42
import java.time.Duration;
43
import java.time.LocalDate;
44
import java.time.LocalTime;
45
import java.time.Period;
46
import java.time.chrono.ChronoPeriod;
47
import java.time.chrono.IsoChronology;
48
import java.time.format.DateTimeParseException;
49
import java.time.temporal.ChronoUnit;
50
import java.time.temporal.IsoFields;
51
import java.time.temporal.Temporal;
52
import java.time.temporal.TemporalAmount;
53
import java.time.temporal.TemporalQueries;
54
import java.time.temporal.TemporalUnit;
55
import java.time.temporal.UnsupportedTemporalTypeException;
56
import java.util.Arrays;
57
import java.util.Collections;
58
import java.util.List;
59
import java.util.Locale;
60
import java.util.Objects;
61

62
import org.joda.convert.FromString;
63
import org.joda.convert.ToString;
64

65
/**
66
 * An amount of time in the ISO-8601 calendar system that combines a period and a duration.
67
 * <p>
68
 * This class models a quantity or amount of time in terms of a {@code Period} and {@code Duration}. A period is a
69
 * date-based amount of time, consisting of years, months and days. A duration is a time-based amount of time,
70
 * consisting of seconds and nanoseconds. See the {@link Period} and {@link Duration} classes for more details.
71
 * <p>
72
 * The days in a period take account of daylight saving changes (23 or 25 hour days). When performing calculations, the
73
 * period is added first, then the duration.
74
 * <p>
75
 * The model is of a directed amount, meaning that the amount may be negative.
76
 * <h3>Implementation Requirements:</h3> This class is immutable and thread-safe.
77
 * <p>
78
 * This class must be treated as a value type. Do not synchronize, rely on the identity hash code or use the distinction
79
 * between equals() and ==.
80
 */
81
public final class PeriodDuration implements TemporalAmount, Serializable {
82

83
  /**
84
   * A constant for a duration of zero.
85
   */
86
  public static final PeriodDuration ZERO = new PeriodDuration(Period.ZERO, Duration.ZERO);
87

88
  /**
1✔
89
   * A serialization identifier for this class.
90
   */
91
  private static final long serialVersionUID = 8815521625671589L;
92
  /**
93
   * The supported units.
94
   */
1✔
95
  private static final List<TemporalUnit> SUPPORTED_UNITS = Collections
1✔
96
      .unmodifiableList(Arrays.<TemporalUnit>asList(YEARS, MONTHS, DAYS, SECONDS, NANOS));
1✔
97
  /**
98
   * The number of seconds per day.
99
   */
100
  private static final long SECONDS_PER_DAY = 86400;
101

102
  /**
103
   * The period.
104
   */
105
  private final Period period;
106
  /**
107
   * The duration.
108
   */
109
  private final Duration duration;
110

111
  // -----------------------------------------------------------------------
112
  /**
113
   * Obtains an instance based on a period and duration.
114
   * <p>
115
   * The total amount of time of the resulting instance is the period plus the duration.
116
   *
117
   * @param period the period, not null
118
   * @param duration the duration, not null
1✔
119
   * @return the combined period-duration, not null
1✔
120
   */
1✔
121
  public static PeriodDuration of(Period period, Duration duration) {
122
    Objects.requireNonNull(period, "The period must not be null");
123
    Objects.requireNonNull(duration, "The duration must not be null");
124
    return new PeriodDuration(period, duration);
125
  }
126

127
  /**
128
   * Obtains an instance based on a period.
129
   * <p>
130
   * The duration will be zero.
131
   *
132
   * @param period the period, not null
1✔
133
   * @return the combined period-duration, not null
1✔
134
   */
135
  public static PeriodDuration of(Period period) {
136
    Objects.requireNonNull(period, "The period must not be null");
137
    return new PeriodDuration(period, Duration.ZERO);
138
  }
139

140
  /**
141
   * Obtains an instance based on a duration.
142
   * <p>
143
   * The period will be zero.
144
   *
145
   * @param duration the duration, not null
1✔
146
   * @return the combined period-duration, not null
1✔
147
   */
148
  public static PeriodDuration of(Duration duration) {
149
    Objects.requireNonNull(duration, "The duration must not be null");
150
    return new PeriodDuration(Period.ZERO, duration);
151
  }
152

153
  // -----------------------------------------------------------------------
154
  /**
155
   * Obtains an instance from a temporal amount.
156
   * <p>
157
   * This obtains an instance based on the specified amount. A {@code TemporalAmount} represents an amount of time which
158
   * this factory extracts to a {@code PeriodDuration}.
159
   * <p>
160
   * The result is calculated by looping around each unit in the specified amount. Any amount that is zero is ignore. If
161
   * a unit has an exact duration, it will be totalled using {@link Duration#plus(Duration)}. If the unit is days or
162
   * weeks, it will be totalled into the days part of the period. If the unit is months or quarters, it will be totalled
163
   * into the months part of the period. If the unit is years, decades, centuries or millennia, it will be totalled into
164
   * the years part of the period.
165
   *
166
   * @param amount the temporal amount to convert, not null
167
   * @return the equivalent duration, not null
168
   * @throws DateTimeException if unable to convert to a {@code Duration}
169
   * @throws ArithmeticException if numeric overflow occurs
1✔
170
   */
1✔
171
  public static PeriodDuration from(TemporalAmount amount) {
172
    if (amount instanceof PeriodDuration) {
1✔
173
      return (PeriodDuration) amount;
1✔
174
    }
175
    if (amount instanceof Period) {
1✔
176
      return PeriodDuration.of((Period) amount);
1✔
177
    }
UNCOV
178
    if (amount instanceof Duration) {
×
UNCOV
179
      return PeriodDuration.of((Duration) amount);
×
UNCOV
180
    }
×
181
    if (amount instanceof ChronoPeriod) {
182
      if (IsoChronology.INSTANCE.equals(((ChronoPeriod) amount).getChronology()) == false) {
183
        throw new DateTimeException("Period requires ISO chronology: " + amount);
×
UNCOV
184
      }
×
UNCOV
185
    }
×
186
    Objects.requireNonNull(amount, "amount");
×
187
    int years = 0;
×
188
    int months = 0;
×
189
    int days = 0;
×
190
    Duration duration = Duration.ZERO;
×
191
    for (TemporalUnit unit : amount.getUnits()) {
192
      long value = amount.get(unit);
×
193
      if (value != 0) {
×
UNCOV
194
        // ignore unless non-zero
×
195
        if (unit.isDurationEstimated()) {
×
196
          if (unit == ChronoUnit.DAYS) {
×
197
            days = Math.addExact(days, Math.toIntExact(value));
×
198
          } else if (unit == ChronoUnit.WEEKS) {
×
199
            days = Math.addExact(days, Math.toIntExact(Math.multiplyExact(value, 7)));
×
200
          } else if (unit == ChronoUnit.MONTHS) {
×
201
            months = Math.addExact(months, Math.toIntExact(value));
×
202
          } else if (unit == IsoFields.QUARTER_YEARS) {
×
203
            months = Math.addExact(months, Math.toIntExact(Math.multiplyExact(value, 3)));
×
204
          } else if (unit == ChronoUnit.YEARS) {
×
205
            years = Math.addExact(years, Math.toIntExact(value));
×
206
          } else if (unit == ChronoUnit.DECADES) {
×
207
            years = Math.addExact(years, Math.toIntExact(Math.multiplyExact(value, 10)));
×
208
          } else if (unit == ChronoUnit.CENTURIES) {
×
209
            years = Math.addExact(years, Math.toIntExact(Math.multiplyExact(value, 100)));
210
          } else if (unit == ChronoUnit.MILLENNIA) {
×
211
            years = Math.addExact(years, Math.toIntExact(Math.multiplyExact(value, 1000)));
212
          } else {
213
            throw new DateTimeException("Unknown unit: " + unit);
UNCOV
214
          }
×
215
        } else {
216
          // total of exact durations
217
          duration = duration.plus(amount.get(unit), unit);
×
UNCOV
218
        }
×
219
      }
220
    }
221
    return PeriodDuration.of(Period.of(years, months, days), duration);
222
  }
223

224
  // -----------------------------------------------------------------------
225
  /**
226
   * Obtains an instance from a text string such as {@code PnYnMnDTnHnMnS}.
227
   * <p>
228
   * This will parse the string produced by {@code toString()} which is based on the ISO-8601 period formats
229
   * {@code PnYnMnDTnHnMnS} and {@code PnW}.
230
   * <p>
231
   * The string starts with an optional sign, denoted by the ASCII negative or positive symbol. If negative, the whole
232
   * amount is negated. The ASCII letter "P" is next in upper or lower case. There are then a number of sections, each
233
   * consisting of a number and a suffix. At least one of the sections must be present. The sections have suffixes in
234
   * ASCII of "Y" for years, "M" for months, "W" for weeks, "D" for days, "H" for hours, "M" for minutes, "S" for
235
   * seconds, accepted in upper or lower case. Note that the ASCII letter "T" separates the date and time parts and must
236
   * be present if any time part is present. The suffixes must occur in order. The number part of each section must
237
   * consist of ASCII digits. The number may be prefixed by the ASCII negative or positive symbol. The number must parse
238
   * to an {@code int}. Any week-based input is multiplied by 7 and treated as a number of days.
239
   * <p>
240
   * The leading plus/minus sign, and negative values for weeks and days are not part of the ISO-8601 standard.
241
   * <p>
242
   * Note that the date style format {@code PYYYY-MM-DDTHH:MM:SS} is not supported.
243
   * <p>
244
   * For example, the following are valid inputs:
245
   * 
246
   * <pre>
247
   *   "P2Y"             -- PeriodDuration.of(Period.ofYears(2))
248
   *   "P3M"             -- PeriodDuration.of(Period.ofMonths(3))
249
   *   "P4W"             -- PeriodDuration.of(Period.ofWeeks(4))
250
   *   "P5D"             -- PeriodDuration.of(Period.ofDays(5))
251
   *   "PT6H"            -- PeriodDuration.of(Duration.ofHours(6))
252
   *   "P1Y2M3D"         -- PeriodDuration.of(Period.of(1, 2, 3))
253
   *   "P1Y2M3W4DT8H"    -- PeriodDuration.of(Period.of(1, 2, 25), Duration.ofHours(8))
254
   *   "P-1Y2M"          -- PeriodDuration.of(Period.of(-1, 2, 0))
255
   *   "-P1Y2M"          -- PeriodDuration.of(Period.of(-1, -2, 0))
256
   * </pre>
257
   *
258
   * @param text the text to parse, not null
259
   * @return the parsed period, not null
260
   * @throws DateTimeParseException if the text cannot be parsed to a period
261
   */
262
  @FromString
263
  public static PeriodDuration parse(CharSequence text) {
264
    Objects.requireNonNull(text, "text");
1✔
265
    String upper = text.toString().toUpperCase(Locale.ENGLISH);
1✔
266
    String negate = "";
1✔
267
    if (upper.startsWith("+")) {
1✔
268
      upper = upper.substring(1);
1✔
269
    } else if (upper.startsWith("-")) {
1✔
270
      upper = upper.substring(1);
1✔
271
      negate = "-";
1✔
272
    }
273
    // duration only, parse original text so it does negation
274
    if (upper.startsWith("PT")) {
1✔
275
      return PeriodDuration.of(Duration.parse(text));
1✔
276
    }
277
    // period only, parse original text so it does negation
278
    int tpos = upper.indexOf('T');
1✔
279
    if (tpos < 0) {
1✔
280
      return PeriodDuration.of(Period.parse(text));
1✔
281
    }
282
    // period and duration
283
    Period period = Period.parse(negate + upper.substring(0, tpos));
1✔
284
    Duration duration = Duration.parse(negate + "P" + upper.substring(tpos));
1✔
285
    return PeriodDuration.of(period, duration);
1✔
286
  }
287

288
  // -----------------------------------------------------------------------
289
  /**
290
   * Obtains an instance consisting of the amount of time between two temporals.
291
   * <p>
292
   * The start is included, but the end is not. The result of this method can be negative if the end is before the
293
   * start.
294
   * <p>
295
   * The calculation examines the temporals and extracts {@link LocalDate} and {@link LocalTime}. If the time is
296
   * missing, it will be defaulted to midnight. If one date is missing, it will be defaulted to the other date. It then
297
   * finds the amount of time between the two dates and between the two times.
298
   *
299
   * @param startInclusive the start, inclusive, not null
300
   * @param endExclusive the end, exclusive, not null
301
   * @return the number of days between this date and the end date, not null
302
   */
303
  public static PeriodDuration between(Temporal startInclusive, Temporal endExclusive) {
304
    LocalDate startDate = startInclusive.query(TemporalQueries.localDate());
305
    LocalDate endDate = endExclusive.query(TemporalQueries.localDate());
1✔
306
    Period period = Period.ZERO;
1✔
307
    if (startDate != null && endDate != null) {
1✔
308
      period = Period.between(startDate, endDate);
1✔
309
    }
1✔
310
    LocalTime startTime = startInclusive.query(TemporalQueries.localTime());
311
    LocalTime endTime = endExclusive.query(TemporalQueries.localTime());
1✔
312
    startTime = startTime != null ? startTime : LocalTime.MIDNIGHT;
1✔
313
    endTime = endTime != null ? endTime : LocalTime.MIDNIGHT;
1✔
314
    Duration duration = Duration.between(startTime, endTime);
1✔
315
    return PeriodDuration.of(period, duration);
1✔
316
  }
1✔
317

318
  // -----------------------------------------------------------------------
319
  /**
320
   * Constructs an instance.
321
   *
322
   * @param period the period
323
   * @param duration the duration
324
   */
325
  private PeriodDuration(Period period, Duration duration) {
326
    this.period = period;
1✔
327
    this.duration = duration;
1✔
328
  }
1✔
329

1✔
330
  /**
331
   * Resolves singletons.
332
   *
333
   * @return the singleton instance
334
   */
335
  private Object readResolve() {
336
    return PeriodDuration.of(period, duration);
337
  }
1✔
338

339
  // -----------------------------------------------------------------------
340
  /**
341
   * Gets the value of the requested unit.
342
   * <p>
343
   * This returns a value for the supported units - {@link ChronoUnit#YEARS}, {@link ChronoUnit#MONTHS},
344
   * {@link ChronoUnit#DAYS}, {@link ChronoUnit#SECONDS} and {@link ChronoUnit#NANOS}. All other units throw an
345
   * exception. Note that hours and minutes throw an exception.
346
   *
347
   * @param unit the {@code TemporalUnit} for which to return the value
348
   * @return the long value of the unit
349
   * @throws UnsupportedTemporalTypeException if the unit is not supported
350
   */
351
  @Override
352
  public long get(TemporalUnit unit) {
353
    if (unit instanceof ChronoUnit) {
354
      switch ((ChronoUnit) unit) {
355
      case YEARS:
1✔
356
        return period.getYears();
1✔
357
      case MONTHS:
358
        return period.getMonths();
1✔
359
      case DAYS:
360
        return period.getDays();
1✔
361
      case SECONDS:
362
        return duration.getSeconds();
1✔
363
      case NANOS:
364
        return duration.getNano();
1✔
365
      default:
366
        break;
1✔
367
      }
368
    }
369
    throw new UnsupportedTemporalTypeException("Unsupported unit: " + unit);
370
  }
371

1✔
372
  /**
373
   * Gets the set of units supported by this amount.
374
   * <p>
375
   * This returns the list {@link ChronoUnit#YEARS}, {@link ChronoUnit#MONTHS}, {@link ChronoUnit#DAYS},
376
   * {@link ChronoUnit#SECONDS} and {@link ChronoUnit#NANOS}.
377
   * <p>
378
   * This set can be used in conjunction with {@link #get(TemporalUnit)} to access the entire state of the amount.
379
   *
380
   * @return a list containing the days unit, not null
381
   */
382
  @Override
383
  public List<TemporalUnit> getUnits() {
384
    return SUPPORTED_UNITS;
385
  }
386

387
  // -----------------------------------------------------------------------
1✔
388
  /**
389
   * Gets the period part.
390
   *
391
   * @return the period part
392
   */
393
  public Period getPeriod() {
394
    return period;
395
  }
396

397
  /**
1✔
398
   * Returns a copy of this period-duration with a different period.
399
   * <p>
400
   * This instance is immutable and unaffected by this method call.
401
   *
402
   * @param period the new period
403
   * @return the updated period-duration
404
   */
405
  public PeriodDuration withPeriod(Period period) {
406
    return PeriodDuration.of(period, duration);
407
  }
408

409
  /**
1✔
410
   * Gets the duration part.
411
   *
412
   * @return the duration part
413
   */
414
  public Duration getDuration() {
415
    return duration;
416
  }
417

418
  /**
1✔
419
   * Returns a copy of this period-duration with a different duration.
420
   * <p>
421
   * This instance is immutable and unaffected by this method call.
422
   *
423
   * @param duration the new duration
424
   * @return the updated period-duration
425
   */
426
  public PeriodDuration withDuration(Duration duration) {
427
    return PeriodDuration.of(period, duration);
428
  }
429

UNCOV
430
  // -----------------------------------------------------------------------
×
431
  /**
432
   * Checks if all parts of this amount are zero.
433
   * <p>
434
   * This returns true if both {@link Period#isZero()} and {@link Duration#isZero()} return true.
435
   *
436
   * @return true if this period is zero-length
437
   */
438
  public boolean isZero() {
439
    return period.isZero() && duration.isZero();
440
  }
441

442
  // -----------------------------------------------------------------------
1✔
443
  /**
444
   * Returns a copy of this amount with the specified amount added.
445
   * <p>
446
   * The parameter is converted using {@link PeriodDuration#from(TemporalAmount)}. The period and duration are combined
447
   * separately.
448
   * <p>
449
   * This instance is immutable and unaffected by this method call.
450
   *
451
   * @param amountToAdd the amount to add, not null
452
   * @return a {@code Days} based on this instance with the requested amount added, not null
453
   * @throws DateTimeException if the specified amount contains an invalid unit
454
   * @throws ArithmeticException if numeric overflow occurs
455
   */
456
  public PeriodDuration plus(TemporalAmount amountToAdd) {
457
    PeriodDuration other = PeriodDuration.from(amountToAdd);
458
    return of(period.plus(other.period), duration.plus(other.duration));
459
  }
460

1✔
461
  // -----------------------------------------------------------------------
1✔
462
  /**
463
   * Returns a copy of this amount with the specified amount subtracted.
464
   * <p>
465
   * The parameter is converted using {@link PeriodDuration#from(TemporalAmount)}. The period and duration are combined
466
   * separately.
467
   * <p>
468
   * This instance is immutable and unaffected by this method call.
469
   *
470
   * @param amountToAdd the amount to add, not null
471
   * @return a {@code Days} based on this instance with the requested amount subtracted, not null
472
   * @throws DateTimeException if the specified amount contains an invalid unit
473
   * @throws ArithmeticException if numeric overflow occurs
474
   */
475
  public PeriodDuration minus(TemporalAmount amountToAdd) {
476
    PeriodDuration other = PeriodDuration.from(amountToAdd);
477
    return of(period.minus(other.period), duration.minus(other.duration));
478
  }
479

1✔
480
  // -----------------------------------------------------------------------
1✔
481
  /**
482
   * Returns an instance with the amount multiplied by the specified scalar.
483
   * <p>
484
   * This instance is immutable and unaffected by this method call.
485
   *
486
   * @param scalar the scalar to multiply by, not null
487
   * @return the amount multiplied by the specified scalar, not null
488
   * @throws ArithmeticException if numeric overflow occurs
489
   */
490
  public PeriodDuration multipliedBy(int scalar) {
491
    if (scalar == 1) {
492
      return this;
493
    }
494
    return of(period.multipliedBy(scalar), duration.multipliedBy(scalar));
1✔
495
  }
1✔
496

497
  /**
1✔
498
   * Returns an instance with the amount negated.
499
   * <p>
500
   * This instance is immutable and unaffected by this method call.
501
   *
502
   * @return the negated amount, not null
503
   * @throws ArithmeticException if numeric overflow occurs, which only happens if the amount is {@code Long.MIN_VALUE}
504
   */
505
  public PeriodDuration negated() {
506
    return multipliedBy(-1);
507
  }
508

509
  // -----------------------------------------------------------------------
510
  /**
1✔
511
   * Returns a copy of this instance with the years and months exactly normalized.
512
   * <p>
513
   * This normalizes the years and months units, leaving the days unit unchanged. The result is exact, always
514
   * representing the same amount of time.
515
   * <p>
516
   * The months unit is adjusted to have an absolute value less than 11, with the years unit being adjusted to
517
   * compensate. For example, a period of "1 year and 15 months" will be normalized to "2 years and 3 months".
518
   * <p>
519
   * The sign of the years and months units will be the same after normalization. For example, a period of "1 year and
520
   * -25 months" will be normalized to "-1 year and -1 month".
521
   * <p>
522
   * Note that no normalization is performed on the days or duration.
523
   * <p>
524
   * This instance is immutable and unaffected by this method call.
525
   *
526
   * @return a {@code PeriodDuration} based on this one with excess months normalized to years, not null
527
   * @throws ArithmeticException if numeric overflow occurs
528
   */
529
  public PeriodDuration normalizedYears() {
530
    return withPeriod(period.normalized());
531
  }
532

533
  /**
534
   * Returns a copy of this instance with the days and duration normalized using the standard day of 24 hours.
535
   * <p>
536
   * This normalizes the days and duration, leaving the years and months unchanged. The result uses a standard day
1✔
537
   * length of 24 hours.
538
   * <p>
539
   * This combines the duration seconds with the number of days and shares the total seconds between the two fields. For
540
   * example, a period of "2 days and 86401 seconds" will be normalized to "3 days and 1 second".
541
   * <p>
542
   * The sign of the days and duration will be the same after normalization. For example, a period of "1 day and -172801
543
   * seconds" will be normalized to "-1 day and -1 second".
544
   * <p>
545
   * Note that no normalization is performed on the years or months.
546
   * <p>
547
   * This instance is immutable and unaffected by this method call.
548
   *
549
   * @return a {@code PeriodDuration} based on this one with excess duration normalized to days, not null
550
   * @throws ArithmeticException if numeric overflow occurs
551
   */
552
  public PeriodDuration normalizedStandardDays() {
553
    long totalSecs = period.getDays() * SECONDS_PER_DAY + duration.getSeconds();
554
    int splitDays = Math.toIntExact(totalSecs / SECONDS_PER_DAY);
555
    long splitSecs = totalSecs % SECONDS_PER_DAY;
556
    if (splitDays == period.getDays() && splitSecs == duration.getSeconds()) {
557
      return this;
558
    }
559
    return PeriodDuration.of(period.withDays(splitDays), duration.withSeconds(splitSecs));
560
  }
561

562
  // -----------------------------------------------------------------------
1✔
563
  /**
1✔
564
   * Adds this amount to the specified temporal object.
1✔
565
   * <p>
1✔
566
   * This returns a temporal object of the same observable type as the input with this amount added. This simply adds
1✔
567
   * the period and duration to the temporal.
568
   * <p>
1✔
569
   * This instance is immutable and unaffected by this method call.
570
   *
571
   * @param temporal the temporal object to adjust, not null
572
   * @return an object of the same type with the adjustment made, not null
573
   * @throws DateTimeException if unable to add
574
   * @throws UnsupportedTemporalTypeException if the DAYS unit is not supported
575
   * @throws ArithmeticException if numeric overflow occurs
576
   */
577
  @Override
578
  public Temporal addTo(Temporal temporal) {
579
    return temporal.plus(period).plus(duration);
580
  }
581

582
  /**
583
   * Subtracts this amount from the specified temporal object.
584
   * <p>
585
   * This returns a temporal object of the same observable type as the input with this amount subtracted. This simply
586
   * subtracts the period and duration from the temporal.
587
   * <p>
588
   * This instance is immutable and unaffected by this method call.
1✔
589
   *
590
   * @param temporal the temporal object to adjust, not null
591
   * @return an object of the same type with the adjustment made, not null
592
   * @throws DateTimeException if unable to subtract
593
   * @throws UnsupportedTemporalTypeException if the DAYS unit is not supported
594
   * @throws ArithmeticException if numeric overflow occurs
595
   */
596
  @Override
597
  public Temporal subtractFrom(Temporal temporal) {
598
    return temporal.minus(period).minus(duration);
599
  }
600

601
  // -----------------------------------------------------------------------
602
  /**
603
   * Checks if this amount is equal to the specified {@code PeriodDuration}.
604
   * <p>
605
   * The comparison is based on the underlying period and duration.
606
   *
607
   * @param otherAmount the other amount, null returns false
1✔
608
   * @return true if the other amount is equal to this one
609
   */
610
  @Override
611
  public boolean equals(Object otherAmount) {
612
    if (this == otherAmount) {
613
      return true;
614
    }
615
    if (otherAmount instanceof PeriodDuration) {
616
      PeriodDuration other = (PeriodDuration) otherAmount;
617
      return this.period.equals(other.period) && this.duration.equals(other.duration);
618
    }
619
    return false;
620
  }
621

1✔
622
  /**
1✔
623
   * A hash code for this amount.
624
   *
1✔
625
   * @return a suitable hash code
1✔
626
   */
1✔
627
  @Override
628
  public int hashCode() {
1✔
629
    return period.hashCode() ^ duration.hashCode();
630
  }
631

632
  // -----------------------------------------------------------------------
633
  /**
634
   * Returns a string representation of the amount. This will be in the format 'PnYnMnDTnHnMnS', with sections omitted
635
   * as necessary. An empty amount will return "PT0S".
636
   *
637
   * @return the period in ISO-8601 string format
638
   */
1✔
639
  @Override
640
  @ToString
641
  public String toString() {
642
    if (period.isZero()) {
643
      return duration.toString();
644
    }
645
    if (duration.isZero()) {
646
      return period.toString();
647
    }
648
    return period.toString() + duration.toString().substring(1);
649
  }
650

651
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc