[New] Added PeriodDuration, Interval and PeriodDurationFormatter from threeten

This commit is contained in:
Robert von Burg 2019-08-30 16:02:00 +02:00
parent 4ef3ae0869
commit 0a227cd2e3
6 changed files with 1367 additions and 0 deletions

View File

@ -0,0 +1,14 @@
package li.strolch.utils;
import java.util.Locale;
import java.util.ResourceBundle;
public class I18nStrolchUtilsBundle {
public static String i18n(Locale locale, String key) {
ResourceBundle bundle = ResourceBundle.getBundle("strolch-utils", locale);
if (bundle.containsKey(key))
return bundle.getString(key);
return key;
}
}

View File

@ -0,0 +1,590 @@
/*
* Copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of JSR-310 nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package li.strolch.utils.time;
import java.io.Serializable;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.time.temporal.TemporalAccessor;
import java.util.Objects;
/**
* An immutable interval of time between two instants.
* <p>
* An interval represents the time on the time-line between two {@link Instant}s.
* The class stores the start and end instants, with the start inclusive and the end exclusive.
* The end instant is always greater than or equal to the start instant.
* <p>
* The {@link Duration} of an interval can be obtained, but is a separate concept.
* An interval is connected to the time-line, whereas a duration is not.
* <p>
* Intervals are not comparable. To compare the length of two intervals, it is
* generally recommended to compare their durations.
*
* <h3>Implementation Requirements:</h3>
* This class is immutable and thread-safe.
* <p>
* 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 Interval
implements Serializable {
/**
* An interval over the whole time-line.
*/
public static final Interval ALL = new Interval(Instant.MIN, Instant.MAX);
/**
* Serialization version.
*/
private static final long serialVersionUID = 8375285238652L;
/**
* The start instant (inclusive).
*/
private final Instant start;
/**
* The end instant (exclusive).
*/
private final Instant end;
//-----------------------------------------------------------------------
/**
* Obtains an instance of {@code Interval} from the start and end instant.
* <p>
* The end instant must not be before the start instant.
*
* @param startInclusive the start instant, inclusive, MIN_DATE treated as unbounded, not null
* @param endExclusive the end instant, exclusive, MAX_DATE treated as unbounded, not null
* @return the half-open interval, not null
* @throws DateTimeException if the end is before the start
*/
public static Interval of(Instant startInclusive, Instant endExclusive) {
Objects.requireNonNull(startInclusive, "startInclusive");
Objects.requireNonNull(endExclusive, "endExclusive");
if (endExclusive.isBefore(startInclusive)) {
throw new DateTimeException("End instant must be equal or after start instant");
}
return new Interval(startInclusive, endExclusive);
}
/**
* Obtains an instance of {@code Interval} from the start and a duration.
* <p>
* The end instant is calculated as the start plus the duration.
* The duration must not be negative.
*
* @param startInclusive the start instant, inclusive, not null
* @param duration the duration from the start to the end, not null
* @return the interval, not null
* @throws DateTimeException if the end is before the start,
* or if the duration addition cannot be made
* @throws ArithmeticException if numeric overflow occurs when adding the duration
*/
public static Interval of(Instant startInclusive, Duration duration) {
Objects.requireNonNull(startInclusive, "startInclusive");
Objects.requireNonNull(duration, "duration");
if (duration.isNegative()) {
throw new DateTimeException("Duration must not be negative");
}
return new Interval(startInclusive, startInclusive.plus(duration));
}
//-----------------------------------------------------------------------
/**
* Obtains an instance of {@code Interval} from a text string such as
* {@code 2007-12-03T10:15:30Z/2007-12-04T10:15:30Z}, where the end instant is exclusive.
* <p>
* The string must consist of one of the following four formats:
* <ul>
* <li>a representations of an {@link OffsetDateTime}, followed by a forward slash,
* followed by a representation of a {@link OffsetDateTime}
* <li>a representations of an {@link OffsetDateTime}, followed by a forward slash,
* followed by a representation of a {@link LocalDateTime}, where the end offset is implied.
* <li>a representation of an {@link OffsetDateTime}, followed by a forward slash,
* followed by a representation of a {@link PeriodDuration}
* <li>a representation of a {@link PeriodDuration}, followed by a forward slash,
* followed by a representation of an {@link OffsetDateTime}
* </ul>
* <p>
* ISO-8601 supports a very wide range of possible inputs, many of which are not supported here.
* For example, basic format, week-based dates, ordinal dates and date-style period formats are not supported.
*
* @param text the text to parse, not null
* @return the parsed interval, not null
* @throws DateTimeParseException if the text cannot be parsed
*/
public static Interval parse(CharSequence text) {
Objects.requireNonNull(text, "text");
for (int i = 0; i < text.length(); i++) {
if (text.charAt(i) == '/') {
return parseSplit(text.subSequence(0, i), text.subSequence(i + 1, text.length()));
}
}
throw new DateTimeParseException("Interval cannot be parsed, no forward slash found", text, 0);
}
private static Interval parseSplit(CharSequence startStr, CharSequence endStr) {
char firstChar = startStr.charAt(0);
if (firstChar == 'P' || firstChar == 'p') {
// duration followed by instant
PeriodDuration amount = PeriodDuration.parse(startStr);
try {
OffsetDateTime end = OffsetDateTime.parse(endStr);
return Interval.of(end.minus(amount).toInstant(), end.toInstant());
} catch (DateTimeParseException ex) {
// handle case where Instant is outside the bounds of OffsetDateTime
Instant end = Instant.parse(endStr);
// addition of PeriodDuration only supported by OffsetDateTime,
// but to make that work need to move point being subtracted from closer to EPOCH
long move = end.isBefore(Instant.EPOCH) ? 1000 * 86400 : -1000 * 86400;
Instant start = end.plusSeconds(move).atOffset(ZoneOffset.UTC).minus(amount).toInstant().minusSeconds(move);
return Interval.of(start, end);
}
}
// instant followed by instant or duration
OffsetDateTime start;
try {
start = OffsetDateTime.parse(startStr);
} catch (DateTimeParseException ex) {
return parseStartExtended(startStr, endStr);
}
if (endStr.length() > 0) {
char c = endStr.charAt(0);
if (c == 'P' || c == 'p') {
PeriodDuration amount = PeriodDuration.parse(endStr);
return Interval.of(start.toInstant(), start.plus(amount).toInstant());
}
}
return parseEndDateTime(start.toInstant(), start.getOffset(), endStr);
}
// handle case where Instant is outside the bounds of OffsetDateTime
private static Interval parseStartExtended(CharSequence startStr, CharSequence endStr) {
Instant start = Instant.parse(startStr);
if (endStr.length() > 0) {
char c = endStr.charAt(0);
if (c == 'P' || c == 'p') {
PeriodDuration amount = PeriodDuration.parse(endStr);
// addition of PeriodDuration only supported by OffsetDateTime,
// but to make that work need to move point being added to closer to EPOCH
long move = start.isBefore(Instant.EPOCH) ? 1000 * 86400 : -1000 * 86400;
Instant end = start.plusSeconds(move).atOffset(ZoneOffset.UTC).plus(amount).toInstant().minusSeconds(move);
return Interval.of(start, end);
}
}
// infer offset from start if not specified by end
return parseEndDateTime(start, ZoneOffset.UTC, endStr);
}
// parse when there are two date-times
private static Interval parseEndDateTime(Instant start, ZoneOffset offset, CharSequence endStr) {
try {
TemporalAccessor temporal = DateTimeFormatter.ISO_DATE_TIME.parseBest(endStr, OffsetDateTime::from, LocalDateTime::from);
if (temporal instanceof OffsetDateTime) {
OffsetDateTime odt = (OffsetDateTime) temporal;
return Interval.of(start, odt.toInstant());
} else {
// infer offset from start if not specified by end
LocalDateTime ldt = (LocalDateTime) temporal;
return Interval.of(start, ldt.toInstant(offset));
}
} catch (DateTimeParseException ex) {
Instant end = Instant.parse(endStr);
return Interval.of(start, end);
}
}
//-----------------------------------------------------------------------
/**
* Constructor.
*
* @param startInclusive the start instant, inclusive, validated not null
* @param endExclusive the end instant, exclusive, validated not null
*/
private Interval(Instant startInclusive, Instant endExclusive) {
this.start = startInclusive;
this.end = endExclusive;
}
//-----------------------------------------------------------------------
/**
* Gets the start of this time interval, inclusive.
* <p>
* This will return {@link Instant#MIN} if the range is unbounded at the start.
* In this case, the range includes all dates into the far-past.
*
* @return the start of the time interval
*/
public Instant getStart() {
return start;
}
/**
* Gets the end of this time interval, exclusive.
* <p>
* This will return {@link Instant#MAX} if the range is unbounded at the end.
* In this case, the range includes all dates into the far-future.
*
* @return the end of the time interval, exclusive
*/
public Instant getEnd() {
return end;
}
//-----------------------------------------------------------------------
/**
* Checks if the range is empty.
* <p>
* An empty range occurs when the start date equals the inclusive end date.
*
* @return true if the range is empty
*/
public boolean isEmpty() {
return start.equals(end);
}
/**
* Checks if the start of the interval is unbounded.
*
* @return true if start is unbounded
*/
public boolean isUnboundedStart() {
return start.equals(Instant.MIN);
}
/**
* Checks if the end of the interval is unbounded.
*
* @return true if end is unbounded
*/
public boolean isUnboundedEnd() {
return end.equals(Instant.MAX);
}
//-----------------------------------------------------------------------
/**
* Returns a copy of this range with the specified start instant.
*
* @param start the start instant for the new interval, not null
* @return an interval with the end from this interval and the specified start
* @throws DateTimeException if the resulting interval has end before start
*/
public Interval withStart(Instant start) {
return Interval.of(start, end);
}
/**
* Returns a copy of this range with the specified end instant.
*
* @param end the end instant for the new interval, not null
* @return an interval with the start from this interval and the specified end
* @throws DateTimeException if the resulting interval has end before start
*/
public Interval withEnd(Instant end) {
return Interval.of(start, end);
}
//-----------------------------------------------------------------------
/**
* Checks if this interval contains the specified instant.
* <p>
* This checks if the specified instant is within the bounds of this interval.
* If this range has an unbounded start then {@code contains(Instant#MIN)} returns true.
* If this range has an unbounded end then {@code contains(Instant#MAX)} returns true.
* If this range is empty then this method always returns false.
*
* @param instant the instant, not null
* @return true if this interval contains the instant
*/
public boolean contains(Instant instant) {
Objects.requireNonNull(instant, "instant");
return start.compareTo(instant) <= 0 && (instant.compareTo(end) < 0 || isUnboundedEnd());
}
/**
* Checks if this interval encloses the specified interval.
* <p>
* This checks if the bounds of the specified interval are within the bounds of this interval.
* An empty interval encloses itself.
*
* @param other the other interval, not null
* @return true if this interval contains the other interval
*/
public boolean encloses(Interval other) {
Objects.requireNonNull(other, "other");
return start.compareTo(other.start) <= 0 && other.end.compareTo(end) <= 0;
}
/**
* Checks if this interval abuts the specified interval.
* <p>
* The result is true if the end of this interval is the start of the other, or vice versa.
* An empty interval does not abut itself.
*
* @param other the other interval, not null
* @return true if this interval abuts the other interval
*/
public boolean abuts(Interval other) {
Objects.requireNonNull(other, "other");
return end.equals(other.start) ^ start.equals(other.end);
}
/**
* Checks if this interval is connected to the specified interval.
* <p>
* The result is true if the two intervals have an enclosed interval in common, even if that interval is empty.
* An empty interval is connected to itself.
* <p>
* This is equivalent to {@code (overlaps(other) || abuts(other))}.
*
* @param other the other interval, not null
* @return true if this interval is connected to the other interval
*/
public boolean isConnected(Interval other) {
Objects.requireNonNull(other, "other");
return this.equals(other) || (start.compareTo(other.end) <= 0 && other.start.compareTo(end) <= 0);
}
/**
* Checks if this interval overlaps the specified interval.
* <p>
* The result is true if the two intervals share some part of the time-line.
* An empty interval overlaps itself.
* <p>
* This is equivalent to {@code (isConnected(other) && !abuts(other))}.
*
* @param other the time interval to compare to, null means a zero length interval now
* @return true if the time intervals overlap
*/
public boolean overlaps(Interval other) {
Objects.requireNonNull(other, "other");
return other.equals(this) || (start.compareTo(other.end) < 0 && other.start.compareTo(end) < 0);
}
//-----------------------------------------------------------------------
/**
* Calculates the interval that is the intersection of this interval and the specified interval.
* <p>
* This finds the intersection of two intervals.
* This throws an exception if the two intervals are not {@linkplain #isConnected(Interval) connected}.
*
* @param other the other interval to check for, not null
* @return the interval that is the intersection of the two intervals
* @throws DateTimeException if the intervals do not connect
*/
public Interval intersection(Interval other) {
Objects.requireNonNull(other, "other");
if (isConnected(other) == false) {
throw new DateTimeException("Intervals do not connect: " + this + " and " + other);
}
int cmpStart = start.compareTo(other.start);
int cmpEnd = end.compareTo(other.end);
if (cmpStart >= 0 && cmpEnd <= 0) {
return this;
} else if (cmpStart <= 0 && cmpEnd >= 0) {
return other;
} else {
Instant newStart = (cmpStart >= 0 ? start : other.start);
Instant newEnd = (cmpEnd <= 0 ? end : other.end);
return Interval.of(newStart, newEnd);
}
}
/**
* Calculates the interval that is the union of this interval and the specified interval.
* <p>
* This finds the union of two intervals.
* This throws an exception if the two intervals are not {@linkplain #isConnected(Interval) connected}.
*
* @param other the other interval to check for, not null
* @return the interval that is the union of the two intervals
* @throws DateTimeException if the intervals do not connect
*/
public Interval union(Interval other) {
Objects.requireNonNull(other, "other");
if (isConnected(other) == false) {
throw new DateTimeException("Intervals do not connect: " + this + " and " + other);
}
int cmpStart = start.compareTo(other.start);
int cmpEnd = end.compareTo(other.end);
if (cmpStart >= 0 && cmpEnd <= 0) {
return other;
} else if (cmpStart <= 0 && cmpEnd >= 0) {
return this;
} else {
Instant newStart = (cmpStart >= 0 ? other.start : start);
Instant newEnd = (cmpEnd <= 0 ? other.end : end);
return Interval.of(newStart, newEnd);
}
}
/**
* Calculates the smallest interval that encloses this interval and the specified interval.
* <p>
* The result of this method will {@linkplain #encloses(Interval) enclose}
* this interval and the specified interval.
*
* @param other the other interval to check for, not null
* @return the interval that spans the two intervals
*/
public Interval span(Interval other) {
Objects.requireNonNull(other, "other");
int cmpStart = start.compareTo(other.start);
int cmpEnd = end.compareTo(other.end);
Instant newStart = (cmpStart >= 0 ? other.start : start);
Instant newEnd = (cmpEnd <= 0 ? other.end : end);
return Interval.of(newStart, newEnd);
}
//-------------------------------------------------------------------------
/**
* Checks if this interval is after the specified instant.
* <p>
* The result is true if this instant starts after the specified instant.
* An empty interval behaves as though it is an instant for comparison purposes.
*
* @param instant the other instant to compare to, not null
* @return true if the start of this interval is after the specified instant
*/
public boolean isAfter(Instant instant) {
return start.compareTo(instant) > 0;
}
/**
* Checks if this interval is before the specified instant.
* <p>
* The result is true if this instant ends before the specified instant.
* Since intervals do not include their end points, this will return true if the
* instant equals the end of the interval.
* An empty interval behaves as though it is an instant for comparison purposes.
*
* @param instant the other instant to compare to, not null
* @return true if the start of this interval is before the specified instant
*/
public boolean isBefore(Instant instant) {
return end.compareTo(instant) <= 0 && start.compareTo(instant) < 0;
}
//-------------------------------------------------------------------------
/**
* Checks if this interval is after the specified interval.
* <p>
* The result is true if this instant starts after the end of the specified interval.
* Since intervals do not include their end points, this will return true if the
* instant equals the end of the interval.
* An empty interval behaves as though it is an instant for comparison purposes.
*
* @param interval the other interval to compare to, not null
* @return true if this instant is after the specified instant
*/
public boolean isAfter(Interval interval) {
return start.compareTo(interval.end) >= 0 && !interval.equals(this);
}
/**
* Checks if this interval is before the specified interval.
* <p>
* The result is true if this instant ends before the start of the specified interval.
* Since intervals do not include their end points, this will return true if the
* two intervals abut.
* An empty interval behaves as though it is an instant for comparison purposes.
*
* @param interval the other interval to compare to, not null
* @return true if this instant is before the specified instant
*/
public boolean isBefore(Interval interval) {
return end.compareTo(interval.start) <= 0 && !interval.equals(this);
}
//-----------------------------------------------------------------------
/**
* Obtains the duration of this interval.
* <p>
* An {@code Interval} is associated with two specific instants on the time-line.
* A {@code Duration} is simply an amount of time, separate from the time-line.
*
* @return the duration of the time interval
* @throws ArithmeticException if the calculation exceeds the capacity of {@code Duration}
*/
public Duration toDuration() {
return Duration.between(start, end);
}
//-----------------------------------------------------------------------
/**
* Checks if this interval is equal to another interval.
* <p>
* Compares this {@code Interval} with another ensuring that the two instants are the same.
* Only objects of type {@code Interval} are compared, other types return false.
*
* @param obj the object to check, null returns false
* @return true if this is equal to the other interval
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof Interval) {
Interval other = (Interval) obj;
return start.equals(other.start) && end.equals(other.end);
}
return false;
}
/**
* A hash code for this interval.
*
* @return a suitable hash code
*/
@Override
public int hashCode() {
return start.hashCode() ^ end.hashCode();
}
//-----------------------------------------------------------------------
/**
* Outputs this interval as a {@code String}, such as {@code 2007-12-03T10:15:30/2007-12-04T10:15:30}.
* <p>
* The output will be the ISO-8601 format formed by combining the
* {@code toString()} methods of the two instants, separated by a forward slash.
*
* @return a string representation of this instant, not null
*/
@Override
public String toString() {
return start.toString() + '/' + end.toString();
}
}

