diff --git a/li.strolch.utils/src/main/java/li/strolch/utils/time/PeriodDuration.java b/li.strolch.utils/src/main/java/li/strolch/utils/time/PeriodDuration.java index f869bac0a..052377d7e 100644 --- a/li.strolch.utils/src/main/java/li/strolch/utils/time/PeriodDuration.java +++ b/li.strolch.utils/src/main/java/li/strolch/utils/time/PeriodDuration.java @@ -32,6 +32,7 @@ package li.strolch.utils.time; import static java.time.temporal.ChronoUnit.*; +import static li.strolch.utils.time.PeriodHelper.daysIn; import java.io.Serializable; import java.time.*; @@ -40,612 +41,683 @@ import java.time.chrono.IsoChronology; import java.time.format.DateTimeParseException; import java.time.temporal.*; import java.util.*; +import java.util.concurrent.TimeUnit; /** * An amount of time in the ISO-8601 calendar system that combines a period and a duration. *
- * This class models a quantity or amount of time in terms of a {@code Period} and {@code Duration}. - * A period is a date-based amount of time, consisting of years, months and days. - * A duration is a time-based amount of time, consisting of seconds and nanoseconds. - * See the {@link Period} and {@link Duration} classes for more details. + * This class models a quantity or amount of time in terms of a {@code Period} and {@code Duration}. A period is a + * date-based amount of time, consisting of years, months and days. A duration is a time-based amount of time, + * consisting of seconds and nanoseconds. See the {@link Period} and {@link Duration} classes for more details. *
- * The days in a period take account of daylight saving changes (23 or 25 hour days). - * When performing calculations, the period is added first, then the duration. + * The days in a period take account of daylight saving changes (23 or 25 hour days). When performing calculations, the + * period is added first, then the duration. *
* The model is of a directed amount, meaning that the amount may be negative. * *
- * This class must be treated as a value type. Do not synchronize, rely on the
- * identity hash code or use the distinction between equals() and ==.
+ * This class must be treated as a value type. Do not synchronize, rely on the identity hash code or use the distinction
+ * between equals() and ==.
*/
-public final class PeriodDuration
- implements TemporalAmount, Serializable {
+public final class PeriodDuration implements TemporalAmount, Serializable, Comparable
- * The total amount of time of the resulting instance is the period plus the duration.
- *
- * @param period the period, not null
- * @param duration the duration, not null
- * @return the combined period-duration, not null
- */
- public static PeriodDuration of(Period period, Duration duration) {
- Objects.requireNonNull(period, "The period must not be null");
- Objects.requireNonNull(duration, "The duration must not be null");
- return new PeriodDuration(period, duration);
- }
+ //-----------------------------------------------------------------------
- /**
- * Obtains an instance based on a period.
- *
- * The duration will be zero.
- *
- * @param period the period, not null
- * @return the combined period-duration, not null
- */
- public static PeriodDuration of(Period period) {
- Objects.requireNonNull(period, "The period must not be null");
- return new PeriodDuration(period, Duration.ZERO);
- }
+ /**
+ * Obtains an instance based on a period and duration.
+ *
+ * The total amount of time of the resulting instance is the period plus the duration.
+ *
+ * @param period
+ * the period, not null
+ * @param duration
+ * the duration, not null
+ *
+ * @return the combined period-duration, not null
+ */
+ public static PeriodDuration of(Period period, Duration duration) {
+ Objects.requireNonNull(period, "The period must not be null");
+ Objects.requireNonNull(duration, "The duration must not be null");
+ return new PeriodDuration(period, duration);
+ }
- /**
- * Obtains an instance based on a duration.
- *
- * The period will be zero.
- *
- * @param duration the duration, not null
- * @return the combined period-duration, not null
- */
- public static PeriodDuration of(Duration duration) {
- Objects.requireNonNull(duration, "The duration must not be null");
- return new PeriodDuration(Period.ZERO, duration);
- }
+ /**
+ * Obtains an instance based on a period.
+ *
+ * The duration will be zero.
+ *
+ * @param period
+ * the period, not null
+ *
+ * @return the combined period-duration, not null
+ */
+ public static PeriodDuration of(Period period) {
+ Objects.requireNonNull(period, "The period must not be null");
+ return new PeriodDuration(period, Duration.ZERO);
+ }
- //-----------------------------------------------------------------------
- /**
- * Obtains an instance from a temporal amount.
- *
- * This obtains an instance based on the specified amount.
- * A {@code TemporalAmount} represents an amount of time which this factory
- * extracts to a {@code PeriodDuration}.
- *
- * The result is calculated by looping around each unit in the specified amount.
- * Any amount that is zero is ignore.
- * If a unit has an exact duration, it will be totalled using {@link Duration#plus(Duration)}.
- * If the unit is days or weeks, it will be totalled into the days part of the period.
- * If the unit is months or quarters, it will be totalled into the months part of the period.
- * If the unit is years, decades, centuries or millennia, it will be totalled into the years part of the period.
- *
- * @param amount the temporal amount to convert, not null
- * @return the equivalent duration, not null
- * @throws DateTimeException if unable to convert to a {@code Duration}
- * @throws ArithmeticException if numeric overflow occurs
- */
- public static PeriodDuration from(TemporalAmount amount) {
- if (amount instanceof PeriodDuration) {
- return (PeriodDuration) amount;
- }
- if (amount instanceof Period) {
- return PeriodDuration.of((Period) amount);
- }
- if (amount instanceof Duration) {
- return PeriodDuration.of((Duration) amount);
- }
- if (amount instanceof ChronoPeriod) {
- if (IsoChronology.INSTANCE.equals(((ChronoPeriod) amount).getChronology()) == false) {
- throw new DateTimeException("Period requires ISO chronology: " + amount);
- }
- }
- Objects.requireNonNull(amount, "amount");
- int years = 0;
- int months = 0;
- int days = 0;
- Duration duration = Duration.ZERO;
- for (TemporalUnit unit : amount.getUnits()) {
- long value = amount.get(unit);
- if (value != 0) {
- // ignore unless non-zero
- if (unit.isDurationEstimated()) {
- if (unit == ChronoUnit.DAYS) {
- days = Math.addExact(days, Math.toIntExact(value));
- } else if (unit == ChronoUnit.WEEKS) {
- days = Math.addExact(days, Math.toIntExact(Math.multiplyExact(value, (long) 7)));
- } else if (unit == ChronoUnit.MONTHS) {
- months = Math.addExact(months, Math.toIntExact(value));
- } else if (unit == IsoFields.QUARTER_YEARS) {
- months = Math.addExact(months, Math.toIntExact(Math.multiplyExact(value, (long) 3)));
- } else if (unit == ChronoUnit.YEARS) {
- years = Math.addExact(years, Math.toIntExact(value));
- } else if (unit == ChronoUnit.DECADES) {
- years = Math.addExact(years, Math.toIntExact(Math.multiplyExact(value, (long) 10)));
- } else if (unit == ChronoUnit.CENTURIES) {
- years = Math.addExact(years, Math.toIntExact(Math.multiplyExact(value, (long) 100)));
- } else if (unit == ChronoUnit.MILLENNIA) {
- years = Math.addExact(years, Math.toIntExact(Math.multiplyExact(value, (long) 1000)));
- } else {
- throw new DateTimeException("Unknown unit: " + unit);
- }
- } else {
- // total of exact durations
- duration = duration.plus(amount.get(unit), unit);
- }
- }
- }
- return PeriodDuration.of(Period.of(years, months, days), duration);
- }
+ /**
+ * Obtains an instance based on a duration.
+ *
+ * The period will be zero.
+ *
+ * @param duration
+ * the duration, not null
+ *
+ * @return the combined period-duration, not null
+ */
+ public static PeriodDuration of(Duration duration) {
+ Objects.requireNonNull(duration, "The duration must not be null");
+ return new PeriodDuration(Period.ZERO, duration);
+ }
- //-----------------------------------------------------------------------
- /**
- * Obtains an instance from a text string such as {@code PnYnMnDTnHnMnS}.
- *
- * This will parse the string produced by {@code toString()} which is
- * based on the ISO-8601 period formats {@code PnYnMnDTnHnMnS} and {@code PnW}.
- *
- * The string starts with an optional sign, denoted by the ASCII negative
- * or positive symbol. If negative, the whole amount is negated.
- * The ASCII letter "P" is next in upper or lower case.
- * There are then a number of sections, each consisting of a number and a suffix.
- * At least one of the sections must be present.
- * The sections have suffixes in ASCII of "Y" for years, "M" for months,
- * "W" for weeks, "D" for days, "H" for hours, "M" for minutes, "S" for seconds,
- * accepted in upper or lower case. Note that the ASCII letter "T" separates
- * the date and time parts and must be present if any time part is present.
- * The suffixes must occur in order.
- * The number part of each section must consist of ASCII digits.
- * The number may be prefixed by the ASCII negative or positive symbol.
- * The number must parse to an {@code int}.
- * Any week-based input is multiplied by 7 and treated as a number of days.
- *
- * The leading plus/minus sign, and negative values for weeks and days are
- * not part of the ISO-8601 standard.
- *
- * Note that the date style format {@code PYYYY-MM-DDTHH:MM:SS} is not supported.
- *
- * For example, the following are valid inputs:
- *
- * The start is included, but the end is not.
- * The result of this method can be negative if the end is before the start.
- *
- * The calculation examines the temporals and extracts {@link LocalDate} and {@link LocalTime}.
- * If the time is missing, it will be defaulted to midnight.
- * If one date is missing, it will be defaulted to the other date.
- * It then finds the amount of time between the two dates and between the two times.
- *
- * @param startInclusive the start, inclusive, not null
- * @param endExclusive the end, exclusive, not null
- * @return the number of days between this date and the end date, not null
- */
- public static PeriodDuration between(Temporal startInclusive, Temporal endExclusive) {
- LocalDate startDate = startInclusive.query(TemporalQueries.localDate());
- LocalDate endDate = endExclusive.query(TemporalQueries.localDate());
- Period period = Period.ZERO;
- if (startDate != null && endDate != null) {
- period = Period.between(startDate, endDate);
- }
- LocalTime startTime = startInclusive.query(TemporalQueries.localTime());
- LocalTime endTime = endExclusive.query(TemporalQueries.localTime());
- startTime = startTime != null ? startTime : LocalTime.MIDNIGHT;
- endTime = endTime != null ? endTime : LocalTime.MIDNIGHT;
- Duration duration = Duration.between(startTime, endTime);
- return PeriodDuration.of(period, duration);
- }
+ /**
+ * Obtains an instance from a temporal amount.
+ *
+ * This obtains an instance based on the specified amount. A {@code TemporalAmount} represents an amount of time
+ * which this factory extracts to a {@code PeriodDuration}.
+ *
+ * The result is calculated by looping around each unit in the specified amount. Any amount that is zero is ignore.
+ * If a unit has an exact duration, it will be totalled using {@link Duration#plus(Duration)}. If the unit is days
+ * or weeks, it will be totalled into the days part of the period. If the unit is months or quarters, it will be
+ * totalled into the months part of the period. If the unit is years, decades, centuries or millennia, it will be
+ * totalled into the years part of the period.
+ *
+ * @param amount
+ * the temporal amount to convert, not null
+ *
+ * @return the equivalent duration, not null
+ *
+ * @throws DateTimeException
+ * if unable to convert to a {@code Duration}
+ * @throws ArithmeticException
+ * if numeric overflow occurs
+ */
+ public static PeriodDuration from(TemporalAmount amount) {
+ if (amount instanceof PeriodDuration) {
+ return (PeriodDuration) amount;
+ }
+ if (amount instanceof Period) {
+ return PeriodDuration.of((Period) amount);
+ }
+ if (amount instanceof Duration) {
+ return PeriodDuration.of((Duration) amount);
+ }
+ if (amount instanceof ChronoPeriod) {
+ if (IsoChronology.INSTANCE.equals(((ChronoPeriod) amount).getChronology()) == false) {
+ throw new DateTimeException("Period requires ISO chronology: " + amount);
+ }
+ }
+ Objects.requireNonNull(amount, "amount");
+ int years = 0;
+ int months = 0;
+ int days = 0;
+ Duration duration = Duration.ZERO;
+ for (TemporalUnit unit : amount.getUnits()) {
+ long value = amount.get(unit);
+ if (value != 0) {
+ // ignore unless non-zero
+ if (unit.isDurationEstimated()) {
+ if (unit == ChronoUnit.DAYS) {
+ days = Math.addExact(days, Math.toIntExact(value));
+ } else if (unit == ChronoUnit.WEEKS) {
+ days = Math.addExact(days, Math.toIntExact(Math.multiplyExact(value, (long) 7)));
+ } else if (unit == ChronoUnit.MONTHS) {
+ months = Math.addExact(months, Math.toIntExact(value));
+ } else if (unit == IsoFields.QUARTER_YEARS) {
+ months = Math.addExact(months, Math.toIntExact(Math.multiplyExact(value, (long) 3)));
+ } else if (unit == ChronoUnit.YEARS) {
+ years = Math.addExact(years, Math.toIntExact(value));
+ } else if (unit == ChronoUnit.DECADES) {
+ years = Math.addExact(years, Math.toIntExact(Math.multiplyExact(value, (long) 10)));
+ } else if (unit == ChronoUnit.CENTURIES) {
+ years = Math.addExact(years, Math.toIntExact(Math.multiplyExact(value, (long) 100)));
+ } else if (unit == ChronoUnit.MILLENNIA) {
+ years = Math.addExact(years, Math.toIntExact(Math.multiplyExact(value, (long) 1000)));
+ } else {
+ throw new DateTimeException("Unknown unit: " + unit);
+ }
+ } else {
+ // total of exact durations
+ duration = duration.plus(amount.get(unit), unit);
+ }
+ }
+ }
+ return PeriodDuration.of(Period.of(years, months, days), duration);
+ }
- //-----------------------------------------------------------------------
- /**
- * Constructs an instance.
- *
- * @param period the period
- * @param duration the duration
- */
- private PeriodDuration(Period period, Duration duration) {
- this.period = period;
- this.duration = duration;
- }
+ //-----------------------------------------------------------------------
- /**
- * Resolves singletons.
- *
- * @return the singleton instance
- */
- private Object readResolve() {
- return PeriodDuration.of(period, duration);
- }
+ /**
+ * Obtains an instance from a text string such as {@code PnYnMnDTnHnMnS}.
+ *
+ * This will parse the string produced by {@code toString()} which is based on the ISO-8601 period formats {@code
+ * PnYnMnDTnHnMnS} and {@code PnW}.
+ *
+ * The string starts with an optional sign, denoted by the ASCII negative or positive symbol. If negative, the whole
+ * amount is negated. The ASCII letter "P" is next in upper or lower case. There are then a number of sections, each
+ * consisting of a number and a suffix. At least one of the sections must be present. The sections have suffixes in
+ * ASCII of "Y" for years, "M" for months, "W" for weeks, "D" for days, "H" for hours, "M" for minutes, "S" for
+ * seconds, accepted in upper or lower case. Note that the ASCII letter "T" separates the date and time parts and
+ * must be present if any time part is present. The suffixes must occur in order. The number part of each section
+ * must consist of ASCII digits. The number may be prefixed by the ASCII negative or positive symbol. The number
+ * must parse to an {@code int}. Any week-based input is multiplied by 7 and treated as a number of days.
+ *
+ * The leading plus/minus sign, and negative values for weeks and days are not part of the ISO-8601 standard.
+ *
+ * Note that the date style format {@code PYYYY-MM-DDTHH:MM:SS} is not supported.
+ *
+ * For example, the following are valid inputs:
+ *
- * This returns a value for the supported units - {@link ChronoUnit#YEARS},
- * {@link ChronoUnit#MONTHS}, {@link ChronoUnit#DAYS}, {@link ChronoUnit#SECONDS}
- * and {@link ChronoUnit#NANOS}.
- * All other units throw an exception.
- * Note that hours and minutes throw an exception.
- *
- * @param unit the {@code TemporalUnit} for which to return the value
- * @return the long value of the unit
- * @throws UnsupportedTemporalTypeException if the unit is not supported
- */
- @Override
- public long get(TemporalUnit unit) {
- if (unit instanceof ChronoUnit) {
- switch ((ChronoUnit) unit) {
- case YEARS:
- return period.getYears();
- case MONTHS:
- return period.getMonths();
- case DAYS:
- return period.getDays();
- case SECONDS:
- return duration.getSeconds();
- case NANOS:
- return duration.getNano();
- default:
- break;
- }
- }
- throw new UnsupportedTemporalTypeException("Unsupported unit: " + unit);
- }
+ //-----------------------------------------------------------------------
- /**
- * Gets the set of units supported by this amount.
- *
- * This returns the list {@link ChronoUnit#YEARS}, {@link ChronoUnit#MONTHS},
- * {@link ChronoUnit#DAYS}, {@link ChronoUnit#SECONDS} and {@link ChronoUnit#NANOS}.
- *
- * This set can be used in conjunction with {@link #get(TemporalUnit)}
- * to access the entire state of the amount.
- *
- * @return a list containing the days unit, not null
- */
- @Override
- public List
+ * The start is included, but the end is not. The result of this method can be negative if the end is before the
+ * start.
+ *
+ * The calculation examines the temporals and extracts {@link LocalDate} and {@link LocalTime}. If the time is
+ * missing, it will be defaulted to midnight. If one date is missing, it will be defaulted to the other date. It
+ * then finds the amount of time between the two dates and between the two times.
+ *
+ * @param startInclusive
+ * the start, inclusive, not null
+ * @param endExclusive
+ * the end, exclusive, not null
+ *
+ * @return the number of days between this date and the end date, not null
+ */
+ public static PeriodDuration between(Temporal startInclusive, Temporal endExclusive) {
+ LocalDate startDate = startInclusive.query(TemporalQueries.localDate());
+ LocalDate endDate = endExclusive.query(TemporalQueries.localDate());
+ Period period = Period.ZERO;
+ if (startDate != null && endDate != null) {
+ period = Period.between(startDate, endDate);
+ }
+ LocalTime startTime = startInclusive.query(TemporalQueries.localTime());
+ LocalTime endTime = endExclusive.query(TemporalQueries.localTime());
+ startTime = startTime != null ? startTime : LocalTime.MIDNIGHT;
+ endTime = endTime != null ? endTime : LocalTime.MIDNIGHT;
+ Duration duration = Duration.between(startTime, endTime);
+ return PeriodDuration.of(period, duration);
+ }
- //-----------------------------------------------------------------------
- /**
- * Gets the period part.
- *
- * @return the period part
- */
- public Period getPeriod() {
- return period;
- }
+ //-----------------------------------------------------------------------
- /**
- * Returns a copy of this period-duration with a different period.
- *
- * This instance is immutable and unaffected by this method call.
- *
- * @param period the new period
- * @return the updated period-duration
- */
- public PeriodDuration withPeriod(Period period) {
- return PeriodDuration.of(period, duration);
- }
+ /**
+ * Constructs an instance.
+ *
+ * @param period
+ * the period
+ * @param duration
+ * the duration
+ */
+ private PeriodDuration(Period period, Duration duration) {
+ this.period = period;
+ this.duration = duration;
+ }
- /**
- * Gets the duration part.
- *
- * @return the duration part
- */
- public Duration getDuration() {
- return duration;
- }
+ /**
+ * Resolves singletons.
+ *
+ * @return the singleton instance
+ */
+ private Object readResolve() {
+ return PeriodDuration.of(period, duration);
+ }
- /**
- * Returns a copy of this period-duration with a different duration.
- *
- * This instance is immutable and unaffected by this method call.
- *
- * @param duration the new duration
- * @return the updated period-duration
- */
- public PeriodDuration withDuration(Duration duration) {
- return PeriodDuration.of(period, duration);
- }
+ //-----------------------------------------------------------------------
- //-----------------------------------------------------------------------
- /**
- * Checks if all parts of this amount are zero.
- *
- * This returns true if both {@link Period#isZero()} and {@link Duration#isZero()}
- * return true.
- *
- * @return true if this period is zero-length
- */
- public boolean isZero() {
- return period.isZero() && duration.isZero();
- }
+ /**
+ * Gets the value of the requested unit.
+ *
+ * This returns a value for the supported units - {@link ChronoUnit#YEARS}, {@link ChronoUnit#MONTHS}, {@link
+ * ChronoUnit#DAYS}, {@link ChronoUnit#SECONDS} and {@link ChronoUnit#NANOS}. All other units throw an exception.
+ * Note that hours and minutes throw an exception.
+ *
+ * @param unit
+ * the {@code TemporalUnit} for which to return the value
+ *
+ * @return the long value of the unit
+ *
+ * @throws UnsupportedTemporalTypeException
+ * if the unit is not supported
+ */
+ @Override
+ public long get(TemporalUnit unit) {
+ if (unit instanceof ChronoUnit) {
+ switch ((ChronoUnit) unit) {
+ case YEARS:
+ return period.getYears();
+ case MONTHS:
+ return period.getMonths();
+ case DAYS:
+ return period.getDays();
+ case SECONDS:
+ return duration.getSeconds();
+ case NANOS:
+ return duration.getNano();
+ default:
+ break;
+ }
+ }
+ throw new UnsupportedTemporalTypeException("Unsupported unit: " + unit);
+ }
- //-----------------------------------------------------------------------
- /**
- * Returns a copy of this amount with the specified amount added.
- *
- * The parameter is converted using {@link PeriodDuration#from(TemporalAmount)}.
- * The period and duration are combined separately.
- *
- * This instance is immutable and unaffected by this method call.
- *
- * @param amountToAdd the amount to add, not null
- * @return a {@code Days} based on this instance with the requested amount added, not null
- * @throws DateTimeException if the specified amount contains an invalid unit
- * @throws ArithmeticException if numeric overflow occurs
- */
- public PeriodDuration plus(TemporalAmount amountToAdd) {
- PeriodDuration other = PeriodDuration.from(amountToAdd);
- return of(period.plus(other.period), duration.plus(other.duration));
- }
+ /**
+ * Gets the set of units supported by this amount.
+ *
+ * This returns the list {@link ChronoUnit#YEARS}, {@link ChronoUnit#MONTHS}, {@link ChronoUnit#DAYS}, {@link
+ * ChronoUnit#SECONDS} and {@link ChronoUnit#NANOS}.
+ *
+ * This set can be used in conjunction with {@link #get(TemporalUnit)} to access the entire state of the amount.
+ *
+ * @return a list containing the days unit, not null
+ */
+ @Override
+ public List
- * The parameter is converted using {@link PeriodDuration#from(TemporalAmount)}.
- * The period and duration are combined separately.
- *
- * This instance is immutable and unaffected by this method call.
- *
- * @param amountToAdd the amount to add, not null
- * @return a {@code Days} based on this instance with the requested amount subtracted, not null
- * @throws DateTimeException if the specified amount contains an invalid unit
- * @throws ArithmeticException if numeric overflow occurs
- */
- public PeriodDuration minus(TemporalAmount amountToAdd) {
- PeriodDuration other = PeriodDuration.from(amountToAdd);
- return of(period.minus(other.period), duration.minus(other.duration));
- }
+ //-----------------------------------------------------------------------
- //-----------------------------------------------------------------------
- /**
- * Returns an instance with the amount multiplied by the specified scalar.
- *
- * This instance is immutable and unaffected by this method call.
- *
- * @param scalar the scalar to multiply by, not null
- * @return the amount multiplied by the specified scalar, not null
- * @throws ArithmeticException if numeric overflow occurs
- */
- public PeriodDuration multipliedBy(int scalar) {
- if (scalar == 1) {
- return this;
- }
- return of(period.multipliedBy(scalar), duration.multipliedBy(scalar));
- }
+ /**
+ * Gets the period part.
+ *
+ * @return the period part
+ */
+ public Period getPeriod() {
+ return period;
+ }
- /**
- * Returns an instance with the amount negated.
- *
- * This instance is immutable and unaffected by this method call.
- *
- * @return the negated amount, not null
- * @throws ArithmeticException if numeric overflow occurs, which only happens if
- * the amount is {@code Long.MIN_VALUE}
- */
- public PeriodDuration negated() {
- return multipliedBy(-1);
- }
+ /**
+ * Returns a copy of this period-duration with a different period.
+ *
+ * This instance is immutable and unaffected by this method call.
+ *
+ * @param period
+ * the new period
+ *
+ * @return the updated period-duration
+ */
+ public PeriodDuration withPeriod(Period period) {
+ return PeriodDuration.of(period, duration);
+ }
- //-----------------------------------------------------------------------
- /**
- * Returns a copy of this instance with the years and months exactly normalized.
- *
- * This normalizes the years and months units, leaving the days unit unchanged.
- * The result is exact, always representing the same amount of time.
- *
- * The months unit is adjusted to have an absolute value less than 11,
- * with the years unit being adjusted to compensate. For example, a period of
- * "1 year and 15 months" will be normalized to "2 years and 3 months".
- *
- * The sign of the years and months units will be the same after normalization.
- * For example, a period of "1 year and -25 months" will be normalized to
- * "-1 year and -1 month".
- *
- * Note that no normalization is performed on the days or duration.
- *
- * This instance is immutable and unaffected by this method call.
- *
- * @return a {@code PeriodDuration} based on this one with excess months normalized to years, not null
- * @throws ArithmeticException if numeric overflow occurs
- */
- public PeriodDuration normalizedYears() {
- return withPeriod(period.normalized());
- }
+ /**
+ * Gets the duration part.
+ *
+ * @return the duration part
+ */
+ public Duration getDuration() {
+ return duration;
+ }
- /**
- * Returns a copy of this instance with the days and duration normalized using the standard day of 24 hours.
- *
- * This normalizes the days and duration, leaving the years and months unchanged.
- * The result uses a standard day length of 24 hours.
- *
- * This combines the duration seconds with the number of days and shares the total
- * seconds between the two fields. For example, a period of
- * "2 days and 86401 seconds" will be normalized to "3 days and 1 second".
- *
- * The sign of the days and duration will be the same after normalization.
- * For example, a period of "1 day and -172801 seconds" will be normalized to
- * "-1 day and -1 second".
- *
- * Note that no normalization is performed on the years or months.
- *
- * This instance is immutable and unaffected by this method call.
- *
- * @return a {@code PeriodDuration} based on this one with excess duration normalized to days, not null
- * @throws ArithmeticException if numeric overflow occurs
- */
- public PeriodDuration normalizedStandardDays() {
- long totalSecs = period.getDays() * SECONDS_PER_DAY + duration.getSeconds();
- int splitDays = Math.toIntExact(totalSecs / SECONDS_PER_DAY);
- long splitSecs = totalSecs % SECONDS_PER_DAY;
- if (splitDays == period.getDays() && splitSecs == duration.getSeconds()) {
- return this;
- }
- return PeriodDuration.of(period.withDays(splitDays), duration.withSeconds(splitSecs));
- }
+ /**
+ * Returns a copy of this period-duration with a different duration.
+ *
+ * This instance is immutable and unaffected by this method call.
+ *
+ * @param duration
+ * the new duration
+ *
+ * @return the updated period-duration
+ */
+ public PeriodDuration withDuration(Duration duration) {
+ return PeriodDuration.of(period, duration);
+ }
- //-----------------------------------------------------------------------
- /**
- * Adds this amount to the specified temporal object.
- *
- * This returns a temporal object of the same observable type as the input
- * with this amount added. This simply adds the period and duration to the temporal.
- *
- * This instance is immutable and unaffected by this method call.
- *
- * @param temporal the temporal object to adjust, not null
- * @return an object of the same type with the adjustment made, not null
- * @throws DateTimeException if unable to add
- * @throws UnsupportedTemporalTypeException if the DAYS unit is not supported
- * @throws ArithmeticException if numeric overflow occurs
- */
- @Override
- public Temporal addTo(Temporal temporal) {
- return temporal.plus(period).plus(duration);
- }
+ //-----------------------------------------------------------------------
- /**
- * Subtracts this amount from the specified temporal object.
- *
- * This returns a temporal object of the same observable type as the input
- * with this amount subtracted. This simply subtracts the period and duration from the temporal.
- *
- * This instance is immutable and unaffected by this method call.
- *
- * @param temporal the temporal object to adjust, not null
- * @return an object of the same type with the adjustment made, not null
- * @throws DateTimeException if unable to subtract
- * @throws UnsupportedTemporalTypeException if the DAYS unit is not supported
- * @throws ArithmeticException if numeric overflow occurs
- */
- @Override
- public Temporal subtractFrom(Temporal temporal) {
- return temporal.minus(period).minus(duration);
- }
+ /**
+ * Checks if all parts of this amount are zero.
+ *
+ * This returns true if both {@link Period#isZero()} and {@link Duration#isZero()} return true.
+ *
+ * @return true if this period is zero-length
+ */
+ public boolean isZero() {
+ return period.isZero() && duration.isZero();
+ }
- //-----------------------------------------------------------------------
- /**
- * Checks if this amount is equal to the specified {@code PeriodDuration}.
- *
- * The comparison is based on the underlying period and duration.
- *
- * @param otherAmount the other amount, null returns false
- * @return true if the other amount is equal to this one
- */
- @Override
- public boolean equals(Object otherAmount) {
- if (this == otherAmount) {
- return true;
- }
- if (otherAmount instanceof PeriodDuration) {
- PeriodDuration other = (PeriodDuration) otherAmount;
- return this.period.equals(other.period) && this.duration.equals(other.duration);
- }
- return false;
- }
+ //-----------------------------------------------------------------------
- /**
- * A hash code for this amount.
- *
- * @return a suitable hash code
- */
- @Override
- public int hashCode() {
- return period.hashCode() ^ duration.hashCode();
- }
+ /**
+ * Returns a copy of this amount with the specified amount added.
+ *
+ * The parameter is converted using {@link PeriodDuration#from(TemporalAmount)}. The period and duration are
+ * combined separately.
+ *
+ * This instance is immutable and unaffected by this method call.
+ *
+ * @param amountToAdd
+ * the amount to add, not null
+ *
+ * @return a {@code Days} based on this instance with the requested amount added, not null
+ *
+ * @throws DateTimeException
+ * if the specified amount contains an invalid unit
+ * @throws ArithmeticException
+ * if numeric overflow occurs
+ */
+ public PeriodDuration plus(TemporalAmount amountToAdd) {
+ PeriodDuration other = PeriodDuration.from(amountToAdd);
+ return of(period.plus(other.period), duration.plus(other.duration));
+ }
- //-----------------------------------------------------------------------
- /**
- * Returns a string representation of the amount.
- * This will be in the format 'PnYnMnDTnHnMnS', with sections omitted as necessary.
- * An empty amount will return "PT0S".
- *
- * @return the period in ISO-8601 string format
- */
- @Override
- public String toString() {
- if (period.isZero()) {
- return duration.toString();
- }
- if (duration.isZero()) {
- return period.toString();
- }
- return period.toString() + duration.toString().substring(1);
- }
+ //-----------------------------------------------------------------------
+ /**
+ * Returns a copy of this amount with the specified amount subtracted.
+ *
+ * The parameter is converted using {@link PeriodDuration#from(TemporalAmount)}. The period and duration are
+ * combined separately.
+ *
+ * This instance is immutable and unaffected by this method call.
+ *
+ * @param amountToAdd
+ * the amount to add, not null
+ *
+ * @return a {@code Days} based on this instance with the requested amount subtracted, not null
+ *
+ * @throws DateTimeException
+ * if the specified amount contains an invalid unit
+ * @throws ArithmeticException
+ * if numeric overflow occurs
+ */
+ public PeriodDuration minus(TemporalAmount amountToAdd) {
+ PeriodDuration other = PeriodDuration.from(amountToAdd);
+ return of(period.minus(other.period), duration.minus(other.duration));
+ }
+
+ //-----------------------------------------------------------------------
+
+ /**
+ * Returns an instance with the amount multiplied by the specified scalar.
+ *
+ * This instance is immutable and unaffected by this method call.
+ *
+ * @param scalar
+ * the scalar to multiply by, not null
+ *
+ * @return the amount multiplied by the specified scalar, not null
+ *
+ * @throws ArithmeticException
+ * if numeric overflow occurs
+ */
+ public PeriodDuration multipliedBy(int scalar) {
+ if (scalar == 1) {
+ return this;
+ }
+ return of(period.multipliedBy(scalar), duration.multipliedBy(scalar));
+ }
+
+ /**
+ * Returns an instance with the amount negated.
+ *
+ * This instance is immutable and unaffected by this method call.
+ *
+ * @return the negated amount, not null
+ *
+ * @throws ArithmeticException
+ * if numeric overflow occurs, which only happens if the amount is {@code Long.MIN_VALUE}
+ */
+ public PeriodDuration negated() {
+ return multipliedBy(-1);
+ }
+
+ //-----------------------------------------------------------------------
+
+ /**
+ * Returns a copy of this instance with the years and months exactly normalized.
+ *
+ * This normalizes the years and months units, leaving the days unit unchanged. The result is exact, always
+ * representing the same amount of time.
+ *
+ * The months unit is adjusted to have an absolute value less than 11, with the years unit being adjusted to
+ * compensate. For example, a period of "1 year and 15 months" will be normalized to "2 years and 3 months".
+ *
+ * The sign of the years and months units will be the same after normalization. For example, a period of "1 year and
+ * -25 months" will be normalized to "-1 year and -1 month".
+ *
+ * Note that no normalization is performed on the days or duration.
+ *
+ * This instance is immutable and unaffected by this method call.
+ *
+ * @return a {@code PeriodDuration} based on this one with excess months normalized to years, not null
+ *
+ * @throws ArithmeticException
+ * if numeric overflow occurs
+ */
+ public PeriodDuration normalizedYears() {
+ return withPeriod(period.normalized());
+ }
+
+ /**
+ * Returns a copy of this instance with the days and duration normalized using the standard day of 24 hours.
+ *
+ * This normalizes the days and duration, leaving the years and months unchanged. The result uses a standard day
+ * length of 24 hours.
+ *
+ * This combines the duration seconds with the number of days and shares the total seconds between the two fields.
+ * For example, a period of "2 days and 86401 seconds" will be normalized to "3 days and 1 second".
+ *
+ * The sign of the days and duration will be the same after normalization. For example, a period of "1 day and
+ * -172801 seconds" will be normalized to "-1 day and -1 second".
+ *
+ * Note that no normalization is performed on the years or months.
+ *
+ * This instance is immutable and unaffected by this method call.
+ *
+ * @return a {@code PeriodDuration} based on this one with excess duration normalized to days, not null
+ *
+ * @throws ArithmeticException
+ * if numeric overflow occurs
+ */
+ public PeriodDuration normalizedStandardDays() {
+ long totalSecs = period.getDays() * SECONDS_PER_DAY + duration.getSeconds();
+ int splitDays = Math.toIntExact(totalSecs / SECONDS_PER_DAY);
+ long splitSecs = totalSecs % SECONDS_PER_DAY;
+ if (splitDays == period.getDays() && splitSecs == duration.getSeconds()) {
+ return this;
+ }
+ return PeriodDuration.of(period.withDays(splitDays), duration.withSeconds(splitSecs));
+ }
+
+ //-----------------------------------------------------------------------
+
+ /**
+ * Adds this amount to the specified temporal object.
+ *
+ * This returns a temporal object of the same observable type as the input with this amount added. This simply adds
+ * the period and duration to the temporal.
+ *
+ * This instance is immutable and unaffected by this method call.
+ *
+ * @param temporal
+ * the temporal object to adjust, not null
+ *
+ * @return an object of the same type with the adjustment made, not null
+ *
+ * @throws DateTimeException
+ * if unable to add
+ * @throws UnsupportedTemporalTypeException
+ * if the DAYS unit is not supported
+ * @throws ArithmeticException
+ * if numeric overflow occurs
+ */
+ @Override
+ public Temporal addTo(Temporal temporal) {
+ return temporal.plus(period).plus(duration);
+ }
+
+ /**
+ * Subtracts this amount from the specified temporal object.
+ *
+ * This returns a temporal object of the same observable type as the input with this amount subtracted. This simply
+ * subtracts the period and duration from the temporal.
+ *
+ * This instance is immutable and unaffected by this method call.
+ *
+ * @param temporal
+ * the temporal object to adjust, not null
+ *
+ * @return an object of the same type with the adjustment made, not null
+ *
+ * @throws DateTimeException
+ * if unable to subtract
+ * @throws UnsupportedTemporalTypeException
+ * if the DAYS unit is not supported
+ * @throws ArithmeticException
+ * if numeric overflow occurs
+ */
+ @Override
+ public Temporal subtractFrom(Temporal temporal) {
+ return temporal.minus(period).minus(duration);
+ }
+
+ //-----------------------------------------------------------------------
+
+ /**
+ * Checks if this amount is equal to the specified {@code PeriodDuration}.
+ *
+ * The comparison is based on the underlying period and duration.
+ *
+ * @param otherAmount
+ * the other amount, null returns false
+ *
+ * @return true if the other amount is equal to this one
+ */
+ @Override
+ public boolean equals(Object otherAmount) {
+ if (this == otherAmount) {
+ return true;
+ }
+ if (otherAmount instanceof PeriodDuration) {
+ PeriodDuration other = (PeriodDuration) otherAmount;
+ return this.period.equals(other.period) && this.duration.equals(other.duration);
+ }
+ return false;
+ }
+
+ /**
+ * A hash code for this amount.
+ *
+ * @return a suitable hash code
+ */
+ @Override
+ public int hashCode() {
+ return period.hashCode() ^ duration.hashCode();
+ }
+
+ //-----------------------------------------------------------------------
+
+ /**
+ * Returns a string representation of the amount. This will be in the format 'PnYnMnDTnHnMnS', with sections omitted
+ * as necessary. An empty amount will return "PT0S".
+ *
+ * @return the period in ISO-8601 string format
+ */
+ @Override
+ public String toString() {
+ if (period.isZero()) {
+ return duration.toString();
+ }
+ if (duration.isZero()) {
+ return period.toString();
+ }
+ return period.toString() + duration.toString().substring(1);
+ }
+
+ @Override
+ public int compareTo(PeriodDuration o) {
+ return Double.compare(daysIn(this), daysIn(o));
+ }
+
+ public int periodPartToDays() {
+ return (getPeriod().getYears() * 365) + (getPeriod().getMonths() * 30) + getPeriod().getDays();
+ }
+
+ public long toMillis() {
+ if (this.period.isZero())
+ return this.duration.toMillis();
+ return TimeUnit.DAYS.toMillis(periodPartToDays()) + this.duration.toMillis();
+ }
}
diff --git a/li.strolch.utils/src/main/java/li/strolch/utils/time/PeriodHelper.java b/li.strolch.utils/src/main/java/li/strolch/utils/time/PeriodHelper.java
new file mode 100644
index 000000000..f58b9c5bf
--- /dev/null
+++ b/li.strolch.utils/src/main/java/li/strolch/utils/time/PeriodHelper.java
@@ -0,0 +1,56 @@
+package li.strolch.utils.time;
+
+import static java.time.Period.between;
+
+import java.time.Period;
+import java.time.ZonedDateTime;
+
+import li.strolch.utils.dbc.DBC;
+
+public class PeriodHelper {
+
+ public static double daysIn(PeriodDuration periodDuration) {
+ return (daysIn(periodDuration.getPeriod()) + (periodDuration.getDuration().toHours() / 24.0));
+ }
+
+ public static double daysIn(Period period) {
+ return (period.getYears() * 365.0) + (period.getMonths() * 30.0) + period.getDays();
+ }
+
+ /**
+ * This special function allows us to shift a date by a multiple of the given {@link PeriodDuration} so that is
+ * before the given to date. It does multiple tries to get as close as possible, due to the inexactness of 30 days
+ * being one month, and 365 days being one year.
+ *
+ * @param date
+ * the date to shift
+ * @param to
+ * the date before which to stop shifting
+ * @param periodDuration
+ * the period shift in multiples by
+ *
+ * @return the shifted date
+ */
+ public static ZonedDateTime shiftByMultipleOfPeriod(ZonedDateTime date, ZonedDateTime to,
+ PeriodDuration periodDuration) {
+ DBC.PRE.assertTrue("date must be before to!", date.isBefore(to));
+ DBC.PRE.assertFalse("period duration may not be null!", periodDuration.isZero());
+ Period between = between(date.toLocalDate(), to.toLocalDate());
+ double daysInBetween = daysIn(between);
+ double daysInPeriod = daysIn(periodDuration);
+ long shifts = (long) (daysInBetween / daysInPeriod);
+ if (shifts < 0)
+ return date;
+
+ ZonedDateTime shiftedDate = date.plusDays((long) (shifts * daysInPeriod));
+
+ // see if we are close enough now
+ between = between(shiftedDate.toLocalDate(), to.toLocalDate());
+ daysInBetween = daysIn(between);
+ shifts = (long) (daysInBetween / daysInPeriod);
+ if (shifts < 2)
+ return shiftedDate;
+
+ return shiftByMultipleOfPeriod(shiftedDate, to, periodDuration);
+ }
+}
diff --git a/li.strolch.utils/src/test/java/li/strolch/utils/time/PeriodHelperTest.java b/li.strolch.utils/src/test/java/li/strolch/utils/time/PeriodHelperTest.java
new file mode 100644
index 000000000..cf993edb2
--- /dev/null
+++ b/li.strolch.utils/src/test/java/li/strolch/utils/time/PeriodHelperTest.java
@@ -0,0 +1,116 @@
+package li.strolch.utils.time;
+
+import static java.time.ZoneId.systemDefault;
+import static li.strolch.utils.time.PeriodHelper.*;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+
+import org.junit.Test;
+
+public class PeriodHelperTest {
+
+ @Test
+ public void shouldCalcHalfADay() {
+ assertEquals(0.5, daysIn(PeriodDuration.parse("PT12H")), 0.0);
+ }
+
+ @Test
+ public void shouldCalc1Day1() {
+ assertEquals(1.0, daysIn(PeriodDuration.parse("P1D")), 0.0);
+ }
+
+ @Test
+ public void shouldCalc1Day2() {
+ assertEquals(1.0, daysIn(PeriodDuration.parse("PT24H")), 0.0);
+ }
+
+ @Test
+ public void shouldCalc1AndAHalfDays() {
+ assertEquals(1.5, daysIn(PeriodDuration.parse("PT36H")), 0.0);
+ }
+
+ @Test
+ public void shouldCalc2Days1() {
+ assertEquals(2.0, daysIn(PeriodDuration.parse("P2D")), 0.0);
+ }
+
+ @Test
+ public void shouldCalc2Days2() {
+ assertEquals(2.0, daysIn(PeriodDuration.parse("PT48H")), 0.0);
+ }
+
+ @Test
+ public void shouldCalc2Days3() {
+ assertEquals(2.0, daysIn(PeriodDuration.parse("P1DT24H")), 0.0);
+ }
+
+ @Test
+ public void shouldCalc2AndAHalfDays0() {
+ assertEquals(2.5, daysIn(PeriodDuration.parse("P1DT36H")), 0.0);
+ }
+
+ @Test
+ public void shouldCalc3Days() {
+ assertEquals(3.0, daysIn(PeriodDuration.parse("PT71H60M")), 0.0);
+ }
+
+ @Test
+ public void shouldCalc2Days23H() {
+ assertEquals(2.958333333, daysIn(PeriodDuration.parse("PT71H")), 0.00001);
+ }
+
+ @Test
+ public void shouldCalcDays() {
+ assertEquals(30, daysIn(PeriodDuration.parse("P1M")), 0.0);
+ }
+
+ @Test
+ public void shouldCalcShiftDays1() {
+ ZonedDateTime past = ZonedDateTime.now().minusDays(35);
+ ZonedDateTime now = ZonedDateTime.now();
+ PeriodDuration periodDuration = PeriodDuration.parse("P1M");
+ ZonedDateTime shiftedDate = shiftByMultipleOfPeriod(past, now, periodDuration);
+
+ assertTrue(shiftedDate.isAfter(now.minusDays(29)));
+ assertTrue(shiftedDate.isBefore(now));
+ }
+
+ @Test
+ public void shouldCalcShiftDays2() {
+ ZonedDateTime past = ZonedDateTime
+ .parse("2007-12-03T10:15:30+01:00", DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(systemDefault()));
+ ZonedDateTime now = ZonedDateTime.now();
+ PeriodDuration periodDuration = PeriodDuration.parse("P1M");
+ ZonedDateTime shiftedDate = shiftByMultipleOfPeriod(past, now, periodDuration);
+
+ // since P1M ist = 28 days, we have a rather inexact match, but it must certainly be before now() - P1M
+ assertTrue(shiftedDate.isAfter(now.minusDays(56)));
+ assertTrue(shiftedDate.isBefore(now.minusDays(28)));
+ }
+
+ @Test
+ public void shouldCalcShiftDays3() {
+ ZonedDateTime past = ZonedDateTime.now().minusDays(20);
+ ZonedDateTime now = ZonedDateTime.now();
+ PeriodDuration periodDuration = PeriodDuration.parse("P7D");
+ ZonedDateTime shiftedDate = shiftByMultipleOfPeriod(past, now, periodDuration);
+
+ assertTrue(shiftedDate.isAfter(now.minusDays(9)));
+ assertTrue(shiftedDate.isBefore(now));
+ }
+
+ @Test
+ public void shouldCalcShiftDays4() {
+ ZonedDateTime past = ZonedDateTime
+ .parse("2007-12-03T10:15:30+01:00", DateTimeFormatter.ISO_OFFSET_DATE_TIME.withZone(systemDefault()));
+ ZonedDateTime now = ZonedDateTime.now();
+ PeriodDuration periodDuration = PeriodDuration.parse("P7D");
+ ZonedDateTime shiftedDate = shiftByMultipleOfPeriod(past, now, periodDuration);
+
+ // since we are many years and months before now, and a year is 356 days and a month is 28 days, we inexact, but at least we must be a more than P7D before now
+ assertTrue(shiftedDate.isBefore(now));
+ }
+}
- * "P2Y" -- PeriodDuration.of(Period.ofYears(2))
- * "P3M" -- PeriodDuration.of(Period.ofMonths(3))
- * "P4W" -- PeriodDuration.of(Period.ofWeeks(4))
- * "P5D" -- PeriodDuration.of(Period.ofDays(5))
- * "PT6H" -- PeriodDuration.of(Duration.ofHours(6))
- * "P1Y2M3D" -- PeriodDuration.of(Period.of(1, 2, 3))
- * "P1Y2M3W4DT8H" -- PeriodDuration.of(Period.of(1, 2, 25), Duration.ofHours(8))
- * "P-1Y2M" -- PeriodDuration.of(Period.of(-1, 2, 0))
- * "-P1Y2M" -- PeriodDuration.of(Period.of(-1, -2, 0))
- *
- *
- * @param text the text to parse, not null
- * @return the parsed period, not null
- * @throws DateTimeParseException if the text cannot be parsed to a period
- */
- public static PeriodDuration parse(CharSequence text) {
- Objects.requireNonNull(text, "text");
- String upper = text.toString().toUpperCase(Locale.ENGLISH);
- String negate = "";
- if (upper.startsWith("+")) {
- upper = upper.substring(1);
- } else if (upper.startsWith("-")) {
- upper = upper.substring(1);
- negate = "-";
- }
- // duration only, parse original text so it does negation
- if (upper.startsWith("PT")) {
- return PeriodDuration.of(Duration.parse(text));
- }
- // period only, parse original text so it does negation
- int tpos = upper.indexOf('T');
- if (tpos < 0) {
- return PeriodDuration.of(Period.parse(text));
- }
- // period and duration
- Period period = Period.parse(negate + upper.substring(0, tpos));
- Duration duration = Duration.parse(negate + "P" + upper.substring(tpos));
- return PeriodDuration.of(period, duration);
- }
+ //-----------------------------------------------------------------------
- //-----------------------------------------------------------------------
- /**
- * Obtains an instance consisting of the amount of time between two temporals.
- *
+ * "P2Y" -- PeriodDuration.of(Period.ofYears(2))
+ * "P3M" -- PeriodDuration.of(Period.ofMonths(3))
+ * "P4W" -- PeriodDuration.of(Period.ofWeeks(4))
+ * "P5D" -- PeriodDuration.of(Period.ofDays(5))
+ * "PT6H" -- PeriodDuration.of(Duration.ofHours(6))
+ * "P1Y2M3D" -- PeriodDuration.of(Period.of(1, 2, 3))
+ * "P1Y2M3W4DT8H" -- PeriodDuration.of(Period.of(1, 2, 25), Duration.ofHours(8))
+ * "P-1Y2M" -- PeriodDuration.of(Period.of(-1, 2, 0))
+ * "-P1Y2M" -- PeriodDuration.of(Period.of(-1, -2, 0))
+ *
+ *
+ * @param text
+ * the text to parse, not null
+ *
+ * @return the parsed period, not null
+ *
+ * @throws DateTimeParseException
+ * if the text cannot be parsed to a period
+ */
+ public static PeriodDuration parse(CharSequence text) {
+ Objects.requireNonNull(text, "text");
+ String upper = text.toString().toUpperCase(Locale.ENGLISH);
+ String negate = "";
+ if (upper.startsWith("+")) {
+ upper = upper.substring(1);
+ } else if (upper.startsWith("-")) {
+ upper = upper.substring(1);
+ negate = "-";
+ }
+ // duration only, parse original text so it does negation
+ if (upper.startsWith("PT")) {
+ return PeriodDuration.of(Duration.parse(text));
+ }
+ // period only, parse original text so it does negation
+ int tpos = upper.indexOf('T');
+ if (tpos < 0) {
+ return PeriodDuration.of(Period.parse(text));
+ }
+ // period and duration
+ Period period = Period.parse(negate + upper.substring(0, tpos));
+ Duration duration = Duration.parse(negate + "P" + upper.substring(tpos));
+ return PeriodDuration.of(period, duration);
+ }
- //-----------------------------------------------------------------------
- /**
- * Gets the value of the requested unit.
- *