View File

@ -0,0 +1,651 @@
/*
* Copyright (c) 2007-present, Stephen Colebourne & Michael Nascimento Santos
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* * Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* * Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* * Neither the name of JSR-310 nor the names of its contributors
* may be used to endorse or promote products derived from this software
* without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
* LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package li.strolch.utils.time;
import static java.time.temporal.ChronoUnit.*;
import java.io.Serializable;
import java.time.*;
import java.time.chrono.ChronoPeriod;
import java.time.chrono.IsoChronology;
import java.time.format.DateTimeParseException;
import java.time.temporal.*;
import java.util.*;
/**
* An amount of time in the ISO-8601 calendar system that combines a period and a duration.
* <p>
* 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.
* <p>
* 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.
* <p>
* The model is of a directed amount, meaning that the amount may be negative.
*
* <h3>Implementation Requirements:</h3>
* This class is immutable and thread-safe.
* <p>
* 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 {
/**
* A constant for a duration of zero.
*/
public static final PeriodDuration ZERO = new PeriodDuration(Period.ZERO, Duration.ZERO);
/**
* A serialization identifier for this class.
*/
private static final long serialVersionUID = 8815521625671589L;
/**
* The supported units.
*/
private static final List<TemporalUnit> SUPPORTED_UNITS =
Collections.unmodifiableList(Arrays.<TemporalUnit>asList(YEARS, MONTHS, DAYS, SECONDS, NANOS));
/**
* The number of seconds per day.
*/
private static final long SECONDS_PER_DAY = 86400;
/**
* The period.
*/
private final Period period;
/**
* The duration.
*/
private final Duration duration;
//-----------------------------------------------------------------------
/**
* Obtains an instance based on a period and duration.
* <p>
* 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.
* <p>
* 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 duration.
* <p>
* 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 temporal amount.
* <p>
* 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}.
* <p>
* 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 from a text string such as {@code PnYnMnDTnHnMnS}.
* <p>
* This will parse the string produced by {@code toString()} which is
* based on the ISO-8601 period formats {@code PnYnMnDTnHnMnS} and {@code PnW}.
* <p>
* 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.
* <p>
* The leading plus/minus sign, and negative values for weeks and days are
* not part of the ISO-8601 standard.
* <p>
* Note that the date style format {@code PYYYY-MM-DDTHH:MM:SS} is not supported.
* <p>
* For example, the following are valid inputs:
* <pre>
* "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))
* </pre>
*
* @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.
* <p>
* The start is included, but the end is not.
* The result of this method can be negative if the end is before the start.
* <p>
* 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);
}
//-----------------------------------------------------------------------
/**
* 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);
}
//-----------------------------------------------------------------------
/**
* Gets the value of the requested unit.
* <p>
* 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.
* <p>
* This returns the list {@link ChronoUnit#YEARS}, {@link ChronoUnit#MONTHS},
* {@link ChronoUnit#DAYS}, {@link ChronoUnit#SECONDS} and {@link ChronoUnit#NANOS}.
* <p>
* 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<TemporalUnit> getUnits() {
return SUPPORTED_UNITS;
}
//-----------------------------------------------------------------------
/**
* Gets the period part.
*
* @return the period part
*/
public Period getPeriod() {
return period;
}
/**
* Returns a copy of this period-duration with a different period.
* <p>
* 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);
}
/**
* Gets the duration part.
*
* @return the duration part
*/
public Duration getDuration() {
return duration;
}
/**
* Returns a copy of this period-duration with a different duration.
* <p>
* 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.
* <p>
* 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();
}
//-----------------------------------------------------------------------
/**
* Returns a copy of this amount with the specified amount added.
* <p>
* The parameter is converted using {@link PeriodDuration#from(TemporalAmount)}.
* The period and duration are combined separately.
* <p>
* 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 copy of this amount with the specified amount subtracted.
* <p>
* The parameter is converted using {@link PeriodDuration#from(TemporalAmount)}.
* The period and duration are combined separately.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* This normalizes the years and months units, leaving the days unit unchanged.
* The result is exact, always representing the same amount of time.
* <p>
* 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".
* <p>
* 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".
* <p>
* Note that no normalization is performed on the days or duration.
* <p>
* 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.
* <p>
* This normalizes the days and duration, leaving the years and months unchanged.
* The result uses a standard day length of 24 hours.
* <p>
* 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".
* <p>
* 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".
* <p>
* Note that no normalization is performed on the years or months.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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}.
* <p>
* 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);
}
}

View File

@ -0,0 +1,76 @@
package li.strolch.utils.time;
import static li.strolch.utils.I18nStrolchUtilsBundle.i18n;
import java.time.Duration;
import java.time.Period;
import java.time.temporal.ChronoUnit;
import java.util.Locale;
public class PeriodDurationFormatter {
public static String formatPeriodDuration(Locale locale, PeriodDuration periodDuration) {
StringBuilder sb = new StringBuilder();
int u;
u = (int) periodDuration.get(ChronoUnit.YEARS);
if (u > 0) {
sb.append(u).append(" ").append(i18n(locale, "years"));
periodDuration = periodDuration.minus(Period.ofYears(u));
if (periodDuration.isZero())
return sb.toString();
sb.append(" ");
}
u = (int) periodDuration.get(ChronoUnit.MONTHS);
if (u > 0) {
sb.append(u).append(" ").append(i18n(locale, "months"));
periodDuration = periodDuration.minus(Period.ofMonths(u));
if (periodDuration.isZero())
return sb.toString();
sb.append(" ");
}
u = (int) periodDuration.get(ChronoUnit.DAYS);
if (u > 0) {
sb.append(u).append(" ").append(i18n(locale, "days"));
periodDuration = periodDuration.minus(Period.ofDays(u));
if (periodDuration.isZero())
return sb.toString();
sb.append(" ");
}
Duration duration = periodDuration.getDuration();
if (duration.isZero())
return sb.toString();
u = (int) duration.toHours();
if (u > 0) {
sb.append(u).append(" ").append(i18n(locale, "hours"));
duration = duration.minusHours(u);
if (duration.isZero())
return sb.toString();
sb.append(" ");
}
u = (int) duration.toMinutes();
if (u > 0) {
sb.append(u).append(" ").append(i18n(locale, "minutes"));
duration = duration.minusMinutes(u);
if (duration.isZero())
return sb.toString();
sb.append(" ");
}
u = (int) duration.toMillis() / 1000;
sb.append(u).append(" ").append(i18n(locale, "seconds"));
return sb.toString();
}
}

View File

@ -0,0 +1,18 @@
day=Day
uom=Unit
Mo=Mo
Mi=Mi
Ev=Ev
Ni=Ni
Tu=Tu
We=We
Th=Th
Fr=Fr
Sa=Sa
Su=Su
days=Days
months=Months
years=Years
minutes=Minutes
hours=Hours
seconds=Seconds

View File

@ -0,0 +1,18 @@
day=Tag
uom=Einheit
Mo=Mo
Mi=Mi
Ev=Ab
Ni=Na
Tu=Di
We=Mi
Th=Do
Fr=Fr
Sa=Sa
Su=So
days=Tage
months=Monate
years=Jahre
minutes=Minuten
hours=Stunden
seconds=Sekunden