net.time4j.Duration Maven / Gradle / Ivy
/*
* -----------------------------------------------------------------------
* Copyright © 2013-2016 Meno Hochschild,
* -----------------------------------------------------------------------
* This file (Duration.java) is part of project Time4J.
*
* Time4J is free software: You can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published
* by the Free Software Foundation, either version 2.1 of the License, or
* (at your option) any later version.
*
* Time4J is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Time4J. If not, see .
* -----------------------------------------------------------------------
*/
package net.time4j;
import net.time4j.base.MathUtils;
import net.time4j.engine.AbstractDuration;
import net.time4j.engine.AbstractMetric;
import net.time4j.engine.ChronoEntity;
import net.time4j.engine.ChronoException;
import net.time4j.engine.ChronoUnit;
import net.time4j.engine.Normalizer;
import net.time4j.engine.TimeMetric;
import net.time4j.engine.TimePoint;
import net.time4j.engine.TimeSpan;
import net.time4j.format.NumberType;
import net.time4j.format.PluralCategory;
import net.time4j.format.PluralRules;
import net.time4j.tz.Timezone;
import java.io.IOException;
import java.io.InvalidObjectException;
import java.io.ObjectInputStream;
import java.io.Serializable;
import java.text.ParseException;
import java.time.temporal.IsoFields;
import java.time.temporal.TemporalAmount;
import java.time.temporal.TemporalUnit;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import static net.time4j.CalendarUnit.CENTURIES;
import static net.time4j.CalendarUnit.DAYS;
import static net.time4j.CalendarUnit.DECADES;
import static net.time4j.CalendarUnit.MILLENNIA;
import static net.time4j.CalendarUnit.MONTHS;
import static net.time4j.CalendarUnit.QUARTERS;
import static net.time4j.CalendarUnit.WEEKS;
import static net.time4j.CalendarUnit.YEARS;
import static net.time4j.ClockUnit.HOURS;
import static net.time4j.ClockUnit.MICROS;
import static net.time4j.ClockUnit.MILLIS;
import static net.time4j.ClockUnit.MINUTES;
import static net.time4j.ClockUnit.NANOS;
import static net.time4j.ClockUnit.SECONDS;
/**
* ISO-8601-compatible duration between two time points.
*
* Instances can be created by following factory methods:
*
*
* - {@link #of(long, IsoUnit) of(long, U)}
* - {@link #ofCalendarUnits(int, int, int)}
* - {@link #ofClockUnits(int, int, int)}
* - {@link #ofPositive()} (builder-pattern)
* - {@link #ofNegative()} (builder-pattern)
* - {@link #parsePeriod(String)}
* - {@link #parseCalendarPeriod(String)}
* - {@link #parseClockPeriod(String)}
* - {@link #parseWeekBasedPeriod(String)}
*
*
* All instances are immutable, but changed copies can be created
* by using the methods {@code plus()}, {@code with()}, {@code union()},
* {@code multipliedBy()}, {@code abs()} and {@code inverse()}. The time
* units {@code ClockUnit.MILLIS} and {@code ClockUnit.MICROS} will
* automatically normalized to nanoseconds. In every other case a normalization
* must be explicitly triggered by {@code with(Normalizer)}.
*
* Note: The definition of an optional negative sign is not part of
* ISO-8061, but part of the XML-schema-specification and defines the
* position of two time points relative to each other. A manipulation of
* the sign is possible with the method {@code inverse()}.
*
* The time arithmetic handles the addition of a duration to a time point
* and the subtraction of a duration from a time point as dependent on the
* sign of the duration as described in the
* standard algorithm
* of the super class.
*
* @param generic type of time units
* @author Meno Hochschild
* @doctags.concurrency {immutable}
*/
/*[deutsch]
* ISO-konforme Zeitspanne zwischen zwei Zeitpunkten.
*
* Instanzen können über folgende Fabrikmethoden erzeugt
* werden:
*
*
* - {@link #of(long, IsoUnit) of(long, U)}
* - {@link #ofCalendarUnits(int, int, int)}
* - {@link #ofClockUnits(int, int, int)}
* - {@link #ofPositive()} (builder-Muster)
* - {@link #ofNegative()} (builder-Muster)
* - {@link #parsePeriod(String)}
* - {@link #parseCalendarPeriod(String)}
* - {@link #parseClockPeriod(String)}
* - {@link #parseWeekBasedPeriod(String)}
*
*
* Alle Instanzen sind immutable, aber geänderte Kopien lassen
* sich über die Methoden {@code plus()}, {@code with()}, {@code union()},
* {@code multipliedBy()}, {@code abs()} und {@code inverse()} erzeugen.
* Hierbei werden die Zeiteinheiten {@code ClockUnit.MILLIS} und
* {@code ClockUnit.MICROS} intern immer zu Nanosekunden normalisiert.
* Ansonsten muß eine Normalisierung explizit mittels
* {@code with(Normalizer)} angestoßen werden.
*
* Notiz: Die Definition eines optionalen negativen Vorzeichens ist streng
* genommen nicht Bestandteil des ISO-Standards, ist aber Bestandteil der
* XML-Schema-Spezifikation und legt die Lage zweier Zeitpunkte relativ
* zueinander fest. Eine Manipulation des Vorzeichens ist mit der Methode
* {@code inverse()} möglich.
*
* Die Zeitarithmetik behandelt die Addition und Subtraktion einer Zeitspanne
* bezogen auf einen Zeitpunkt abhängig vom Vorzeichen der Zeitspanne wie
* im Standardalgorithmus
* von Time4J beschrieben.
*
* @param generic type of time units
* @author Meno Hochschild
* @doctags.concurrency {immutable}
*/
public final class Duration
extends AbstractDuration
implements Serializable {
//~ Statische Felder/Initialisierungen --------------------------------
private static final char ISO_DECIMAL_SEPARATOR = (
Boolean.getBoolean("net.time4j.format.iso.decimal.dot")
? '.'
: ',' // Empfehlung des ISO-Standards
);
private static final Object SIGN_KEY = new Object();
private static final long MRD = 1000000000L;
private static final long MIO = 1000000L;
@SuppressWarnings("rawtypes")
private static final Duration ZERO = new Duration();
private static final Duration.Formatter CF_EXT_CAL =
createAlternativeDateFormat(true, false);
private static final Duration.Formatter CF_EXT_ORD =
createAlternativeDateFormat(true, true);
private static final Duration.Formatter CF_BAS_CAL =
createAlternativeDateFormat(false, false);
private static final Duration.Formatter CF_BAS_ORD =
createAlternativeDateFormat(false, true);
private static final Duration.Formatter TF_EXT =
createAlternativeTimeFormat(true);
private static final Duration.Formatter TF_BAS =
createAlternativeTimeFormat(false);
private static final Comparator- > ITEM_COMPARATOR = StdNormalizer.comparator();
/**
*
Normalizes the duration items on the base of
* {@code 1 year = 12 months} and {@code 1 day = 24 hours} and
* {@code 1 hour = 60 minutes} and {@code 1 minute = 60 seconds} -
* without converting days to months.
*
* Attention: Timezone-dependent changes of length of day or
* leapseconds are ignored. That is why this normalization should
* only be applied on ISO-timestamps without timezone reference.
* Only time units of enum types {@link CalendarUnit} and
* {@link ClockUnit} will be normalized.
*
* Weeks will be normalized to days if weeks do not represent
* the only calendrical duration items.
*
* @see PlainTimestamp#normalize(TimeSpan)
*/
/*[deutsch]
* Normalisiert die Zeitspannenelemente einer Zeitspanne auf der Basis
* {@code 1 Jahr = 12 Monate} und {@code 1 Tag = 24 Stunden} und
* {@code 1 Stunde = 60 Minuten} und {@code 1 Minute = 60 Sekunden},
* jedoch ohne die Tage zu Monaten zu konvertieren.
*
* VORSICHT: Zeitzonenbedingte Veränderungen der Tageslänge
* oder Schaltsekunden werden hier ignoriert. Deshalb sollte diese
* Normalisierung möglichst nur auf ISO-Zeitstempel ohne Zeitzonen-
* oder UTC-Unterstützung angewandt werden. Nur Zeiteinheiten der
* Enums {@link CalendarUnit} und {@link ClockUnit} können normalisiert
* werden.
*
* Wochen werden genau dann zu Tagen konvertiert, wenn sie nicht das
* einzige datumsbezogene Zeitspannenelement darstellen.
*
* @see PlainTimestamp#normalize(TimeSpan)
*/
public static Normalizer STD_PERIOD = StdNormalizer.ofMixedUnits();
/**
* Normalizes the calendrical items of a duration on the base
* {@code 1 year = 12 months} - without converting the days to months.
*
* Weeks will be normalized to days if weeks do not represent
* the only calendrical duration items. Only time units of type
* {@link CalendarUnit} will be normalized.
*
* @see PlainDate#normalize(TimeSpan)
*/
/*[deutsch]
* Normalisiert die Datumselemente einer Zeitspanne auf der Basis
* {@code 1 Jahr = 12 Monate}, jedoch ohne die Tage zu Monaten zu
* konvertieren.
*
* Wochen werden genau dann zu Tagen konvertiert, wenn sie nicht das
* einzige datumsbezogene Zeitspannenelement darstellen. Nur Zeiteinheiten
* des Enums {@link CalendarUnit} werden normalisiert.
*
* @see PlainDate#normalize(TimeSpan)
*/
public static Normalizer STD_CALENDAR_PERIOD = StdNormalizer.ofCalendarUnits();
/**
* Normalizes the wall time items of a duration on the base
* {@code 1 day = 24 hours} und {@code 1 hour = 60 minutes} and
* {@code 1 minute = 60 seconds}.
*
* Attention: Timezone-dependent changes of length of day or
* leapseconds are ignored. That is why this normalization should
* only be applied on ISO-timestamps without timezone reference.
* Only time units of enum type {@link ClockUnit} will be normalized.
*
* @see PlainTime
*/
/*[deutsch]
* Normalisiert die Uhrzeitelemente einer Zeitspanne auf der Basis
* {@code 1 Tag = 24 Stunden} und {@code 1 Stunde = 60 Minuten} und
* {@code 1 Minute = 60 Sekunden}.
*
* VORSICHT: Zeitzonenbedingte Veränderungen der Tageslänge
* oder UTC-Schaltsekunden werden hier ignoriert. Deshalb sollte diese
* Normalisierung nicht auf Zeitzonen- oder UTC-sensible Zeitpunkttypen
* angewandt werden. Nur Zeiteinheiten des Enums {@link ClockUnit}
* werden normalisiert.
*
* @see PlainTime
*/
public static Normalizer STD_CLOCK_PERIOD = StdNormalizer.ofClockUnits();
private static final int PRINT_STYLE_NORMAL = 0;
private static final int PRINT_STYLE_ISO = 1;
private static final int PRINT_STYLE_XML = 2;
private static final long serialVersionUID = -6321211763598951499L;
private static final TimeMetric> YMD_METRIC =
Duration.in(YEARS, MONTHS, DAYS);
private static final TimeMetric> CLOCK_METRIC =
Duration.in(HOURS, MINUTES, SECONDS, NANOS);
private static final TimeMetric> WEEK_BASED_METRIC =
Duration.in(CalendarUnit.weekBasedYears(), WEEKS, DAYS);
private static final int SUPER_TYPE = -1;
private static final int CALENDAR_TYPE = 0;
private static final int CLOCK_TYPE = 1;
private static final int WEEK_BASED_TYPE = 2;
//~ Instanzvariablen --------------------------------------------------
private transient final List- > items;
private transient final boolean negative;
//~ Konstruktoren -----------------------------------------------------
/**
* Standard-Konstruktor.
*
* @param items list of duration items
* @param negative negative duration indicated?
* @throws IllegalArgumentException if different units of same length exist
*/
Duration(
List
- > items,
boolean negative
) {
super();
boolean empty = items.isEmpty();
if (empty) {
this.items = Collections.emptyList();
} else {
Collections.sort(items, ITEM_COMPARATOR);
this.items = Collections.unmodifiableList(items);
}
this.negative = (!empty && negative);
}
// Kopiekonstruktor (siehe inverse())
private Duration(
Duration duration,
boolean inverse
) {
super();
this.items = duration.items;
this.negative = (inverse ? !duration.negative : duration.negative);
}
// leere Zeitspanne
private Duration() {
super();
this.items = Collections.emptyList();
this.negative = false;
}
//~ Methoden ----------------------------------------------------------
/**
*
Gets an empty duration without units.
*
* This method can also be used for generic upcasting of the
* type U to {@code IsoUnit}:
*
*
* Duration<CalendarUnit> dur = Duration.ofCalendarUnits(2, 5, 30);
* Duration<IsoUnit> upcasted = Duration.ofZero().plus(dur);
*
*
* @param generic unit type
* @return empty duration
*/
/*[deutsch]
* Liefert eine leere Zeitspanne ohne Einheiten.
*
* Diese Methode kann auch dafür benutzt werden, den generischen
* Typparameter U zu {@code IsoUnit} zu ändern:
*
*
* Duration<CalendarUnit> dur = Duration.ofCalendarUnits(2, 5, 30);
* Duration<IsoUnit> upcasted = Duration.ofZero().plus(dur);
*
*
* @param generic unit type
* @return empty duration
*/
@SuppressWarnings("unchecked")
public static Duration ofZero() {
return ZERO;
}
/**
* Creates a new duration which only knows one unit.
*
* Is the given amount is negative then the duration will be
* negative, too. Is the amount equal to {@code 0} then an empty
* duration will be returned. Milliseconds or microseconds will
* be automatically normalized to nanoseconds.
*
* @param generic unit type
* @param amount amount as count of units
* @param unit single time unit
* @return new duration
*/
/*[deutsch]
* Erzeugt eine neue Zeitspanne, die auf nur einer Zeiteinheit
* beruht.
*
* Ist der angegebene Betrag negativ, so wird auch die Zeitspanne
* negativ sein. Ist er {@code 0}, wird eine leere Zeitspanne
* zurückgegeben. Millisekunden oder Mikrosekunden werden
* automatisch zu Nanosekunden normalisiert.
*
* @param generic unit type
* @param amount amount as count of units
* @param unit single time unit
* @return new duration
*/
public static Duration of(
long amount,
U unit
) {
if (amount == 0) {
return ofZero();
}
U u = unit;
long value = amount;
if (amount < 0) {
value = MathUtils.safeNegate(amount);
}
if (unit instanceof ClockUnit) {
switch (unit.getSymbol()) {
case '3':
u = cast(ClockUnit.NANOS);
value = MathUtils.safeMultiply(value, MIO);
break;
case '6':
u = cast(ClockUnit.NANOS);
value = MathUtils.safeMultiply(value, 1000);
break;
default:
// no-op
}
}
List- > items = new ArrayList<>(1);
items.add(Item.of(value, u));
return new Duration<>(items, (amount < 0));
}
/**
*
Konstructs a new positive duration for combined date- and time items
* by applying the builder pattern.
*
* @return help object for building a positive {@code Duration}
*/
/*[deutsch]
* Konstruiert über den Umweg des builder-Entwurfsmusters
* eine neue ISO-konforme positive Zeitspanne für kombinierte Datums- und
* Uhrzeiteinheiten.
*
* @return help object for building a positive {@code Duration}
*/
public static Builder ofPositive() {
return new Builder(false);
}
/**
* Konstructs a new negative duration for combined date- and time items
* by applying the builder pattern.
*
* @return help object for building a negative {@code Duration}
*/
/*[deutsch]
* Konstruiert über den Umweg des builder-Entwurfsmusters
* eine neue ISO-konforme negative Zeitspanne für kombinierte Datums- und
* Uhrzeiteinheiten.
*
* @return help object for building a negative {@code Duration}
*/
public static Builder ofNegative() {
return new Builder(true);
}
/**
* Creates a positive duration in years, months and days.
*
* All arguments must not be negative. Is any argument equal to
* {@code 0} then it will be ignored. If a negative duration is needed
* then an application can simply call {@code inverse()} on the result.
*
* @param years amount in years
* @param months amount in months
* @param days amount in days
* @return new duration
* @throws IllegalArgumentException if any argument is negative
* @see #inverse()
*/
/*[deutsch]
* Erzeugt eine positive Zeitspanne in Jahren, Monaten und Tagen.
*
* Alle Argumente dürfen nicht negativ sein. Ist ein Argument
* gleich {@code 0}, wird es ignoriert. Wird eine negative Zeitspanne
* gewünscht, kann auf dem Ergebnis einfach {@code inverse()}
* aufgerufen werden.
*
* @param years amount in years
* @param months amount in months
* @param days amount in days
* @return new duration
* @throws IllegalArgumentException if any argument is negative
* @see #inverse()
*/
public static Duration ofCalendarUnits(
int years,
int months,
int days
) {
return Duration.ofCalendarUnits(years, months, days, false);
}
/**
* Creates a positive duratioon in hours, minutes and seconds.
*
* All arguments must not be negative. Is any argument equal to
* {@code 0} then it will be ignored. If a negative duration is needed
* then an application can simply call {@code inverse()} on the result.
*
* @param hours amount in hours
* @param minutes amount in minutes
* @param seconds amount in seconds
* @return new duration
* @throws IllegalArgumentException if any argument is negative
* @see #inverse()
*/
/*[deutsch]
* Erzeugt eine positive Zeitspanne in Stunden, Minuten und
* Sekunden.
*
* Alle Argumente dürfen nicht negativ sein. Ist ein Argument
* gleich {@code 0}, wird es ignoriert. Wird eine negative Zeitspanne
* gewünscht, kann auf dem Ergebnis einfach {@code inverse()}
* aufgerufen werden.
*
* @param hours amount in hours
* @param minutes amount in minutes
* @param seconds amount in seconds
* @return new duration
* @throws IllegalArgumentException if any argument is negative
* @see #inverse()
*/
public static Duration ofClockUnits(
int hours,
int minutes,
int seconds
) {
return Duration.ofClockUnits(hours, minutes, seconds, 0, false);
}
/**
* Tries to convert given temporal amount to a duration.
*
* Note that the arithmetic of given amount is probably different from that of the result. Every unit
* compatible with {@code java.time.temporal.ChronoUnit} with exception of {@code ERAS} and {@code FOREVER}
* can be processed. Also the units {@code IsoFields.QUARTER_YEARS} and {@code IsoFields.WEEK_BASED_YEARS}
* are accepted.
*
* @param threeten temporal amount
* @return normalized duration
* @throws IllegalArgumentException if the argument contains a non-mappable temporal unit or has mixed signs
* @see #from(java.time.Duration)
* @see #from(java.time.Period)
* @see #STD_PERIOD
* @since 4.0
*/
/*[deutsch]
* Versucht, den angegebenen temporalen Betrag zu einer Dauer zu konvertieren.
*
* Zu beachten: Der Algorithmus des Arguments ist wahrscheinlich anders als der der erzeugten
* Dauer. Jede Zeiteinheit kompatibel zu {@code java.time.temporal.ChronoUnit} mit Ausnahme von
* {@code ERAS} und {@code FOREVER} kann verarbeitet werden. Auch die Einheiten {@code IsoFields.QUARTER_YEARS}
* und {@code IsoFields.WEEK_BASED_YEARS} werden erkannt.
*
* @param threeten temporal amount
* @return normalized duration
* @throws IllegalArgumentException if the argument contains a non-mappable temporal unit or has mixed signs
* @see #from(java.time.Duration)
* @see #from(java.time.Period)
* @see #STD_PERIOD
* @since 4.0
*/
public static Duration from(TemporalAmount threeten) {
Duration result = Duration.ofZero();
for (TemporalUnit tu : threeten.getUnits()) {
long amount = threeten.get(tu);
if (amount == 0) {
continue;
}
IsoUnit unit;
if (tu instanceof java.time.temporal.ChronoUnit) {
switch (java.time.temporal.ChronoUnit.class.cast(tu)) {
case NANOS:
unit = ClockUnit.NANOS;
break;
case MICROS:
unit = ClockUnit.MICROS;
break;
case MILLIS:
unit = ClockUnit.MILLIS;
break;
case SECONDS:
unit = ClockUnit.SECONDS;
break;
case MINUTES:
unit = ClockUnit.MINUTES;
break;
case HOURS:
unit = ClockUnit.HOURS;
break;
case HALF_DAYS:
unit = ClockUnit.HOURS;
amount = Math.multiplyExact(amount, 12);
break;
case DAYS:
unit = CalendarUnit.DAYS;
break;
case WEEKS:
unit = CalendarUnit.WEEKS;
break;
case MONTHS:
unit = CalendarUnit.MONTHS;
break;
case YEARS:
unit = CalendarUnit.YEARS;
break;
case DECADES:
unit = CalendarUnit.DECADES;
break;
case CENTURIES:
unit = CalendarUnit.CENTURIES;
break;
case MILLENNIA:
unit = CalendarUnit.MILLENNIA;
break;
default:
throw new IllegalArgumentException("Not supported: " + tu);
}
} else if (tu.equals(IsoFields.QUARTER_YEARS)) {
unit = CalendarUnit.QUARTERS;
} else if (tu.equals(IsoFields.WEEK_BASED_YEARS)) {
unit = CalendarUnit.weekBasedYears();
} else {
throw new IllegalArgumentException("Not supported: " + tu);
}
result = result.plus(amount, unit);
}
return result.with(STD_PERIOD);
}
/**
* Short cut for {@code TemporalType.THREETEN_DURATION.translate(threetenDuration)}.
*
* @param threetenDuration Threeten-equivalent of a clock duration
* @return normalized clock duration
* @since 4.0
* @see TemporalType#THREETEN_DURATION
* @see #STD_CLOCK_PERIOD
*/
/*[deutsch]
* Abkürzung für {@code TemporalType.THREETEN_DURATION.translate(threetenDuration)}.
*
* @param threetenDuration Threeten-equivalent of a clock duration
* @return normalized clock duration
* @since 4.0
* @see TemporalType#THREETEN_DURATION
* @see #STD_CLOCK_PERIOD
*/
public static Duration from(java.time.Duration threetenDuration) {
return TemporalType.THREETEN_DURATION.translate(threetenDuration);
}
/**
* Short cut for {@code TemporalType.THREETEN_PERIOD.translate(threetenPeriod)}.
*
* Note that the arithmetic of given period is slightly different from that of the result when
* it comes to handling negative durations:
*
*
* System.out.println(
* LocalDate.of(2015, 7, 1).minus(Period.of(0, 1, 1))); // 2015-05-31
* System.out.println(
* PlainDate.of(2015, 7, 1).minus(Duration.ofCalendarUnits(0, 1, 1))); // 2015-05-30
*
*
* @param threetenPeriod Threeten-equivalent of a calendar duration
* @return normalized calendar duration
* @throws ChronoException if conversion fails due to mixed signs
* @since 4.0
* @see TemporalType#THREETEN_PERIOD
* @see #STD_CALENDAR_PERIOD
*/
/*[deutsch]
* Abkürzung für {@code TemporalType.THREETEN_PERIOD.translate(threetenPeriod)}.
*
* Zu beachten: Der Algorithmus des Arguments ist etwas anders als der der erzeugten
* Dauer, wenn es um eine negative Dauer geht.
*
*
* System.out.println(
* LocalDate.of(2015, 7, 1).minus(Period.of(0, 1, 1))); // 2015-05-31
* System.out.println(
* PlainDate.of(2015, 7, 1).minus(Duration.ofCalendarUnits(0, 1, 1))); // 2015-05-30
*
*
* @param threetenPeriod Threeten-equivalent of a calendar duration
* @return normalized calendar duration
* @throws ChronoException if conversion fails due to mixed signs
* @since 4.0
* @see TemporalType#THREETEN_PERIOD
* @see #STD_CALENDAR_PERIOD
*/
public static Duration from(java.time.Period threetenPeriod) {
return TemporalType.THREETEN_PERIOD.translate(threetenPeriod);
}
/**
* Constructs a metric for any kind of standard units in
* normalized form.
*
* Important: If the smallest unit is missing which
* fits the precision of timepoints to be compared then a remainder of
* subtraction will exist. The result of distance calculation will not
* express the full temporal distance in this case. For the completeness
* of calculation, the day unit is required if the distance between
* two dates is needed.
*
* Example with different unit types: If this method
* is called with different unit types then it is strongly recommended to
* first assign the units to static constants of type {@code IsoUnit} in
* order to avoid compiler problems with generics. This practice also
* helps to improve the readability of code.
*
*
* private static final IsoUnit DAYS = CalendarUnit.DAYS;
* private static final IsoUnit HOURS = ClockUnit.HOURS;
* private static final IsoUnit MINUTES = ClockUnit.MINUTES;
*
* PlainTimestamp start = PlainTimestamp.of(2014, 3, 28, 0, 30);
* PlainTimestamp end = PlainTimestamp.of(2014, 4, 5, 14, 15);
* Duration<IsoUnit> duration =
* Duration.in(DAYS, HOURS, MINUTES).between(start, end);
* System.out.println(duration); // output: P8DT13H45M
*
*
* @param generic unit type
* @param units time units to be used in calculation
* @return immutable metric for calculating a duration in given units
* @throws IllegalArgumentException if no time unit is given or
* if there are unit duplicates
*/
/*[deutsch]
* Konstruiert eine Metrik für beliebige Standard-Zeiteinheiten
* in normalisierter Form.
*
* Wichtig: Fehlt die der Präzision der zu
* vergleichenden Zeitpunkte entsprechende kleinste Zeiteinheit, wird
* im allgemeinen ein Subtraktionsrest übrigbleiben. Das Ergebnis
* der Metrikberechnung wird dann nicht den vollständigen zeitlichen
* Abstand zwischen den Zeitpunkten ausdrücken. Für die
* Vollständigkeit der Berechnung ist bei Datumsangaben mindestens
* die explizite Angabe der Tageseinheit notwendig.
*
* Beispiel mit verschiedenen Einheitstypen: Wenn diese
* Methode mit verschiedenen Zeiteinheitstypen aufgerufen wird, dann wird
* dringend empfohlen, zuerst die Einheiten statischen Konstanten vom Typ
* {@code IsoUnit} zuzuweisen, um Compiler-Probleme mit Generics zu
* vermeiden. Diese Praxis hilft auch, die Lesbarkeit des Code zu
* verbessern.
*
*
* private static final IsoUnit DAYS = CalendarUnit.DAYS;
* private static final IsoUnit HOURS = ClockUnit.HOURS;
* private static final IsoUnit MINUTES = ClockUnit.MINUTES;
*
* PlainTimestamp start = PlainTimestamp.of(2014, 3, 28, 0, 30);
* PlainTimestamp end = PlainTimestamp.of(2014, 4, 5, 14, 15);
* Duration<IsoUnit> duration =
* Duration.in(DAYS, HOURS, MINUTES).between(start, end);
* System.out.println(duration); // output: P8DT13H45M
*
*
* @param generic unit type
* @param units time units to be used in calculation
* @return immutable metric for calculating a duration in given units
* @throws IllegalArgumentException if no time unit is given or
* if there are unit duplicates
*/
@SafeVarargs
public static TimeMetric> in(U... units) {
return new Metric<>(units);
}
/**
* Constructs a metric in years, months and days.
*
* Finally the resulting duration will be normalized such that
* smaller units will be converted to bigger units if possible.
*
* @return immutable metric for calculating a duration in years,
* months and days
* @see #in(IsoUnit[]) in(U[])
* @see CalendarUnit#YEARS
* @see CalendarUnit#MONTHS
* @see CalendarUnit#DAYS
*/
/*[deutsch]
* Konstruiert eine Metrik in Jahren, Monaten und Tagen.
*
* Am Ende wird die Darstellung automatisch normalisiert, also kleine
* Zeiteinheiten so weit wie möglich in große Einheiten
* umgerechnet.
*
* @return immutable metric for calculating a duration in years,
* months and days
* @see #in(IsoUnit[]) in(U[])
* @see CalendarUnit#YEARS
* @see CalendarUnit#MONTHS
* @see CalendarUnit#DAYS
*/
public static TimeMetric> inYearsMonthsDays() {
return YMD_METRIC;
}
/**
* Constructs a metric in hours, minutes, seconds and nanoseconds.
*
* Finally the resulting duration will be normalized such that
* smaller units will be converted to bigger units if possible.
*
* @return immutable metric for calculating a duration in clock units
* @see #in(IsoUnit[]) in(U[])
* @see ClockUnit#HOURS
* @see ClockUnit#MINUTES
* @see ClockUnit#SECONDS
* @see ClockUnit#NANOS
*/
/*[deutsch]
* Konstruiert eine Metrik in Stunden, Minuten, Sekunden und Nanos.
*
* Am Ende wird die Darstellung automatisch normalisiert, also kleine
* Zeiteinheiten so weit wie möglich in große Einheiten
* umgerechnet.
*
* @return immutable metric for calculating a duration in clock units
* @see #in(IsoUnit[]) in(U[])
* @see ClockUnit#HOURS
* @see ClockUnit#MINUTES
* @see ClockUnit#SECONDS
* @see ClockUnit#NANOS
*/
public static TimeMetric> inClockUnits() {
return CLOCK_METRIC;
}
/**
* Constructs a metric in week-based years, weeks and days.
*
* Finally the resulting duration will be normalized such that
* smaller units will be converted to bigger units if possible.
*
* @return immutable metric for calculating a duration in week-based years, weeks and days
* @see #in(IsoUnit[]) in(U[])
* @see CalendarUnit#weekBasedYears()
* @see CalendarUnit#WEEKS
* @see CalendarUnit#DAYS
* @since 3.21/4.17
*/
/*[deutsch]
* Konstruiert eine Metrik in wochen-basierten Jahren, Wochen und Tagen.
*
* Am Ende wird die Darstellung automatisch normalisiert, also kleine
* Zeiteinheiten so weit wie möglich in große Einheiten umgerechnet.
*
* @return immutable metric for calculating a duration in week-based years, weeks and days
* @see #in(IsoUnit[]) in(U[])
* @see CalendarUnit#weekBasedYears()
* @see CalendarUnit#WEEKS
* @see CalendarUnit#DAYS
* @since 3.21/4.17
*/
public static TimeMetric> inWeekBasedUnits() {
return WEEK_BASED_METRIC;
}
/**
* Helps to evaluate the zonal duration between two timestamps
* and applies an offset correction if necessary.
*
* Following example handles the change from winter time to summer time
* in Germany causing 4 instead of 5 hours difference:
*
*
* PlainTimestamp start = PlainTimestamp.of(2014, 3, 30, 0, 0);
* PlainTimestamp end = PlainTimestamp.of(2014, 3, 30, 5, 0);
* IsoUnit hours = ClockUnit.HOURS;
* System.out.println(
* Duration.in(Timezone.of("Europe/Berlin"), hours)
* .between(start, end).toString()); // output: PT4H
*
*
* @param tz timezone
* @param units time units to be used in calculation
* @return zonal metric for calculating a duration in given units
* @throws IllegalArgumentException if no time unit is given or
* if there are unit duplicates
* @since 1.2
*/
/*[deutsch]
* Hilfsmethode zur Bestimmung der lokalen beziehungsweise zonalen
* Dauer zwischen zwei Zeitstempeln, die bei Bedarf eine Offset-Korrektur
* anwendet.
*
* Das folgende Beispiel behandelt den Wechsel von der Winterzeit zur
* Sommerzeit in Deutschland, so daß nur 4 statt 5 Stunden Differenz
* errechnet werden:
*
*
* PlainTimestamp start = PlainTimestamp.of(2014, 3, 30, 0, 0);
* PlainTimestamp end = PlainTimestamp.of(2014, 3, 30, 5, 0);
* IsoUnit hours = ClockUnit.HOURS;
* System.out.println(
* Duration.in(Timezone.of("Europe/Berlin"), hours)
* .between(start, end).toString()); // Ausgabe: PT4H
*
*
* @param tz timezone
* @param units time units to be used in calculation
* @return zonal metric for calculating a duration in given units
* @throws IllegalArgumentException if no time unit is given or
* if there are unit duplicates
* @since 1.2
*/
public static TimeMetric> in(
Timezone tz,
IsoUnit... units
) {
return new ZonalMetric(tz, units);
}
@Override
public List- > getTotalLength() {
return this.items;
}
@Override
public boolean isNegative() {
return this.negative;
}
/**
*
Queries if this duration contains given time unit.
*
* Any time unit is also part of this duration if it is a fractional
* part of a second (digit symbol) which is to be converted first.
*
* @param unit time unit to be checked (optional)
* @return {@code true} if this duration contains given unit
* else {@code false}
* @see #getPartialAmount(IsoUnit) getPartialAmount(U)
*/
/*[deutsch]
* Ist die angegebene Zeiteinheit in dieser Zeitspanne enthalten?
*
* Eine Zeiteinheit ist auch dann enthalten, wenn sie als
* Sekundenbruchteil (Ziffer in Symboldarstellung) erst konvertiert
* werden muß.
*
* @param unit time unit to be checked (optional)
* @return {@code true} if this duration contains given unit
* else {@code false}
* @see #getPartialAmount(IsoUnit) getPartialAmount(U)
*/
@Override
public boolean contains(IsoUnit unit) {
if (unit == null) {
return false;
}
boolean fractional = isFractionUnit(unit);
for (int i = 0, n = this.items.size(); i < n; i++) {
Item item = this.items.get(i);
U u = item.getUnit();
if (
u.equals(unit)
|| (fractional && isFractionUnit(u))
) {
return (item.getAmount() > 0);
}
}
return false;
}
/**
* Gets the partial amount associated with given time unit.
*
* If this duration does not contain given time unit then this method
* will yield the value {@code 0}. Fractional parts of seconds which
* are known by their numerical symbols will automatically be converted.
* That means if a duration stores nanoseconds, but is queried for
* microseconds then the nanosecond amount will be multiplied by factor
* {@code 1000} and finally returned.
*
* @param unit time unit the amount is queried for (optional)
* @return non-negative amount associated with given unit ({@code >= 0})
*/
/*[deutsch]
* Liefert den Betrag zu einer Zeiteinheit.
*
* Wenn die angegebene Zeiteinheit nicht in der Zeitspanne enthalten ist,
* liefert die Methode den Wert {@code 0}. Sekundenbruchteile, die an der
* Symboldarstellung ihrer Einheiten erkennbar sind, werden automatisch
* konvertiert. Konkret: Wenn eine Zeitspanne z.B. Nanosekunden speichert,
* aber nach Mikrosekunden gefragt wird, dann wird der in der Zeitspanne
* enthaltene Nanosekundenwert mit dem Faktor {@code 1000} multipliziert
* und zurückgegeben.
*
* @param unit time unit the amount is queried for (optional)
* @return non-negative amount associated with given unit ({@code >= 0})
*/
@Override
public long getPartialAmount(IsoUnit unit) {
if (unit == null) {
return 0;
}
boolean fractional = isFractionUnit(unit);
for (int i = 0, n = this.items.size(); i < n; i++) {
Item item = this.items.get(i);
U u = item.getUnit();
if (u.equals(unit)) {
return item.getAmount();
} else if (
fractional
&& isFractionUnit(u)
) {
int d1 = u.getSymbol() - '0';
int d2 = unit.getSymbol() - '0';
int factor = 1;
for (int j = 0, m = Math.abs(d1 - d2); j < m; j++) {
factor *= 10;
}
if (d1 >= d2) {
return item.getAmount() / factor;
} else {
return item.getAmount() * factor;
}
}
}
return 0;
}
/**
* Creates a {@code Comparator} which compares durations based on
* their lengths.
*
* Internally, the comparing algorithm uses the expression
* {@code base.plus(duration1).compareTo(base.plus(duration2))}.
* The given basis time point is necessary because some durations
* with flexible units like months have else no fixed length.
*
* @param generic unit type
* @param generic type of time point
* @param base base time point which durations will use for comparison
* @return {@code Comparator} for plain durations
* @see TimePoint#compareTo(TimePoint) TimePoint.compareTo(T)
*/
/*[deutsch]
* Liefert ein Hilfsobjekt zum Vergleichen von Zeitspannenobjekten
* auf Basis ihrer Länge.
*
* Erzeugt einen {@code Comparator}, der letztlich auf dem Ausdruck
* {@code base.plus(duration1).compareTo(base.plus(duration2))} beruht.
* Der Basiszeitpunkt ist notwendig, weil sonst Zeitspannenobjekte dieser
* Klasse nicht notwendig eine physikalisch feste Länge haben.
* Zum Beispiel sind Monate variable Zeiteinheiten mit unterschiedlich
* vielen Tagen.
*
* @param generic unit type
* @param generic type of time point
* @param base base time point which durations will use for comparison
* @return {@code Comparator} for plain durations
* @see TimePoint#compareTo(TimePoint) TimePoint.compareTo(T)
*/
public static >
Comparator> comparator(T base) {
return new LengthComparator<>(base);
}
/**
* Gets a copy of this duration where given amount will be added
* to the partial amount of this duration in given unit.
*
* The method also takes in account the sign of the duration.
* Example:
*
*
* System.out.println(Duration.of(5, MONTHS).plus(-6, MONTHS));
* // output: -P1M
*
*
* Notes: Is the amount to be added equal to {@code 0} then the method
* will simply yield this duration. Mixed signs are not permitted and will
* be rejected by an exception. For example following expression is not
* allowed:
*
*
* Duration.of(-1, MONTHS).plus(30, DAYS); // throws IllegalStateException
*
*
* @param amount temporal amount to be added (maybe negative)
* @param unit associated time unit
* @return new changed duration while this duration remains unaffected
* @throws IllegalStateException if the result gets mixed signs by adding the partial amounts
* @throws IllegalArgumentException if different units of same length exist
* @throws ArithmeticException in case of long overflow
* @see #with(long, IsoUnit) with(long, U)
*/
/*[deutsch]
* Liefert eine Kopie dieser Instanz, in der der angegebene Betrag zum
* mit der angegebenen Zeiteinheit assoziierten Feldwert addiert wird.
*
* Die Methode berücksichtigt auch das Vorzeichen der Zeitspanne.
* Beispiel:
*
*
* System.out.println(Duration.of(5, MONTHS).plus(-6, MONTHS));
* // output: -P1M
*
*
* Notiz: Ist der zu addierende Betrag gleich {@code 0}, liefert die
* Methode einfach diese Instanz selbst zurück. Gemischte Vorzeichen
* im Ergebnis sind nicht zulässig und werden mit einem Abbruch
* quittiert:
*
*
* Duration.of(-1, MONTHS).plus(30, DAYS); // throws IllegalStateException
*
*
* @param amount temporal amount to be added (maybe negative)
* @param unit associated time unit
* @return new changed duration while this duration remains unaffected
* @throws IllegalStateException if the result gets mixed signs by adding the partial amounts
* @throws IllegalArgumentException if different units of same length exist
* @throws ArithmeticException in case of long overflow
* @see #with(long, IsoUnit) with(long, U)
*/
public Duration plus(
long amount,
U unit
) {
if (unit == null) {
throw new NullPointerException("Missing chronological unit.");
}
long originalAmount = amount;
U originalUnit = unit;
boolean negatedValue = false;
if (amount == 0) {
return this;
} else if (amount < 0) {
amount = MathUtils.safeNegate(amount);
negatedValue = true;
}
// Millis und Micros ersetzen
List- > temp = new ArrayList<>(this.getTotalLength());
Item item = replaceFraction(amount, unit);
if (item != null) {
amount = item.getAmount();
unit = item.getUnit();
}
if (this.isEmpty()) {
temp.add((item == null) ? Item.of(amount, unit) : item);
return new Duration<>(temp, negatedValue);
}
// Items aktualisieren
int index = this.getIndex(unit);
boolean resultNegative = this.isNegative();
if (index < 0) { // Einheit nicht vorhanden
if (this.isNegative() == negatedValue) {
temp.add(Item.of(amount, unit));
} else { // mixed signs possible => last try
return this.plus(Duration.of(originalAmount, originalUnit));
}
} else {
long sum =
MathUtils.safeAdd(
MathUtils.safeMultiply(
temp.get(index).getAmount(),
(this.isNegative() ? -1 : 1)
),
MathUtils.safeMultiply(
amount,
(negatedValue ? -1 : 1)
)
);
if (sum == 0) {
temp.remove(index);
} else if (
(this.count() == 1)
|| (this.isNegative() == (sum < 0))
) {
long absSum = ((sum < 0) ? MathUtils.safeNegate(sum) : sum);
temp.set(index, Item.of(absSum, unit));
resultNegative = (sum < 0);
} else { // mixed signs possible => last try
return this.plus(Duration.of(originalAmount, originalUnit));
}
}
return new Duration<>(temp, resultNegative);
}
/**
*
Creates a duration as union of this instance and given timespan
* where partial amounts of equal units will be summed up.
*
* In order to sum up timespans with different unit types, following
* trick can be applied:
*
*
* Duration<IsoUnit> zero = Duration.ofZero();
* Duration<IsoUnit> result = zero.plus(this).plus(timespan);
*
*
* Note about sign handling: If this duration and
* given timespan have different signs then Time4J tries to apply a
* normalization in the hope the mixed signs disappear. Otherwise
* this method will throw an exception in case of mixed signs for
* different duration items. So it is strongly recommended only to
* merge durations with equal signs.
*
* @param timespan other time span this duration will be merged
* with by adding the partial amounts
* @return new merged duration
* @throws IllegalStateException if the result gets mixed signs by
* adding the partial amounts
* @throws IllegalArgumentException if different units of same length exist
* @throws ArithmeticException in case of long overflow
* @see #union(TimeSpan)
*/
/*[deutsch]
* Erzeugt eine neue Zeitspanne als Vereinigung dieser und der
* angegebenen Zeitspanne, wobei Beträge zu gleichen Zeiteinheiten
* addiert werden.
*
* Um Zeitspannen mit verschiedenen Einheitstypen zu vereinigen, kann
* folgender Kniff angewandt werden:
*
*
* Duration<IsoUnit> zero = Duration.ofZero();
* Duration<IsoUnit> result = zero.plus(this).plus(timespan);
*
*
* Hinweis zur Vorzeichenbehandlung: Wenn diese Dauer
* und die angegebene Zeitspanne verschiedene Vorzeichen haben, wird
* Time4J bei Bedarf eine automatische Normalisierung durchführen.
* Sind dann immer noch gemischte Vorzeichen für einzelne
* Dauerelemente vorhanden, wird eine Ausnahme geworfen. Es wird
* deshalb empfohlen, nur Zeitspannen mit gleichen
* Vorzeichen zusammenzuführen.
*
* @param timespan other time span this duration will be merged
* with by adding the partial amounts
* @return new merged duration
* @throws IllegalStateException if the result gets mixed signs by
* adding the partial amounts
* @throws IllegalArgumentException if different units of same length exist
* @throws ArithmeticException in case of long overflow
* @see #union(TimeSpan)
*/
@SuppressWarnings("unchecked")
public Duration plus(TimeSpan extends U> timespan) {
Duration result = merge(this, timespan);
if (result == null) {
long[] sums = new long[4];
sums[0] = 0;
sums[1] = 0;
sums[2] = 0;
sums[3] = 0;
if (summarize(this, sums) && summarize(timespan, sums)) {
long months = sums[0];
long days = sums[1];
long secs = sums[2];
long nanos = sums[3];
long daytime;
if (nanos != 0) {
daytime = nanos;
} else if (secs != 0) {
daytime = secs;
} else {
daytime = days;
}
if (!hasMixedSigns(months, daytime)) {
boolean neg = ((months < 0) || (daytime < 0));
if (neg) {
months = MathUtils.safeNegate(months);
days = MathUtils.safeNegate(days);
secs = MathUtils.safeNegate(secs);
nanos = MathUtils.safeNegate(nanos);
}
long years = months / 12;
months = months % 12;
long nanosecs = 0;
if (nanos != 0) {
nanosecs = nanos % MRD;
secs = nanos / MRD;
}
long hours = secs / 3600;
secs = secs % 3600;
long minutes = secs / 60;
secs = secs % 60;
Map map = new HashMap<>();
map.put(YEARS, years);
map.put(MONTHS, months);
map.put(DAYS, days);
map.put(HOURS, hours);
map.put(MINUTES, minutes);
map.put(SECONDS, secs);
map.put(NANOS, nanosecs);
return (Duration) Duration.create(map, neg);
}
}
throw new IllegalStateException(
"Mixed signs in result time span not allowed: "
+ this
+ " PLUS "
+ timespan);
}
return result;
}
/**
* Gets a copy of this duration where the partial amount associated
* with given time unit is changed.
*
* Equivalent to {@code plus(amount - getAmount(unit), unit)}.
*
* @param amount temporal amount to be set (maybe negative)
* @param unit associated time unit
* @return new changed duration while this duration remains unaffected
* @throws IllegalStateException if the result gets mixed signs by
* setting the partial amounts
* @throws IllegalArgumentException if different units of same length exist
* @throws ArithmeticException in case of long overflow
* @see #plus(long, IsoUnit) plus(long, U)
*/
/*[deutsch]
* Liefert eine Kopie dieser Instanz mit dem angegebenen geänderten
* Wert.
*
* Entspricht {@code plus(amount - getAmount(unit), unit)}.
*
* @param amount temporal amount to be set (maybe negative)
* @param unit associated time unit
* @return new changed duration while this duration remains unaffected
* @throws IllegalStateException if the result gets mixed signs by
* setting the partial amounts
* @throws IllegalArgumentException if different units of same length exist
* @throws ArithmeticException in case of long overflow
* @see #plus(long, IsoUnit) plus(long, U)
*/
public Duration with(
long amount,
U unit
) {
long absAmount =
((amount < 0) ? MathUtils.safeNegate(amount) : amount);
Item item = replaceFraction(absAmount, unit);
if (item != null) {
absAmount = item.getAmount();
unit = item.getUnit();
}
long oldAmount = this.getPartialAmount(unit);
return this.plus(
MathUtils.safeSubtract(
MathUtils.safeMultiply(
absAmount,
(amount < 0) ? -1 : 1
),
MathUtils.safeMultiply(
oldAmount,
this.isNegative() ? -1 : 1
)
),
unit
);
}
/**
* Gets the absolute always non-negative copy of this duration.
*
* Example:
*
*
* System.out.println(Duration.of(-5, MONTHS).abs());
* // output: P5M
*
*
* @return new positive duration if this duration is negative else this
* duration unchanged
* @see #isNegative()
* @see #inverse()
*/
/*[deutsch]
* Liefert die absolute immer positive Variante dieser Zeitspanne.
*
* Beispiel:
*
*
* System.out.println(Duration.of(-5, MONTHS).abs());
* // output: P5M
*
*
* @return new positive duration if this duration is negative else this
* duration unchanged
* @see #isNegative()
* @see #inverse()
*/
public Duration abs() {
if (this.isNegative()) {
return this.inverse();
} else {
return this;
}
}
/**
* Gets a copy of this duration with reversed sign.
*
* A double call of this method will yield an equal duration so
* following invariant holds:
*
*
* System.out.println(this.inverse().inverse().equals(this));
* // output: true
*
*
* For the special case of an empty duration, this method has no
* effect and just returns the same instance. The method is equivalent
* to the expression {@code multipliedBy(-1)}.
*
* Example:
*
*
* System.out.println(Duration.of(-5, MONTHS).inverse());
* // output: P5M
*
*
* @return new negative duration if this duration is positive else a new
* positive duration with the same partial amounts and units
* @see #isNegative()
* @see #multipliedBy(int)
*/
/*[deutsch]
* Liefert eine Kopie dieser Instanz, die das negative Äquivalent
* darstellt.
*
* Ein zweifacher Aufruf dieser Methode liefert wieder eine
* inhaltlich gleiche Instanz. Also gilt immer folgende Beziehung:
* {@code this.inverse().inverse().equals(this) == true}. Liegt der
* Sonderfall einer leeren Zeitspanne vor, dann ist diese Methode ohne
* Wirkung und liefert nur die gleiche Instanz zurück. Entspricht
* dem Ausdruck {@code multipliedBy(-1)}.
*
* Beispiel: {@code [-P5M].inverse()} wird zu {@code [P5M]}.
*
* @return new negative duration if this duration is positive else a new
* positive duration with the same partial amounts and units
* @see #isNegative()
* @see #multipliedBy(int)
*/
@Override
public Duration inverse() {
return this.multipliedBy(-1);
}
/**
* Multiplies all partial amounts of this duration by given factor.
*
* Is the factor equal to {@code 0} then the new duration is empty.
* If the factor {@code 1} is specified then the method will just yield
* this instance unaffected. In the case of a negative factor the sign
* will also be inverted.
*
* @param factor multiplication factor
* @return new duration with all amounts multiplied while this duration
* remains unaffected
* @throws ArithmeticException in case of long overflow
*/
/*[deutsch]
* Multipliziert alle enthaltenen Beträge mit dem angegebenen
* Faktor.
*
* Ist der Faktor {@code 0}, ist die neue Zeitspanne leer. Mit dem
* Faktor {@code 1} wird diese Instanz selbst unverändert
* zurückgegeben. Bei einem negativen Faktor wird zusätzlich
* das Vorzeichen geändert.
*
* @param factor multiplication factor
* @return new duration with all amounts multiplied while this duration
* remains unaffected
* @throws ArithmeticException in case of long overflow
*/
public Duration multipliedBy(int factor) {
if (
this.isEmpty()
|| (factor == 1)
) {
return this;
} else if (factor == 0) {
return ofZero();
} else if (factor == -1) {
return new Duration<>(this, true);
}
List- > newItems = new ArrayList<>(this.count());
int scalar = Math.abs(factor);
for (int i = 0, n = this.count(); i < n; i++) {
Item item = this.getTotalLength().get(i);
newItems.add(
Item.of(
MathUtils.safeMultiply(item.getAmount(), scalar),
item.getUnit()
)
);
}
return new Duration<>(
newItems,
((factor < 0) ? !this.isNegative() : this.isNegative())
);
}
/**
*
Creates a duration as union of this instance and given timespan
* where partial amounts of equal units will be summed up.
*
* The list result of this method can be used in time arithmetic as
* follows:
*
*
* Duration<CalendarUnit> dateDur =
* Duration.ofCalendarUnits(2, 7, 10);
* Duration<ClockUnit> timeDur =
* Duration.ofClockUnits(0, 30, 0);
* PlainTimestamp tsp = PlainTimestamp.of(2014, 1, 1, 0, 0);
*
* for (Duration<?> dur : Duration.ofZero().plus(dateDur).union(timeDur)) {
* tsp = tsp.plus(dur);
* }
*
* System.out.println(tsp); // 2016-08-11T00:30
*
*
* Note that this example will even work in case of mixed signs. No
* exception will be thrown. Instead this duration and the other one would
* just be added to the timestamp within a loop - step by step. In contrast
* to {@code plus(TimeSpan)}, Time4J does not try to normalize the durations
* in order to produce a unique sign (on best effort base) in case of
* mixed signs.
*
* @param timespan other time span this duration is to be merged with
* @return unmodifiable list with one new merged duration or two unmerged
* durations in case of mixed signs
* @throws IllegalArgumentException if different units of same length exist
* @throws ArithmeticException in case of long overflow
* @see #plus(TimeSpan)
*/
/*[deutsch]
* Erzeugt eine neue Zeitspanne als Vereinigung dieser und der
* angegebenen Zeitspanne, wobei Beträge zu gleichen Zeiteinheiten
* addiert werden.
*
* Das Listenergebnis dieser Methode kann in der Zeitarithmetik wie folgt
* genutzt werden:
*
*
* Duration<CalendarUnit> dateDur =
* Duration.ofCalendarUnits(2, 7, 10);
* Duration<ClockUnit> timeDur =
* Duration.ofClockUnits(0, 30, 0);
* PlainTimestamp tsp = PlainTimestamp.of(2014, 1, 1, 0, 0);
*
* for (Duration<?> dur : Duration.ofZero().plus(dateDur).union(timeDur)) {
* tsp = tsp.plus(dur);
* }
*
* System.out.println(tsp); // 2016-08-11T00:30
*
*
* Zu beachten: Dieses Beispiel funktioniert sogar, wenn beide
* Dauer-Objekte wegen gemischter Vorzeichen nicht zusammengeführt
* werden können. Stattdessen werden dann diese Dauer und die
* angegebene Zeitspanne Schritt für Schritt innerhalb der Schleife
* zum Zeitstempel aufaddiert. Anders als in {@code plus(TimeSpan)}
* versucht Time4J hier nicht, im Fall gemischter Vorzeichen mit Hilfe
* einer Normalisierung ein eindeutiges Vorzeichen herzustellen.
*
* @param timespan other time span this duration is to be merged with
* @return unmodifiable list with one new merged duration or two unmerged
* durations in case of mixed signs
* @throws IllegalArgumentException if different units of same length exist
* @throws ArithmeticException in case of long overflow
* @see #plus(TimeSpan)
*/
public List> union(TimeSpan extends U> timespan) {
Duration merged = merge(this, timespan);
if (merged == null) {
List> result = new ArrayList<>();
result.add(this);
Duration empty = ofZero();
Duration other = empty.plus(timespan);
result.add(other);
return Collections.unmodifiableList(result);
}
return Collections.singletonList(merged);
}
/**
* Creates a composition of a calendar period and a clock period.
*
* @param calendarPeriod calendrical duration
* @param clockPeriod duration with clock units
* @return composite duration
* @since 3.0
* @see #toCalendarPeriod()
* @see #toClockPeriod()
*/
/*[deutsch]
* Bildet eine aus kalendarischer Dauer und Uhrzeitperiode zusammengesetzte Dauer.
*
* @param calendarPeriod calendrical duration
* @param clockPeriod duration with clock units
* @return composite duration
* @since 3.0
* @see #toCalendarPeriod()
* @see #toClockPeriod()
*/
public static Duration compose(
Duration calendarPeriod,
Duration clockPeriod
) {
Duration dur = Duration.ofZero();
return dur.plus(calendarPeriod).plus(clockPeriod);
}
/**
* Extracts a new duration with all contained calendar units only.
*
* The clock time part will be removed.
*
* @return new duration with calendar units only
* @since 3.0
* @see #compose(Duration, Duration)
* @see #toClockPeriod()
*/
/*[deutsch]
* Extrahiert eine neue Dauer, die nur alle kalendarischen Zeiteinheiten
* dieser Dauer enthält.
*
* Der Uhrzeitanteil wird entfernt.
*
* @return new duration with calendar units only
* @since 3.0
* @see #compose(Duration, Duration)
* @see #toClockPeriod()
*/
public Duration toCalendarPeriod() {
if (this.isEmpty()) {
return Duration.ofZero();
}
List- > calItems = new ArrayList<>();
for (Item item : this.items) {
if (item.getUnit() instanceof CalendarUnit) {
calItems.add(Item.of(item.getAmount(), CalendarUnit.class.cast(item.getUnit())));
}
}
if (calItems.isEmpty()) {
return Duration.ofZero();
}
return new Duration<>(calItems, this.isNegative());
}
/**
*
Extracts a new duration with all contained clock units only.
*
* The calendrical part will be removed.
*
* @return new duration with clock units only
* @since 3.0
* @see #compose(Duration, Duration)
* @see #toCalendarPeriod()
*/
/*[deutsch]
* Extrahiert eine neue Dauer, die nur alle Uhrzeiteinheiten
* dieser Dauer enthält.
*
* Der kalendarische Teil wird entfernt.
*
* @return new duration with clock units only
* @since 3.0
* @see #compose(Duration, Duration)
* @see #toCalendarPeriod()
*/
public Duration toClockPeriod() {
if (this.isEmpty()) {
return Duration.ofZero();
}
List- > clockItems = new ArrayList<>();
for (Item item : this.items) {
if (item.getUnit() instanceof ClockUnit) {
clockItems.add(Item.of(item.getAmount(), ClockUnit.class.cast(item.getUnit())));
}
}
if (clockItems.isEmpty()) {
return Duration.ofZero();
}
return new Duration<>(clockItems, this.isNegative());
}
/**
*
Converts this duration to a general temporal amount compatible with JSR-310-spec.
*
* The conversion is possible if and only if following units are part of this duration:
*
*
* - CalendarUnit.MILLENNIA
* - CalendarUnit.CENTURIES
* - CalendarUnit.DECADES
* - CalendarUnit.YEARS
* - CalendarUnit.QUARTERS
* - CalendarUnit.MONTHS
* - CalendarUnit.WEEKS
* - CalendarUnit.DAYS
* - ClockUnit.HOURS
* - ClockUnit.MINUTES
* - ClockUnit.SECONDS
* - ClockUnit.MILLIS
* - ClockUnit.MICROS
* - ClockUnit.NANOS
* - CalendarUnit.weekBasedYears()
*
*
* The resulting temporal amount can only be applied on the local types of JSR-310. The general
* mapping using this method looks like:
*
*
* - {@code Duration
} can be applied on {@code LocalDateTime}
* - {@code Duration
} can be applied on {@code LocalDate}
* - {@code Duration
} can be applied on {@code LocalTime}
*
*
* Other temporal types of JSR-310 are not supported. Note also that using this method cannot be type-safe
* due to the design of JSR-310 and includes some performance penalty because of the costs of conversion.
* Users should rather use Time4J-types for achieving best results.
*
* @return temporal amount applicable on the types {@code LocalDateTime}, {@code LocalDate} or {@code LocalTime}
* @throws UnsupportedOperationException if this duration contains any unit not listed above
* @since 4.17
*/
/*[deutsch]
* Konvertiert diese Dauer zu einem allgemeinen {@code TemporalAmount} kompatibel mit der Spezifikation
* des JSR-310.
*
* Die Konversion ist genau dann möglich, wenn diese Dauer nur Einheiten wie folgt besitzt:
*
*
* - CalendarUnit.MILLENNIA
* - CalendarUnit.CENTURIES
* - CalendarUnit.DECADES
* - CalendarUnit.YEARS
* - CalendarUnit.QUARTERS
* - CalendarUnit.MONTHS
* - CalendarUnit.WEEKS
* - CalendarUnit.DAYS
* - ClockUnit.HOURS
* - ClockUnit.MINUTES
* - ClockUnit.SECONDS
* - ClockUnit.MILLIS
* - ClockUnit.MICROS
* - ClockUnit.NANOS
* - CalendarUnit.weekBasedYears()
*
*
* Der resultierende {@code TemporalAmount} kann nur auf die lokalen Typen des JSR-310
* angewandt werden. Das allgemeine Schema unter Verwendung dieser Methode sieht so aus:
*
*
* - {@code Duration
} anwendbar auf {@code LocalDateTime}
* - {@code Duration
} anwendbar auf {@code LocalDate}
* - {@code Duration
} anwendbar auf {@code LocalTime}
*
*
* Andere temporale Typen des JSR-310 werden nicht unterstützt. Zu beachten: Diese Methode ist weder
* typsicher (dem Design des JSR-310 geschuldet) noch besonders schnell, weil die Konversion mit einem
* gewissen Aufwand verbunden ist. Für beste Ergebnisse ist eher die durchgehende Verwendung von
* Time4J-Typen empfohlen.
*
* @return temporal amount applicable on the types {@code LocalDateTime}, {@code LocalDate} or {@code LocalTime}
* @throws UnsupportedOperationException if this duration contains any unit not listed above
* @since 4.17
*/
public TemporalAmount toTemporalAmount() {
return new JSR310DurationAdapter(this);
}
/**
* Normalizes this duration by given normalizer.
*
* @param normalizer help object for normalizing this duration
* @return new normalized duration while this duration remains unaffected
* @see #STD_PERIOD
* @see #STD_CALENDAR_PERIOD
* @see #STD_CLOCK_PERIOD
* @see #approximateHours(int)
* @see #approximateMinutes(int)
* @see #approximateSeconds(int)
* @see ClockUnit#only()
*/
/*[deutsch]
* Normalisiert diese Zeitspanne über den angegebenen
* Mechanismus.
*
* @param normalizer help object for normalizing this duration
* @return new normalized duration while this duration remains unaffected
* @see #STD_PERIOD
* @see #STD_CALENDAR_PERIOD
* @see #STD_CLOCK_PERIOD
* @see #approximateHours(int)
* @see #approximateMinutes(int)
* @see #approximateSeconds(int)
* @see ClockUnit#only()
*/
public Duration with(Normalizer normalizer) {
return convert(normalizer.normalize(this));
}
/**
* Yields an approximate normalizer in steps of hours which
* finally uses years, months, days and rounded hours.
*
* The rounding algorithm consists of a combination of integer division and
* multiplication using the given step width. Example for suppressing hours (and
* all smaller units) by mean of an extra big step width of 24 hours (= 1 day):
*
*
* Duration<IsoUnit> dur =
* Duration.ofPositive().years(2).months(13).days(35)
* .minutes(132).build()
* .with(Duration.approximateHours(24));
* System.out.println(dur); // output: P3Y2M4D
*
*
* Another example for rounded hours using static imports:
*
*
* System.out.println(Duration.<IsoUnit>of(7, HOURS).with(approximateHours(3)));
* // output: PT6H
*
*
* @param steps rounding step width
* @return new normalizer for fuzzy and approximate durations
* @throws IllegalArgumentException if the argument is not positive
* @since 2.0
*/
/*[deutsch]
* Liefert einen Normalisierer, der eine Dauer in Stundenschritten auf
* Näherungsbasis mit Jahren, Monaten, Tagen und gerundeten Stunden
* erstellt.
*
* Der Rundungsalgorithmus wendet zuerst die Integerdivision und dann eine
* Multiplikation unter Benutzung des angegebenen Schrittfaktors an. Beispiel
* für das Unterdrücken von Stunden mittels einer extra großen
* Schrittweite von 24 Stunden (= 1 Tag):
*
*
* Duration<IsoUnit> dur =
* Duration.ofPositive().years(2).months(13).days(35)
* .minutes(132).build()
* .with(Duration.approximateHours(24));
* System.out.println(dur); // Ausgabe: P3Y2M4D
*
*
* Ein anderes Beispiel für gerundete Stunden mit Hilfe von static imports:
*
*
* System.out.println(Duration.<IsoUnit>of(7, HOURS).with(approximateHours(3)));
* // Ausgabe: PT6H
*
*
* @param steps rounding step width
* @return new normalizer for fuzzy and approximate durations
* @throws IllegalArgumentException if the argument is not positive
* @since 2.0
*/
public static Normalizer approximateHours(int steps) {
return new ApproximateNormalizer(steps, HOURS);
}
/**
* Yields an approximate normalizer in steps of minutes which
* finally uses years, months, days, hours and rounded minutes.
*
* @param steps rounding step width
* @return new normalizer for fuzzy and approximate durations
* @throws IllegalArgumentException if the argument is not positive
* @see #approximateHours(int)
* @since 2.0
*/
/*[deutsch]
* Liefert einen Normalisierer, der eine Dauer in Minutenschritten auf
* Näherungsbasis mit Jahren, Monaten, Tagen, Stunden und gerundeten
* Minuten erstellt.
*
* @param steps rounding step width
* @return new normalizer for fuzzy and approximate durations
* @throws IllegalArgumentException if the argument is not positive
* @see #approximateHours(int)
* @since 2.0
*/
public static Normalizer approximateMinutes(int steps) {
return new ApproximateNormalizer(steps, MINUTES);
}
/**
* Yields an approximate normalizer in steps of seconds which finally
* uses years, months, days, hours, minutes and rounded seconds.
*
* @param steps rounding step width
* @return new normalizer for fuzzy and approximate durations
* @throws IllegalArgumentException if the argument is not positive
* @see #approximateHours(int)
* @since 2.0
*/
/*[deutsch]
* Liefert einen Normalisierer, der eine Dauer in Sekundenschritten auf
* Näherungsbasis mit Jahren, Monaten, Tagen, Stunden, Minuten und
* gerundeten Sekunden erstellt.
*
* @param steps rounding step width
* @return new normalizer for fuzzy and approximate durations
* @throws IllegalArgumentException if the argument is not positive
* @see #approximateHours(int)
* @since 2.0
*/
public static Normalizer approximateSeconds(int steps) {
return new ApproximateNormalizer(steps, SECONDS);
}
/**
* Creates a normalizer which yields an approximate duration based on the maximum unit
* of the original duration (but not smaller than seconds).
*
* @return new normalizer for fuzzy and approximate durations of only one unit
* (either years, months, days, hours, minutes or seconds)
* @see #approximateHours(int)
* @see #approximateMinutes(int)
* @see #approximateSeconds(int)
* @since 3.14/4.11
*/
/*[deutsch]
* Liefert einen Normalisierer, der eine genäherte Dauer basierend auf der
* größten Zeiteinheit der ursprünglichen Dauer (aber nicht kleiner
* als Sekunden) erstellt.
*
* @return new normalizer for fuzzy and approximate durations of only one unit
* (either years, months, days, hours, minutes or seconds)
* @see #approximateHours(int)
* @see #approximateMinutes(int)
* @see #approximateSeconds(int)
* @since 3.14/4.11
*/
public static Normalizer approximateMaxUnitOnly() {
return new ApproximateNormalizer(false);
}
/**
* Like {@code approximateMaxUnitOnly()} but can create week-based durations
* if the count of days is bigger than {@code 6}.
*
* @return new normalizer for fuzzy and approximate durations of only one unit
* (either years, months, weeks/days, hours, minutes or seconds)
* @see #approximateMaxUnitOnly()
* @since 3.14/4.11
*/
/*[deutsch]
* Wie {@code approximateMaxUnitOnly()}, kann aber eine wochenbasierte Dauer erzeugen,
* wenn die Anzahl der Tage größer als {@code 6} Tage ist.
*
* @return new normalizer for fuzzy and approximate durations of only one unit
* (either years, months, weeks/days, hours, minutes or seconds)
* @see #approximateMaxUnitOnly()
* @since 3.14/4.11
*/
public static Normalizer approximateMaxUnitOrWeeks() {
return new ApproximateNormalizer(true);
}
/**
* Based on all stored duration items and the sign.
*
* @return {@code true} if {@code obj} is also a {@code Duration},
* has the same units and amounts, the same sign and the same
* calendrical status else {@code false}
* @see #getTotalLength()
* @see #isNegative()
*/
/*[deutsch]
* Basiert auf allen gespeicherten Zeitspannenelementen und dem
* Vorzeichen.
*
* @return {@code true} if {@code obj} is also a {@code Duration},
* has the same units and amounts, the same sign and the same
* calendrical status else {@code false}
* @see #getTotalLength()
* @see #isNegative()
*/
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
} else if (obj instanceof Duration) {
Duration> that = Duration.class.cast(obj);
return (
(this.negative == that.negative)
&& this.getTotalLength().equals(that.getTotalLength())
);
} else {
return false;
}
}
/**
* Computes the hash code.
*/
/*[deutsch]
* Basiert auf allen gespeicherten Zeitspannenelementen und dem
* Vorzeichen passend zur Definition von {@code equals()}.
*/
@Override
public int hashCode() {
int hash = this.getTotalLength().hashCode();
if (this.negative) {
hash ^= hash;
}
return hash;
}
/**
* Gets a canonical representation which optionally starts with a
* negative sign then continues with the letter "P", followed
* by a sequence of alphanumerical chars similar to the definition given
* in ISO-8601.
*
* Example: In ISO-8601 a duration of one month, three days and four
* hours is described as "P1M3DT4H". The special char
* "T" separates date and time part. Units are normally
* printed using their symbols, as second alternative using the output
* of their {@code toString()}-method within curly brackets.
*
* Is the duration negative then the representation will have a
* preceding minus sign as specified by XML-schema (for example
* "-P2D") while an empty duration will always have the
* format "PT0S" (second as universal unit). If the second
* part is also fractional then this method will use the comma as
* decimal separator char as recommended by ISO-8601.
*
* Note: The latter ISO-recommendation to use the comma as decimal
* separator char can be overriden by setting the system property
* "net.time4j.format.iso.decimal.dot" to "true"
* so that the english variation of a dot will be choosen instead.
*
* @see #toStringISO()
* @see #toStringXML()
* @see #parsePeriod(String)
*/
/*[deutsch]
* Liefert eine kanonische Darstellung, die optional mit einem negativen
* Vorzeichen beginnt, dann mit dem Buchstaben "P" fortsetzt,
* gefolgt von einer Reihe von alphanumerischen Zeichen analog zur
* ISO8601-Definition.
*
* Beispiel: Im ISO8601-Format ist eine Zeitspanne von 1 Monat, 3 Tagen
* und 4 Stunden als "P1M3DT4H" beschrieben, wobei der Buchstabe
* "T" Datums- und Uhrzeitteil trennt. Einheiten werden in der
* Regel als Symbole ausgegeben, andernfalls wird die Ausgabe ihrer
* {@code toString()}-Methode in geschweiften Klammern benutzt.
*
* Ist die Zeitspanne negativ, so wird in Übereinstimmung mit der
* XML-Schema-Norm ein Minuszeichen vorangestellt (z.B. "-P2D"),
* während eine leere Zeitspanne das Format "PT0S"
* (Sekunde als universelles Zeitmaß) hat. Hat der Sekundenteil einen
* Bruchteil, wird als Dezimaltrennzeichen das Komma entsprechend der
* Empfehlung des ISO-Standards gewählt.
*
* Hinweis: Die ISO-Empfehlung, ein Komma als Dezimaltrennzeichen zu
* verwenden, kann mit Hilfe der bool'schen System-Property
* "net.time4j.format.iso.decimal.dot" so geändert
* werden, daß die angelsächsiche Variante mit Punkt statt
* Komma verwendet wird.
*
* @see #toStringISO()
* @see #toStringXML()
* @see #parsePeriod(String)
*/
@Override
public String toString() {
return this.toString(PRINT_STYLE_NORMAL);
}
/**
* Gets a canonical representation which starts with the letter
* "P", followed by a sequence of alphanumerical chars as
* defined in ISO-8601.
*
* A negative sign is not defined in ISO-8601 and will be rejected
* by this method with an exception. An empty duration will always have
* the format "PT0S" (second as universal unit). If the second
* part is also fractional then this method will use the comma as
* decimal separator char as recommended by ISO-8601.
*
* Note: The latter ISO-recommendation to use the comma as decimal
* separator char can be overriden by setting the system property
* "net.time4j.format.iso.decimal.dot" to "true"
* so that the english variation of a dot will be choosen instead.
* Furthermore, weeks are normalized to days if there are other
* calendrical units like years or months.
*
* Only units of types {@code CalendarUnit} or {@code ClockUnit} can
* be printed.
*
* @return String
* @throws ChronoException if this duration is negative or if any special
* units shall be output, but units of type {@code CalendarUnit}
* will be translated to iso-compatible units if necessary
* @see #parsePeriod(String)
* @see IsoUnit#getSymbol()
*/
/*[deutsch]
* Liefert eine ISO-konforme Darstellung, die mit dem Buchstaben
* "P" beginnt, gefolgt von einer Reihe von alphanumerischen
* Zeichen analog zur ISO8601-Definition.
*
* Ein negatives Vorzeichen ist im ISO-8601-Standard nicht vorgesehen.
* In diesem Fall wirft die Methode eine Ausnahme. Eine leere Zeitspanne
* hat das Format "PT0S". Hat der Sekundenteil einen
* Bruchteil, wird als Dezimaltrennzeichen das Komma entsprechend der
* Empfehlung des ISO-Standards gewählt.
*
* Hinweis: Die ISO-Empfehlung, ein Komma als Dezimaltrennzeichen zu
* verwenden, kann mit Hilfe der bool'schen System-Property
* "net.time4j.format.iso.decimal.dot" so geändert
* werden, daß die angelsächsiche Variante mit Punkt statt
* Komma verwendet wird. Es gilt auch, daß ein vorhandenes Wochenfeld
* zu Tagen auf der Basis (1 Woche = 7 Tage) normalisiert wird, wenn
* zugleich auch andere Kalendereinheiten vorhanden sind.
*
* Nur Einheiten der Typen {@code CalendarUnit} oder {@code ClockUnit
* können angezeigt werden.
*
* @return String
* @throws ChronoException if this duration is negative or if any special
* units shall be output, but units of type {@code CalendarUnit}
* will be translated to iso-compatible units if necessary
* @see #parsePeriod(String)
* @see IsoUnit#getSymbol()
*/
public String toStringISO() {
return this.toString(PRINT_STYLE_ISO);
}
/**
* Gets a canonical representation conforming to XML-schema which
* optionally starts with a negative sign then continues with the letter
* "P", followed by a sequence of alphanumerical chars similar
* to the definition given in ISO-8601.
*
* Is the duration negative then the representation will have a
* preceding minus sign as specified by XML-schema (for example
* "-P2D") while an empty duration will always have the
* format "PT0S" (second as universal unit). If the second
* part is also fractional then this method will use the dot as
* decimal separator char (deviating specification in XML-schema).
* Weeks will always be normalized to days.
*
* Only units of types {@code CalendarUnit} or {@code ClockUnit} can
* be printed.
*
* @return String
* @throws ChronoException if any special units shall be
* output, but units of type {@code CalendarUnit} will be
* translated to xml-compatible units if necessary
* @see #parsePeriod(String)
* @see IsoUnit#getSymbol()
*/
/*[deutsch]
* Liefert eine XML-konforme Darstellung, die optional mit einem
* negativen Vorzeichen beginnt, dann mit dem Buchstaben "P"
* fortsetzt, gefolgt von einer Reihe von alphanumerischen Zeichen analog
* zur ISO8601-Definition.
*
* Ist die Zeitspanne negativ, so wird in Übereinstimmung mit der
* XML-Schema-Norm ein Minuszeichen vorangestellt (z.B. "-P2D"),
* während eine leere Zeitspanne das Format "PT0S"
* (Sekunde als universelles Zeitmaß) hat. Hat der Sekundenteil einen
* Bruchteil, wird als Dezimaltrennzeichen der Punkt anders als in der
* Empfehlung des ISO-Standards gewählt. Es gilt auch, daß
* ein vorhandenes Wochenfeld zu Tagen auf der Basis (1 Woche = 7 Tage)
* normalisiert wird.
*
* Nur Einheiten der Typen {@code CalendarUnit} oder {@code ClockUnit
* können angezeigt werden.
*
* @return String
* @throws ChronoException if any special units shall be
* output, but units of type {@code CalendarUnit} will be
* translated to xml-compatible units if necessary
* @see #parsePeriod(String)
* @see IsoUnit#getSymbol()
*/
public String toStringXML() {
return this.toString(PRINT_STYLE_XML);
}
/**
* Parses a canonical representation to a duration.
*
* Canonical representations which start with the literal P are
* also called "period" in Time4J (P-string). This format
* is strongly recommended for storage in databases or XML. Syntax
* in a notation similar to regular expressions:
*
*
* sign := [-]?
* amount := [0-9]+
* fraction := [,\.]{amount}
* years-months-days := ({amount}Y)?({amount}M)?({amount}D)?
* weeks := ({amount}W)?
* date := {years-months-days} | {weeks}
* time := ({amount}H)?({amount}M)?({amount}{fraction}?S)?
* period := {sign}P{date}(T{time})? | PT{time}
*
*
* The units MILLENNIA, CENTURIES, DECADES and QUARTERS defined in
* {@link CalendarUnit} are supported but not special units like
* {@code CalendarUnit.weekBasedYears()}.
*
* Furthermore there is the constraint that the symbols P and T
* must be followed by at least one duration item of amount and unit.
* All items with zero amount will be ignored however. The only item
* which is allowed to have a fractional part is SECONDS and can contain
* a comma as well as a dot as decimal separator. In ISO-8601 the comma
* is the preferred char, in XML-schema only the dot is allowed. If this
* parser is used in context of XML-schema (type xs:duration) it must
* be stated that week items are missing in contrast to ISO-8601. The
* method {@code toStringXML()} takes into account these characteristics
* of XML-schema (leaving aside the fact that XML-schema is potentially
* designed for unlimited big amounts but Time4J can define durations
* only in long range with nanosecond precision at best).
*
* Note: The alternative ISO-formats PYYYY-MM-DDThh:mm:ss and
* PYYYY-DDDThh:mm:ss and their basic variants are supported since
* version v2.0.
*
* Examples for supported formats:
*
*
* date := -P7Y4M3D (negative: 7 years, 4 months, 3 days)
* time := PT3H2M1,4S (positive: 3 hours, 2 minutes, 1400 milliseconds)
* date-time := P1Y1M5DT15H59M10.400S (dot as decimal separator)
* alternative := P0000-02-15T17:45
*
*
* @param period duration in canonical, ISO-8601-compatible or
* XML-schema-compatible format (P-string)
* @return parsed duration in all possible standard units of date and time
* @throws ParseException if parsing fails
* @since 1.2.1
* @see #parseCalendarPeriod(String)
* @see #parseClockPeriod(String)
* @see #toString()
* @see #toStringISO()
* @see #toStringXML()
*/
/*[deutsch]
* Parst eine kanonische Darstellung zu einer Dauer.
*
* Kanonische Darstellungen, die mit dem Literal P beginnen, werden
* in Time4J auch als "period" bezeichnet (P-string). Dieses
* Format ist erste Wahl, wenn es um das Speichern einer Dauer in
* Datenbanken oder XML geht. Syntax in RegExp-ähnlicher Notation:
*
*
* sign := [-]?
* amount := [0-9]+
* fraction := [,\.]{amount}
* years-months-days := ({amount}Y)?({amount}M)?({amount}D)?
* weeks := ({amount}W)?
* date := {years-months-days} | {weeks}
* time := ({amount}H)?({amount}M)?({amount}{fraction}?S)?
* period := {sign}P{date}(T{time})? | PT{time}
*
*
* Die in {@link CalendarUnit} definierten Zeiteinheiten MILLENNIA,
* CENTURIES, DECADES und QUARTERS werden mitsamt ihren Symbolen ebenfalls
* unterstützt, nicht aber spezielle Zeiteinheiten wie zum Beispiel
* {@code CalendarUnit.weekBasedYears()}.
*
* Weiterhin gilt die Einschränkung, daß die Symbole P und T
* mindestens ein Zeitfeld nach sich ziehen müssen. Alle Felder mit
* {@code 0}-Beträgen werden beim Parsen ignoriert. Das einzig erlaubte
* Dezimalfeld der Sekunden kann sowohl einen Punkt wie auch ein Komma
* als Dezimaltrennzeichen haben. Im ISO-Standard ist das Komma das
* bevorzugte Zeichen, in XML-Schema nur der Punkt zulässig. Speziell
* für die Verwendung in XML-Schema (Typ xs:duration) ist zu beachten,
* daß Wochenfelder anders als im ISO-Standard nicht vorkommen. Die
* Methode {@code toStringXML()} berücksichtigt diese Besonderheiten
* von XML-Schema (abgesehen davon, daß XML-Schema potentiell
* unbegrenzt große Zahlen zuläßt, aber Time4J eine
* Zeitspanne nur im long-Bereich mit maximal Nanosekunden-Genauigkeit
* definiert).
*
* Hinweis: Die alternativen ISO-Formate PYYYY-MM-DDThh:mm:ss und
* PYYYY-DDDThh:mm:ss und ihre Basisvarianten werden seit Version v2.0
* ebenfalls unterstützt.
*
* Beispiele für unterstützte Formate:
*
*
* date := -P7Y4M3D (negativ: 7 Jahre, 4 Monate, 3 Tage)
* time := PT3H2M1,4S (positiv: 3 Stunden, 2 Minuten, 1400 Millisekunden)
* date-time := P1Y1M5DT15H59M10.400S (Punkt als Dezimaltrennzeichen)
* alternative := P0000-02-15T17:45
*
*
* @param period duration in canonical, ISO-8601-compatible or
* XML-schema-compatible format (P-string)
* @return parsed duration in all possible standard units of date and time
* @throws ParseException if parsing fails
* @since 1.2.1
* @see #parseCalendarPeriod(String)
* @see #parseClockPeriod(String)
* @see #toString()
* @see #toStringISO()
* @see #toStringXML()
*/
public static Duration parsePeriod(String period)
throws ParseException {
return parsePeriod(period, IsoUnit.class);
}
/**
* Parses a canonical representation with only date units to a
* calendrical duration.
*
* @param period duration in canonical, ISO-8601-compatible or
* XML-schema-compatible format (P-string)
* @return parsed calendrical duration
* @throws ParseException if parsing fails
* @see #parsePeriod(String)
* @see #parseClockPeriod(String)
*/
/*[deutsch]
* Parst eine kanonische Darstellung nur mit
* Datumskomponenten zu einer Dauer.
*
* @param period duration in canonical, ISO-8601-compatible or
* XML-schema-compatible format (P-string)
* @return parsed calendrical duration
* @throws ParseException if parsing fails
* @see #parsePeriod(String)
* @see #parseClockPeriod(String)
*/
public static Duration parseCalendarPeriod(String period)
throws ParseException {
return parsePeriod(period, CalendarUnit.class);
}
/**
* Parses a canonical representation with only wall time units to a
* time-only duration.
*
* @param period duration in canonical, ISO-8601-compatible or
* XML-schema-compatible format (P-string)
* @return parsed time-only duration
* @throws ParseException if parsing fails
* @see #parsePeriod(String)
* @see #parseCalendarPeriod(String)
*/
/*[deutsch]
* Parst eine kanonische Darstellung nur mit
* Uhrzeitkomponenten zu einer Dauer.
*
* @param period duration in canonical, ISO-8601-compatible or
* XML-schema-compatible format (P-string)
* @return parsed time-only duration
* @throws ParseException if parsing fails
* @see #parsePeriod(String)
* @see #parseCalendarPeriod(String)
*/
public static Duration parseClockPeriod(String period)
throws ParseException {
return parsePeriod(period, ClockUnit.class);
}
/**
* Parses a canonical representation with only week-based units (Y, W and D) to a
* calendrical duration where years are interpreted as week-based years.
*
* @param period duration in canonical or ISO-8601-compatible format (P-string)
* @return parsed calendrical duration
* @throws ParseException if parsing fails
* @see #parsePeriod(String)
* @see CalendarUnit#weekBasedYears()
* @since 3.21/4.17
*/
/*[deutsch]
* Parst eine kanonische Darstellung nur mit wochen-basierten
* Datumskomponenten (Y, W, D) zu einer Dauer, in der Jahr als
* wochenbasierte Jahre interpretiert werden.
*
* @param period duration in canonical or ISO-8601-compatible format (P-string)
* @return parsed calendrical duration
* @throws ParseException if parsing fails
* @see #parsePeriod(String)
* @see CalendarUnit#weekBasedYears()
* @since 3.21/4.17
*/
public static Duration parseWeekBasedPeriod(String period)
throws ParseException {
return parsePeriod(period, IsoDateUnit.class);
}
/**
* Equivalent to {@link net.time4j.Duration.Formatter#ofPattern(String)}.
*
* @param pattern format pattern
* @return new formatter instance
* @since 3.0
*/
/*[deutsch]
* Äquivalent zu {@link net.time4j.Duration.Formatter#ofPattern(String)}.
*
* @param pattern format pattern
* @return new formatter instance
* @since 3.0
*/
public static Duration.Formatter formatter(String pattern) {
return Duration.Formatter.ofPattern(pattern);
}
/**
* Equivalent to {@link net.time4j.Duration.Formatter#ofPattern(Class, String)}.
*
* @param generic unit type
* @param type reified unit type
* @param pattern format pattern
* @return new formatter instance
* @since 3.0
*/
/*[deutsch]
* Äquivalent zu {@link net.time4j.Duration.Formatter#ofPattern(Class, String)}.
*
* @param generic unit type
* @param type reified unit type
* @param pattern format pattern
* @return new formatter instance
* @since 3.0
*/
public static Duration.Formatter formatter(
Class type,
String pattern
) {
return Duration.Formatter.ofPattern(type, pattern);
}
private String toString(int style) {
if (
(style == PRINT_STYLE_ISO)
&& this.isNegative()
) {
throw new ChronoException("Negative sign not allowed in ISO-8601.");
}
if (this.isEmpty()) {
return "PT0S";
}
boolean xml = (style == PRINT_STYLE_XML);
StringBuilder sb = new StringBuilder();
if (this.isNegative()) {
sb.append('-');
}
sb.append('P');
boolean timeAppended = false;
boolean weekBased = false;
long nanos = 0;
long seconds = 0;
long weeksAsDays = 0;
for (int index = 0, limit = this.count(); index < limit; index++) {
Item item = this.getTotalLength().get(index);
U unit = item.getUnit();
if (!timeAppended && !unit.isCalendrical()) {
sb.append('T');
timeAppended = true;
}
long amount = item.getAmount();
char symbol = unit.getSymbol();
if (unit == Weekcycle.YEARS) {
weekBased = true;
}
if ((symbol > '0') && (symbol <= '9')) {
assert (symbol == '9');
nanos = amount;
} else if (symbol == 'S') {
seconds = amount;
} else {
if (xml || (style == PRINT_STYLE_ISO)) {
switch (symbol) {
case 'D':
if (weeksAsDays != 0) {
amount = MathUtils.safeAdd(amount, weeksAsDays);
weeksAsDays = 0;
}
sb.append(amount);
break;
case 'M':
case 'Y':
case 'H':
sb.append(amount);
break;
case 'W':
if (limit == 1) {
if (xml) {
sb.append(MathUtils.safeMultiply(amount, 7));
symbol = 'D';
} else {
sb.append(amount);
}
} else {
weeksAsDays = MathUtils.safeMultiply(amount, 7);
if (this.contains(DAYS)) {
continue;
} else {
sb.append(weeksAsDays);
weeksAsDays = 0;
symbol = 'D';
}
}
break;
case 'Q':
sb.append(MathUtils.safeMultiply(amount, 3));
symbol = 'M';
break;
case 'E':
sb.append(MathUtils.safeMultiply(amount, 10));
symbol = 'Y';
break;
case 'C':
sb.append(MathUtils.safeMultiply(amount, 100));
symbol = 'Y';
break;
case 'I':
sb.append(MathUtils.safeMultiply(amount, 1000));
symbol = 'Y';
break;
default:
String mode = xml ? "XML" : "ISO";
throw new ChronoException(
"Special units cannot be output in "
+ mode + "-mode: "
+ this.toString(PRINT_STYLE_NORMAL));
}
} else {
sb.append(amount);
}
if (symbol == '\u0000') {
sb.append('{');
sb.append(unit);
sb.append('}');
} else {
sb.append(symbol);
}
}
}
if (nanos != 0) {
seconds = MathUtils.safeAdd(seconds, nanos / MRD);
sb.append(seconds);
sb.append(xml ? '.' : ISO_DECIMAL_SEPARATOR);
String f = String.valueOf(nanos % MRD);
for (int i = 0, len = 9 - f.length(); i < len; i++) {
sb.append('0');
}
sb.append(f);
sb.append('S');
} else if (seconds != 0) {
sb.append(seconds);
sb.append('S');
}
if (weekBased) {
boolean representable = !timeAppended;
if (representable) {
for (int index = 0, limit = this.count(); index < limit; index++) {
Object unit = this.getTotalLength().get(index).getUnit();
if ((unit != Weekcycle.YEARS) && (unit != CalendarUnit.WEEKS) && (unit != CalendarUnit.DAYS)) {
representable = false;
break;
}
}
}
if (!representable) {
int pos = sb.indexOf("Y");
sb.replace(pos, pos + 1, "{WEEK_BASED_YEARS}");
}
}
return sb.toString();
}
private static boolean hasMixedSigns(
long months,
long daytime
) {
return (
((months < 0) && (daytime > 0))
|| ((months > 0) && (daytime < 0)));
}
private int count() {
return this.getTotalLength().size();
}
// wildcard capture
private static boolean isEmpty(TimeSpan timespan) {
List- > items = timespan.getTotalLength();
for (int i = 0, n = items.size(); i < n; i++) {
if (items.get(i).getAmount() > 0) {
return false;
}
}
return true;
}
private static Duration
ofCalendarUnits(
long years,
long months,
long days,
boolean negative
) {
List- > items = new ArrayList<>(3);
if (years != 0) {
items.add(Item.of(years, YEARS));
}
if (months != 0) {
items.add(Item.of(months, MONTHS));
}
if (days != 0) {
items.add(Item.of(days, DAYS));
}
return new Duration<>(items, negative);
}
private static Duration
ofClockUnits(
long hours,
long minutes,
long seconds,
long nanos,
boolean negative
) {
List- > items = new ArrayList<>(4);
if (hours != 0) {
items.add(Item.of(hours, HOURS));
}
if (minutes != 0) {
items.add(Item.of(minutes, MINUTES));
}
if (seconds != 0) {
items.add(Item.of(seconds, SECONDS));
}
if (nanos != 0) {
items.add(Item.of(nanos, NANOS));
}
return new Duration<>(items, negative);
}
private static Duration create(
Map map,
boolean negative
) {
if (map.isEmpty()) {
return ofZero();
}
List
- > temp = new ArrayList<>(map.size());
long nanos = 0;
for (Map.Entry entry : map.entrySet()) {
long amount = entry.getValue().longValue();
if (amount != 0) {
U key = entry.getKey();
if (key == MILLIS) {
nanos =
MathUtils.safeAdd(
nanos,
MathUtils.safeMultiply(amount, MIO));
} else if (key == MICROS) {
nanos =
MathUtils.safeAdd(
nanos,
MathUtils.safeMultiply(amount, 1000));
} else if (key == NANOS) {
nanos = MathUtils.safeAdd(nanos, amount);
} else {
temp.add(Item.of(amount, key));
}
}
}
if (nanos != 0) {
U key = cast(NANOS);
temp.add(Item.of(nanos, key));
} else if (temp.isEmpty()) {
return ofZero();
}
return new Duration<>(temp, negative);
}
// binäre Suche
private int getIndex(ChronoUnit unit) {
return getIndex(unit, this.getTotalLength());
}
// binäre Suche
private static int getIndex(
ChronoUnit unit,
List
- > list
) {
int low = 0;
int high = list.size() - 1;
while (low <= high) {
int mid = (low + high) >>> 1;
ChronoUnit midUnit = list.get(mid).getUnit();
int cmp = StdNormalizer.compare(midUnit, unit);
if (cmp < 0) {
low = mid + 1;
} else if (cmp > 0) {
high = mid - 1;
} else {
return mid; // gefunden
}
}
return -1;
}
// optional
private static Item replaceFraction(
long amount,
U unit
) {
if (unit.equals(MILLIS)) {
amount = MathUtils.safeMultiply(amount, MIO);
unit = cast(NANOS);
} else if (unit.equals(MICROS)) {
amount = MathUtils.safeMultiply(amount, 1000L);
unit = cast(NANOS);
} else {
return null;
}
return Item.of(amount, unit);
}
private static Duration merge(
Duration duration,
TimeSpan extends U> timespan
) {
if (duration.isEmpty()) {
if (isEmpty(timespan)) {
return duration;
} else if (timespan instanceof Duration) {
return cast(timespan);
}
}
Map map = new HashMap<>();
for (int i = 0, n = duration.count(); i < n; i++) {
Item item = duration.getTotalLength().get(i);
map.put(
item.getUnit(),
Long.valueOf(
MathUtils.safeMultiply(
item.getAmount(),
(duration.isNegative() ? -1 : 1)
)
)
);
}
boolean tsign = timespan.isNegative();
for (int i = 0, n = timespan.getTotalLength().size(); i < n; i++) {
TimeSpan.Item extends U> e = timespan.getTotalLength().get(i);
U unit = e.getUnit();
long amount = e.getAmount();
// Millis und Micros ersetzen
Item item = replaceFraction(amount, unit);
if (item != null) {
amount = item.getAmount();
unit = item.getUnit();
}
// Items aktualisieren
if (map.containsKey(unit)) {
map.put(
unit,
Long.valueOf(
MathUtils.safeAdd(
map.get(unit).longValue(),
MathUtils.safeMultiply(amount, (tsign ? -1 : 1))
)
)
);
} else {
map.put(
unit,
MathUtils.safeMultiply(amount, (tsign ? -1 : 1))
);
}
}
boolean negative = false;
if (duration.isNegative() == tsign) {
negative = tsign;
} else {
boolean firstScan = true;
for (Map.Entry entry : map.entrySet()) {
boolean nsign = (entry.getValue().longValue() < 0);
if (firstScan) {
negative = nsign;
firstScan = false;
} else if (negative != nsign) {
return null; // mixed signs
}
}
}
if (negative) {
for (Map.Entry entry : map.entrySet()) {
long value = entry.getValue().longValue();
map.put(
entry.getKey(),
Long.valueOf(
(value < 0)
? MathUtils.safeNegate(value)
: value)
);
}
}
return Duration.create(map, negative);
}
private static boolean summarize(
TimeSpan extends U> timespan,
long[] sums
) {
long months = sums[0];
long days = sums[1];
long secs = sums[2];
long nanos = sums[3];
for (Item extends U> item : timespan.getTotalLength()) {
U unit = item.getUnit();
long amount = item.getAmount();
if (timespan.isNegative()) {
amount = MathUtils.safeNegate(amount);
}
if (unit instanceof CalendarUnit) {
CalendarUnit cu = CalendarUnit.class.cast(unit);
switch (cu) {
case MILLENNIA:
months =
MathUtils.safeAdd(months,
MathUtils.safeMultiply(amount, 12 * 1000));
break;
case CENTURIES:
months =
MathUtils.safeAdd(months,
MathUtils.safeMultiply(amount, 12 * 100));
break;
case DECADES:
months =
MathUtils.safeAdd(months,
MathUtils.safeMultiply(amount, 12 * 10));
break;
case YEARS:
months =
MathUtils.safeAdd(months,
MathUtils.safeMultiply(amount, 12));
break;
case QUARTERS:
months =
MathUtils.safeAdd(months,
MathUtils.safeMultiply(amount, 3));
break;
case MONTHS:
months = MathUtils.safeAdd(months, amount);
break;
case WEEKS:
days =
MathUtils.safeAdd(days,
MathUtils.safeMultiply(amount, 7));
break;
case DAYS:
days = MathUtils.safeAdd(days, amount);
break;
default:
throw new UnsupportedOperationException(cu.name());
}
} else if (unit instanceof ClockUnit) {
ClockUnit cu = ClockUnit.class.cast(unit);
switch (cu) {
case HOURS:
secs =
MathUtils.safeAdd(secs,
MathUtils.safeMultiply(amount, 3600));
break;
case MINUTES:
secs =
MathUtils.safeAdd(secs,
MathUtils.safeMultiply(amount, 60));
break;
case SECONDS:
secs = MathUtils.safeAdd(secs, amount);
break;
case MILLIS:
nanos =
MathUtils.safeAdd(nanos,
MathUtils.safeMultiply(amount, MIO));
break;
case MICROS:
nanos =
MathUtils.safeAdd(nanos,
MathUtils.safeMultiply(amount, 1000));
break;
case NANOS:
nanos = MathUtils.safeAdd(nanos, amount);
break;
default:
throw new UnsupportedOperationException(cu.name());
}
} else {
return false;
}
}
if (nanos != 0) {
nanos =
MathUtils.safeAdd(nanos,
MathUtils.safeMultiply(days, 86400L * MRD));
nanos =
MathUtils.safeAdd(nanos,
MathUtils.safeMultiply(secs, MRD));
days = 0;
secs = 0;
} else if (secs != 0) {
secs =
MathUtils.safeAdd(secs,
MathUtils.safeMultiply(days, 86400L));
days = 0;
}
sums[0] = months;
sums[1] = days;
sums[2] = secs;
sums[3] = nanos;
return true;
}
private static Duration convert(
TimeSpan timespan
) {
if (timespan instanceof Duration) {
return cast(timespan);
} else {
Duration zero = ofZero();
return zero.plus(timespan);
}
}
private boolean isFractionUnit(IsoUnit unit) {
char symbol = unit.getSymbol();
return ((symbol >= '1') && (symbol <= '9'));
}
@SuppressWarnings("unchecked")
private static
T cast(Object obj) {
return (T) obj;
}
//~ Parse-Routinen ----------------------------------------------------
private static Duration parsePeriod(
String period,
Class type
) throws ParseException {
int index = 0;
boolean negative = false;
if (period.length() == 0) {
throw new ParseException("Empty period string.", index);
} else if (period.charAt(0) == '-') {
negative = true;
index = 1;
}
try {
if (period.charAt(index) != 'P') {
throw new ParseException(
"Format symbol \'P\' expected: " + period, index);
} else {
index++;
}
List- > items = new ArrayList<>();
int sep = period.indexOf('T', index);
boolean calendrical = (sep == -1);
int typeID = SUPER_TYPE;
if (type == CalendarUnit.class) {
typeID = CALENDAR_TYPE;
} else if (type == ClockUnit.class) {
typeID = CLOCK_TYPE;
} else if (type == IsoDateUnit.class) {
typeID = WEEK_BASED_TYPE;
}
if (calendrical) {
if (typeID == CLOCK_TYPE) {
throw new ParseException(
"Format symbol \'T\' expected: " + period, index);
} else {
parse(period, index, period.length(), ((typeID == SUPER_TYPE) ? CALENDAR_TYPE : typeID), items);
}
} else {
boolean alternative = false;
if (sep > index) {
if (typeID == CLOCK_TYPE) {
throw new ParseException(
"Unexpected date component found: " + period,
index);
} else {
alternative = parse(period, index, sep, CALENDAR_TYPE, items);
}
}
if (type == CalendarUnit.class) {
throw new ParseException(
"Unexpected time component found: " + period, sep);
} else if (alternative) {
parseAlt(period, sep + 1, period.length(), false, items);
} else {
parse(period, sep + 1, period.length(), CLOCK_TYPE, items);
}
}
return new Duration<>(items, negative);
} catch (IndexOutOfBoundsException ex) {
ParseException pe =
new ParseException(
"Unexpected termination of period string: " + period,
index);
pe.initCause(ex);
throw pe;
}
}
private static boolean parse(
String period,
int from,
int to,
int typeID,
List
- > items
) throws ParseException {
// alternative format?
char ending = period.charAt(to - 1);
if ((ending >= '0') && (ending <= '9') && (typeID != WEEK_BASED_TYPE)) {
parseAlt(period, from, to, (typeID == CALENDAR_TYPE), items);
return true;
}
if (from == to) {
throw new ParseException(period, from);
}
StringBuilder num = null;
boolean endOfItem = false;
ChronoUnit last = null;
int index = from;
boolean decimal = false;
for (int i = from; i < to; i++) {
char c = period.charAt(i);
if ((c >= '0') && (c <= '9')) {
if (num == null) {
num = new StringBuilder();
endOfItem = false;
index = i;
}
num.append(c);
} else if ((c == ',') || (c == '.')) {
if ((num == null) || (typeID != CLOCK_TYPE)) {
throw new ParseException(
"Decimal separator misplaced: " + period, i);
} else {
endOfItem = true;
long amount = parseAmount(period, num.toString(), index);
ChronoUnit unit = SECONDS;
last =
addParsedItem(unit, last, amount, period, i, items);
num = null;
decimal = true;
}
} else if (endOfItem) {
throw new ParseException(
"Unexpected char \'" + c + "\' found: " + period, i);
} else if (decimal) {
if (c != 'S') {
throw new ParseException(
"Second symbol expected: " + period, i);
} else if (num == null) {
throw new ParseException(
"Decimal separator misplaced: " + period, i - 1);
} else if (num.length() > 9) {
num.delete(9, num.length());
}
for (int j = num.length(); j < 9; j++) {
num.append('0');
}
endOfItem = true;
long amount = parseAmount(period, num.toString(), index);
ChronoUnit unit = NANOS;
num = null;
last = addParsedItem(unit, last, amount, period, i, items);
} else {
endOfItem = true;
long amount =
parseAmount(
period,
(num == null) ? String.valueOf(c) : num.toString(),
index);
num = null;
ChronoUnit unit;
if (typeID == CLOCK_TYPE) {
unit = parseTimeSymbol(c, period, i);
} else if (typeID == WEEK_BASED_TYPE) {
unit = parseWeekBasedSymbol(c, period, i);
} else {
unit = parseDateSymbol(c, period, i);
}
last = addParsedItem(unit, last, amount, period, i, items);
}
}
if (!endOfItem) {
throw new ParseException("Unit symbol expected: " + period, to);
}
return false;
}
private static void parseAlt(
String period,
int from,
int to,
boolean date,
List
- > items
) throws ParseException {
boolean extended = false;
if (date) {
if (from + 4 < to) {
extended = (period.charAt(from + 4) == '-');
}
boolean ordinalStyle = (
extended
? (from + 8 == to)
: (from + 7 == to));
Duration> dur = getAlternativeDateFormat(extended, ordinalStyle).parse(period, from);
long years = dur.getPartialAmount(YEARS);
long months;
long days;
if (ordinalStyle) {
months = 0;
days = dur.getPartialAmount(DAYS);
// ISO does not specify any constraint here
} else {
months = dur.getPartialAmount(MONTHS);
days = dur.getPartialAmount(DAYS);
if (months > 12) {
throw new ParseException(
"ISO-8601 prohibits months-part > 12: " + period,
from + 4 + (extended ? 1 : 0));
}
if (days > 30) {
throw new ParseException(
"ISO-8601 prohibits days-part > 30: " + period,
from + 6 + (extended ? 2 : 0));
}
}
if (years > 0) {
U unit = cast(YEARS);
items.add(Item.of(years, unit));
}
if (months > 0) {
U unit = cast(MONTHS);
items.add(Item.of(months, unit));
}
if (days > 0) {
U unit = cast(DAYS);
items.add(Item.of(days, unit));
}
} else {
if (from + 2 < to) {
extended = (period.charAt(from + 2) == ':');
}
Duration> dur = getAlternativeTimeFormat(extended).parse(period, from);
long hours = dur.getPartialAmount(HOURS);
if (hours > 0) {
if (hours > 24) {
throw new ParseException(
"ISO-8601 prohibits hours-part > 24: " + period,
from);
}
U unit = cast(HOURS);
items.add(Item.of(hours, unit));
}
long minutes = dur.getPartialAmount(MINUTES);
if (minutes > 0) {
if (minutes > 60) {
throw new ParseException(
"ISO-8601 prohibits minutes-part > 60: " + period,
from + 2 + (extended ? 1 : 0));
}
U unit = cast(MINUTES);
items.add(Item.of(minutes, unit));
}
long seconds = dur.getPartialAmount(SECONDS);
if (seconds > 0) {
if (seconds > 60) {
throw new ParseException(
"ISO-8601 prohibits seconds-part > 60: " + period,
from + 4 + (extended ? 2 : 0));
}
U unit = cast(SECONDS);
items.add(Item.of(seconds, unit));
}
long nanos = dur.getPartialAmount(NANOS);
if (nanos > 0) {
U unit = cast(NANOS);
items.add(Item.of(nanos, unit));
}
}
}
private static Duration.Formatter
createAlternativeDateFormat(
boolean extended,
boolean ordinalStyle
) {
String pattern;
if (extended) {
if (ordinalStyle) {
pattern = "YYYY-DDD";
} else {
pattern = "YYYY-MM-DD";
}
} else {
if (ordinalStyle) {
pattern = "YYYYDDD";
} else {
pattern = "YYYYMMDD";
}
}
return Duration.Formatter.ofPattern(CalendarUnit.class, pattern);
}
private static Duration.Formatter getAlternativeDateFormat(
boolean extended,
boolean ordinalStyle
) {
if (extended) {
return (ordinalStyle ? CF_EXT_ORD : CF_EXT_CAL);
} else {
return (ordinalStyle ? CF_BAS_ORD : CF_BAS_CAL);
}
}
private static Duration.Formatter createAlternativeTimeFormat(boolean extended) {
String pattern = (
extended
? "hh[:mm[:ss[,fffffffff]]]"
: "hh[mm[ss[,fffffffff]]]"
);
return Duration.Formatter.ofPattern(ClockUnit.class, pattern);
}
private static Duration.Formatter getAlternativeTimeFormat(boolean extended) {
return (extended ? TF_EXT : TF_BAS);
}
private static CalendarUnit parseDateSymbol(
char c,
String period,
int index
) throws ParseException {
switch (c) {
case 'I':
return MILLENNIA;
case 'C':
return CENTURIES;
case 'E':
return DECADES;
case 'Y':
return YEARS;
case 'Q':
return QUARTERS;
case 'M':
return MONTHS;
case 'W':
return WEEKS;
case 'D':
return DAYS;
default:
throw new ParseException(
"Symbol \'" + c + "\' not supported: " + period, index);
}
}
private static ClockUnit parseTimeSymbol(
char c,
String period,
int index
) throws ParseException {
switch (c) {
case 'H':
return HOURS;
case 'M':
return MINUTES;
case 'S':
return SECONDS;
default:
throw new ParseException(
"Symbol \'" + c + "\' not supported: " + period, index);
}
}
private static IsoDateUnit parseWeekBasedSymbol(
char c,
String period,
int index
) throws ParseException {
switch (c) {
case 'Y':
return CalendarUnit.weekBasedYears();
case 'W':
return WEEKS;
case 'D':
return DAYS;
default:
throw new ParseException(
"Symbol \'" + c + "\' not supported: " + period, index);
}
}
private static ChronoUnit addParsedItem(
ChronoUnit unit,
ChronoUnit last, // optional
long amount,
String period,
int index,
List- > items
) throws ParseException {
if (
(last == null)
|| (Double.compare(unit.getLength(), last.getLength()) < 0)
) {
if (amount != 0) {
U reified = cast(unit);
items.add(Item.of(amount, reified));
}
return unit;
} else if (Double.compare(unit.getLength(), last.getLength()) == 0) {
throw new ParseException(
"Duplicate unit items: " + period, index);
} else {
throw new ParseException(
"Wrong order of unit items: " + period, index);
}
}
private static long parseAmount(
String period,
String number,
int index
) throws ParseException {
try {
return Long.parseLong(number);
} catch (NumberFormatException nfe) {
ParseException pe = new ParseException(period, index);
pe.initCause(nfe);
throw pe;
}
}
/**
* @serialData Uses
* a dedicated serialization form as proxy. The layout
* is bit-compressed. The first byte contains within the
* four most significant bits the type id {@code 6} and as
* least significant bit the value 1 if long should be used
* for transferring the item amounts (else using int). Then
* the data bytes for the duration items follow. The byte
* sequence optionally ends with the sign information.
*
* Schematic algorithm:
*
*
* boolean useLong = ...;
* byte header = (6 << 4);
* if (useLong) header |= 1;
* out.writeByte(header);
* out.writeInt(getTotalLength().size());
* for (Item<U> item : getTotalLength()) {
* if (useLong) {
* out.writeLong(item.getAmount());
* } else {
* out.writeInt((int) item.getAmount());
* }
* out.writeObject(item.getUnit());
* }
* if (getTotalLength().size() > 0) {
* out.writeBoolean(isNegative());
* }
*
*
* @return replacement object in serialization graph
*/
private Object writeReplace() {
return new SPX(this, SPX.DURATION_TYPE);
}
/**
* @serialData Blocks because a serialization proxy is required.
* @param in object input stream
* @throws InvalidObjectException (always)
*
*/
private void readObject(ObjectInputStream in)
throws IOException {
throw new InvalidObjectException("Serialization proxy required.");
}
//~ Innere Klassen ----------------------------------------------------
/**
* Builder class for constructing a duration conforming to ISO-8601
* which consists of years, months, days and any wall time units.
*
* The week unit is not possible in builder because this unit should
* be stand-alone according to ISO-8601. A week-based duration can be
* created by expression {@code Duration.of(amount, CalendarUnit.WEEKS)}
* however.
*
* A builder instance must be created by {@link Duration#ofPositive()}
* or {@link Duration#ofNegative()}. Note that the builder is only be
* designed for single-thread-environments that is always creating a
* new builder instance per thread.
*/
/*[deutsch]
* Hilfsobjekt zum Bauen einer ISO-konformen Zeitspanne bestehend aus
* Jahren, Monaten, Tagen und allen Uhrzeiteinheiten.
*
* Lediglich die Wocheneinheit ist ausgenommen, da eine wochenbasierte
* Zeitspanne nach dem ISO-Standard für sich alleine stehen sollte.
* Eine wochenbasierte Zeitspanne kann auf einfache Weise mit dem Ausdruck
* {@code Duration.of(amount, CalendarUnit.WEEKS)} erzeugt werden.
*
* Eine Instanz wird mittels {@link Duration#ofPositive()} oder
* {@link Duration#ofNegative()} erzeugt. Diese Instanz ist nur zur
* lokalen Verwendung in einem Thread gedacht, da keine Thread-Sicherheit
* gegeben ist.
*/
public static class Builder {
//~ Instanzvariablen ----------------------------------------------
private final List- > items;
private final boolean negative;
private boolean millisSet = false;
private boolean microsSet = false;
private boolean nanosSet = false;
//~ Konstruktoren -------------------------------------------------
/**
*
Konstruiert ein Hilfsobjekt zum Bauen einer Zeitspanne.
*
* @param negative Is a negative duration asked for?
*/
Builder(boolean negative) {
super();
this.items = new ArrayList<>(10);
this.negative = negative;
}
//~ Methoden ------------------------------------------------------
/**
* Adds a year item.
*
* @param num count of years {@code >= 0}
* @return this instance for method chaining
* @throws IllegalArgumentException if the argument is negative
* @throws IllegalStateException if already called
* @see CalendarUnit#YEARS
*/
/*[deutsch]
* Erzeugt eine Länge in Jahren.
*
* @param num count of years {@code >= 0}
* @return this instance for method chaining
* @throws IllegalArgumentException if the argument is negative
* @throws IllegalStateException if already called
* @see CalendarUnit#YEARS
*/
public Builder years(int num) {
this.set(num, YEARS);
return this;
}
/**
* Adds a month item.
*
* @param num count of months {@code >= 0}
* @return this instance for method chaining
* @throws IllegalArgumentException if the argument is negative
* @throws IllegalStateException if already called
* @see CalendarUnit#MONTHS
*/
/*[deutsch]
* Erzeugt eine Länge in Monaten.
*
* @param num count of months {@code >= 0}
* @return this instance for method chaining
* @throws IllegalArgumentException if the argument is negative
* @throws IllegalStateException if already called
* @see CalendarUnit#MONTHS
*/
public Builder months(int num) {
this.set(num, MONTHS);
return this;
}
/**
* Adds a day item.
*
* @param num count of days {@code >= 0}
* @return this instance for method chaining
* @throws IllegalArgumentException if the argument is negative
* @throws IllegalStateException if already called
* @see CalendarUnit#DAYS
*/
/*[deutsch]
* Erzeugt eine Länge in Tagen.
*
* @param num count of days {@code >= 0}
* @return this instance for method chaining
* @throws IllegalArgumentException if the argument is negative
* @throws IllegalStateException if already called
* @see CalendarUnit#DAYS
*/
public Builder days(int num) {
this.set(num, DAYS);
return this;
}
/**
* Adds a hour item.
*
* @param num count of hours {@code >= 0}
* @return this instance for method chaining
* @throws IllegalArgumentException if the argument is negative
* @throws IllegalStateException if already called
* @see ClockUnit#HOURS
*/
/*[deutsch]
* Erzeugt eine Länge in Stunden.
*
* @param num count of hours {@code >= 0}
* @return this instance for method chaining
* @throws IllegalArgumentException if the argument is negative
* @throws IllegalStateException if already called
* @see ClockUnit#HOURS
*/
public Builder hours(int num) {
this.set(num, HOURS);
return this;
}
/**
* Adds a minute item.
*
* @param num count of minutes {@code >= 0}
* @return this instance for method chaining
* @throws IllegalArgumentException if the argument is negative
* @throws IllegalStateException if already called
* @see ClockUnit#MINUTES
*/
/*[deutsch]
* Erzeugt eine Länge in Minuten.
*
* @param num count of minutes {@code >= 0}
* @return this instance for method chaining
* @throws IllegalArgumentException if the argument is negative
* @throws IllegalStateException if already called
* @see ClockUnit#MINUTES
*/
public Builder minutes(int num) {
this.set(num, MINUTES);
return this;
}
/**
* Adds a second item.
*
* @param num count of seconds {@code >= 0}
* @return this instance for method chaining
* @throws IllegalArgumentException if the argument is negative
* @throws IllegalStateException if already called
* @see ClockUnit#SECONDS
*/
/*[deutsch]
* Erzeugt eine Länge in Sekunden.
*
* @param num count of seconds {@code >= 0}
* @return this instance for method chaining
* @throws IllegalArgumentException if the argument is negative
* @throws IllegalStateException if already called
* @see ClockUnit#SECONDS
*/
public Builder seconds(int num) {
this.set(num, SECONDS);
return this;
}
/**
* Adds a millisecond item.
*
* The argument will automatically be normalized to nanoseconds.
*
* @param num count of milliseconds {@code >= 0}
* @return this instance for method chaining
* @throws IllegalArgumentException if the argument is negative
* @throws IllegalStateException if already called
*/
/*[deutsch]
* Erzeugt eine Länge in Millisekunden.
*
* Es wird eine Normalisierung durchgeführt, indem das Argument
* mit dem Faktor {@code 1} Million multipliziert und in Nanosekunden
* gespeichert wird.
*
* @param num count of milliseconds {@code >= 0}
* @return this instance for method chaining
* @throws IllegalArgumentException if the argument is negative
* @throws IllegalStateException if already called
*/
public Builder millis(int num) {
this.millisCalled();
this.update(num, MIO);
return this;
}
/**
* Adds a microsecond item.
*
* The argument will automatically be normalized to nanoseconds.
*
* @param num count of microseconds {@code >= 0}
* @return this instance for method chaining
* @throws IllegalArgumentException if the argument is negative
* @throws IllegalStateException if already called
*/
/*[deutsch]
* Erzeugt eine Länge in Mikrosekunden.
*
* Es wird eine Normalisierung durchgeführt, indem das Argument
* mit dem Faktor {@code 1000} multipliziert und in Nanosekunden
* gespeichert wird.
*
* @param num count of microseconds {@code >= 0}
* @return this instance for method chaining
* @throws IllegalArgumentException if the argument is negative
* @throws IllegalStateException if already called
*/
public Builder micros(int num) {
this.microsCalled();
this.update(num, 1000L);
return this;
}
/**
* Adds a nanosecond item.
*
* @param num count of nanoseconds {@code >= 0}
* @return this instance for method chaining
* @throws IllegalArgumentException if the argument is negative
* @throws IllegalStateException if already called
* @see ClockUnit#NANOS
*/
/*[deutsch]
* Erzeugt eine Länge in Nanosekunden.
*
* @param num count of nanoseconds {@code >= 0}
* @return this instance for method chaining
* @throws IllegalArgumentException if the argument is negative
* @throws IllegalStateException if already called
* @see ClockUnit#NANOS
*/
public Builder nanos(int num) {
this.nanosCalled();
this.update(num, 1L);
return this;
}
/**
* Creates a new duration conforming to ISO-8601.
*
* @return new {@code Duration}
*/
/*[deutsch]
* Erzeugt eine neue ISO-konforme Zeitspanne.
*
* @return new {@code Duration}
*/
public Duration build() {
if (this.items.isEmpty()) {
throw new IllegalStateException("Not set any amount and unit.");
}
return new Duration<>(
this.items,
this.negative
);
}
private Builder set(
long amount,
IsoUnit unit
) {
for (int i = 0, n = this.items.size(); i < n; i++) {
if (this.items.get(i).getUnit() == unit) {
throw new IllegalStateException(
"Already registered: " + unit);
}
}
if (amount != 0) {
Item item = Item.of(amount, unit);
this.items.add(item);
}
return this;
}
private void update(
long amount,
long factor
) {
if (amount >= 0) {
IsoUnit unit = NANOS;
for (int i = this.items.size() - 1; i >= 0; i--) {
Item item = this.items.get(i);
if (item.getUnit().equals(NANOS)) {
this.items.set(
i,
Item.of(
MathUtils.safeAdd(
MathUtils.safeMultiply(amount, factor),
item.getAmount()
),
unit
)
);
return;
}
}
if (amount != 0) {
this.items.add(
Item.of(
MathUtils.safeMultiply(amount, factor),
unit
)
);
}
} else {
throw new IllegalArgumentException(
"Illegal negative amount: " + amount);
}
}
private void millisCalled() {
if (this.millisSet) {
throw new IllegalStateException(
"Called twice for: " + MILLIS.name());
}
this.millisSet = true;
}
private void microsCalled() {
if (this.microsSet) {
throw new IllegalStateException(
"Called twice for: " + MICROS.name());
}
this.microsSet = true;
}
private void nanosCalled() {
if (this.nanosSet) {
throw new IllegalStateException(
"Called twice for: " + NANOS.name());
}
this.nanosSet = true;
}
}
private static class ZonalMetric
implements TimeMetric> {
//~ Instanzvariablen ----------------------------------------------
private final Timezone tz;
private final TimeMetric> metric;
//~ Konstruktoren -------------------------------------------------
private ZonalMetric(
Timezone tz,
IsoUnit... units
) {
super();
if (tz == null) {
throw new NullPointerException("Missing timezone.");
}
this.tz = tz;
this.metric = new Metric<>(units);
}
//~ Methoden ------------------------------------------------------
@Override
public >
Duration between(
T start,
T end
) {
T t1 = start;
T t2 = end;
boolean negative = false;
if (start.compareTo(end) > 0) {
t1 = end;
t2 = start;
negative = true;
}
int o1 = this.getOffset(t1);
int o2 = this.getOffset(t2);
t2 = t2.plus(o1 - o2, SECONDS);
Duration duration = this.metric.between(t1, t2);
if (negative) {
duration = duration.inverse();
}
return duration;
}
private int getOffset(ChronoEntity> entity) {
return this.tz.getStrategy().getOffset(
entity.get(PlainDate.COMPONENT),
entity.get(PlainTime.COMPONENT),
this.tz
).getIntegralAmount();
}
}
/**
* Non-localized and user-defined format for durations based on a
* pattern containing some standard symbols and literals.
*
* Note: For storing purposes users should normally use the canonical
* or ISO- or XML-representation of a duration, not this custom format.
* Otherwise, if users want a localized output then the class
* {@link PrettyTime} is usually the best choice. This class is mainly
* designed for handling non-standardized formats.
*
* First example (parsing a Joda-Time-Period using a max width of 2):
*
*
* Duration.Formatter<IsoUnit> f = Duration.Formatter.ofJodaStyle();
* Duration<?> dur = f.parse("P-2Y-15DT-30H-5M");
* System.out.println(dur); // output: -P2Y15DT30H5M
*
*
* Second example (printing a wall-time-like duration):
*
*
* Duration.Formatter<ClockUnit> f =
* Duration.Formatter.ofPattern(ClockUnit.class, "+hh:mm:ss");
* String s = f.print(Duration.ofClockUnits(27, 30, 5));
* System.out.println(s); // output: +27:30:05
*
*
* @param generic type of time units
* @since 1.2
* @see Duration#toString()
* @see Duration#parsePeriod(String)
* @see #ofPattern(Class, String)
* @doctags.concurrency {immutable}
*/
/*[deutsch]
* Nicht-lokalisiertes benutzerdefiniertes Dauerformat, das auf
* Symbolmustern beruht.
*
* Hinweis: Zum Speichern sollten Anwender normalerweise die kanonische
* Form oder die ISO- oder die XML-Form einer Dauer verwenden, nicht dieses
* benutzerdefinierte Format. Wird andererseits eine lokalisierte Ausgabe
* gewünscht, dann ist die Klasse {@link PrettyTime} erste Wahl.
* Diese Klasse ist vor allem für die Verarbeitung von nicht-
* standardisierten Formaten zuständig.
*
* Beispiel 1 (Analyse einer Joda-Time-Periode):
*
*
* Duration.Formatter<IsoUnit> f = Duration.Formatter.ofJodaStyle();
* Duration<?> dur = f.parse("P-2Y-15DT-30H-5M");
* System.out.println(dur); // Ausgabe: -P2Y15DT30H5M
*
*
* Zweites Beispiel (Ausgabe einer uhrzeitähnlichen Dauer):
*
*
* Duration.Formatter<ClockUnit> f =
* Duration.Formatter.ofPattern(ClockUnit.class, "+hh:mm:ss");
* String s = f.print(Duration.ofClockUnits(27, 30, 5));
* System.out.println(s); // Ausgabe: +27:30:05
*
*
* @param generic type of time units
* @since 1.2
* @see Duration#toString()
* @see Duration#parsePeriod(String)
* @see #ofPattern(Class, String)
* @doctags.concurrency {immutable}
*/
public static final class Formatter {
//~ Statische Felder/Initialisierungen ----------------------------
private static final String JODA_PATTERN =
"'P'[-#################Y'Y'][-#################M'M'][-#################W'W'][-#################D'D']"
+ "['T'[-#################h'H'][-#################m'M'][-#################s'S'[.fffffffff]]]";
//~ Instanzvariablen ----------------------------------------------
private final Class type;
private final List items;
private final String pattern;
//~ Konstruktoren -------------------------------------------------
private Formatter(
Class type,
List items,
String pattern
) {
super();
if (type == null) {
throw new NullPointerException("Missing unit type.");
} else if (items.isEmpty()) {
throw new IllegalArgumentException("Missing format pattern.");
}
int n = items.size();
int reserved = items.get(n - 1).getMinWidth();
for (int i = n - 2; i >= 0; i--) {
FormatItem item = items.get(i);
items.set(i, item.update(reserved));
reserved += item.getMinWidth();
}
this.type = type;
this.items = Collections.unmodifiableList(items);
this.pattern = pattern;
}
//~ Methoden ------------------------------------------------------
/**
* Handles Joda-Time-style-patterns which in general follow XML-schema
* - with the exception of sign handling.
*
* The sign handling of Joda-Time allows and even enforces in contrast
* to XML-schema negative signs not before the P-symbol but for every
* single duration item repeatedly. Warning: Mixed signs are never supported
* by Time4J.
*
* @return new formatter instance for parsing Joda-Style period expressions
* @since 3.0
* @see #ofPattern(Class, String)
*/
/*[deutsch]
* Behandelt Joda-Time-Stil-Formatmuster, die im allgemeinen XML-Schema
* folgen - mit der Ausnahme der Vorzeichenbehandlung.
*
* Die Vorzeichenbehandlung von Joda-Time erlaubt und erzwingt im Kontrast
* zu XML-Schema negative Vorzeichen nicht vor dem P-Symbol, sondern wiederholt
* für jedes einzelne Dauerelement. Warnung: Gemischte Vorzeichen werden
* von Time4J dennoch nicht unterstützt.
*
* @return new formatter instance for parsing Joda-Style period expressions
* @since 3.0
* @see #ofPattern(Class, String)
*/
public static Formatter ofJodaStyle() {
return ofPattern(IsoUnit.class, JODA_PATTERN);
}
/**
* Equivalent to {@code ofPattern(IsoUnit.class, pattern)}.
*
* @param pattern format pattern
* @return new formatter instance
* @since 1.2
* @see #ofPattern(Class, String)
*/
/*[deutsch]
* Äquivalent zu {@code ofPattern(IsoUnit.class, pattern)}.
*
* @param pattern format pattern
* @return new formatter instance
* @since 1.2
* @see #ofPattern(Class, String)
*/
public static Formatter ofPattern(String pattern) {
return ofPattern(IsoUnit.class, pattern);
}
/**
* Constructs a new instance of duration formatter.
*
* Uses a pattern with symbols as followed:
*
*
* Legend
* Symbol Description
* + sign of duration, printing + or -
* - sign of duration, printing only -
* I {@link CalendarUnit#MILLENNIA}
* C {@link CalendarUnit#CENTURIES}
* E {@link CalendarUnit#DECADES}
* Y {@link CalendarUnit#YEARS}
* Q {@link CalendarUnit#QUARTERS}
* M {@link CalendarUnit#MONTHS}
* W {@link CalendarUnit#WEEKS}
* D {@link CalendarUnit#DAYS}
* h {@link ClockUnit#HOURS}
* m {@link ClockUnit#MINUTES}
* s {@link ClockUnit#SECONDS}
* , decimal separator, comma is preferred
* . decimal separator, dot is preferred
* f
* {@link ClockUnit#NANOS} as fraction, (1-9) chars
* ' apostroph, for escaping literal chars
* [] optional section
* {} section with plural forms, since v2.0
* # placeholder for an optional digit, since v3.0
*
*
* All letters in range a-z and A-Z are always reserved chars
* and must be escaped by apostrophes for interpretation as literals.
* If such a letter is repeated then the count of symbols controls
* the minimum width for formatted output. Such a minimum width also
* reserves this area for parsing of any preceding item. If necessary a
* number (of units) will be padded from left with the zero digit. The
* unit symbol (with exception of "f") can be preceded by
* any count of char "#" (>= 0). The sum of min width and
* count of #-chars define the maximum width for formatted output and
* parsing.
*
* Optional sections let the parser be error-tolerant and continue
* with the next section in case of errors. Since v2.3: During printing,
* an optional section will only be printed if there is any non-zero
* part.
*
* Enhancement since version v2.0: plural forms
*
* Every expression inside curly brackets represents a combination
* of amount, separator and pluralized unit name and has following
* syntax:
*
* {[symbol]:[separator]:[locale]:[CATEGORY=LITERAL][:...]}
*
* The symbol is one of following chars:
* I, C, E, Y, Q, M, W, D, h, m, s, f (legend see table above)
*
* Afterwards the definition of separator chars follows. The
* empty separator (represented by zero space between colons) is
* permitted, too. The next section denotes the locale necessary
* for determination of suitable plural rules. The form
* [language]-[country]-[variant] can be used, for example
* "en-US" or "en_US". At least the language
* must be present. The underscore is an acceptable alternative
* for the minus-sign. Finally there must be a sequence of
* name-value-pairs in the form CATEGORY=LITERAL. Every category
* label must be the name of a {@link PluralCategory plural category}.
* The category OTHER must exist. Example:
*
*
* Duration.Formatter<CalendarUnit> formatter =
* Duration.Formatter.ofPattern(
* CalendarUnit.class,
* "{D: :en:ONE=day:OTHER=days}");
* String s = formatter.format(Duration.of(3, DAYS));
* System.out.println(s); // output: 3 days
*
*
* Enhancement since version v3.0: numerical placeholders
*
* Before version v3.0, the maximum numerical width was always 18. Now it is
* the sum of min width and the count of preceding #-chars. Example:
*
*
* Duration.Formatter<CalendarUnit> formatter1 =
* Duration.Formatter.ofPattern(CalendarUnit.class, "D");
* formatter1.format(Duration.of(123, DAYS)); throws IllegalArgumentException
*
* Duration.Formatter<CalendarUnit> formatter2 =
* Duration.Formatter.ofPattern(CalendarUnit.class, "##D");
* String s = formatter2.format(Duration.of(123, DAYS));
* System.out.println(s); // output: 123
*
*
* @param generic unit type
* @param type reified unit type
* @param pattern format pattern
* @return new formatter instance
* @since 1.2
*/
/*[deutsch]
* Konstruiert eine neue Formatinstanz.
*
* Benutzt ein Formatmuster mit Symbolen wie folgt:
*
*
* Legende
* Symbol Beschreibung
* + Vorzeichen der Dauer, gibt + oder - aus
* - Vorzeichen der Dauer, gibt nur - aus
* I {@link CalendarUnit#MILLENNIA}
* C {@link CalendarUnit#CENTURIES}
* E {@link CalendarUnit#DECADES}
* Y {@link CalendarUnit#YEARS}
* Q {@link CalendarUnit#QUARTERS}
* M {@link CalendarUnit#MONTHS}
* W {@link CalendarUnit#WEEKS}
* D {@link CalendarUnit#DAYS}
* h {@link ClockUnit#HOURS}
* m {@link ClockUnit#MINUTES}
* s {@link ClockUnit#SECONDS}
* , Dezimaltrennzeichen, vorzugsweise Komma
* . Dezimaltrennzeichen, vorzugsweise Punkt
* f
* {@link ClockUnit#NANOS} als Bruchteil, (1-9) Zeichen
* ' Apostroph, zum Markieren von Literalen
* [] optionaler Abschnitt
* {} Abschnitt mit Pluralformen, seit v2.0
* # zukünftiges reserviertes Zeichen
*
*
* Alle Buchstaben im Bereich a-z und A-Z sind grundsätzlich
* reservierte Zeichen und müssen als Literale in Apostrophe
* gefasst werden. Wird ein Buchstabensymbol mehrfach wiederholt,
* dann regelt die Anzahl der Symbole die Mindestbreite in der formatierten
* Ausgabe. Solch eine Mindestbreite reserviert auch das zugehörige Element,
* wenn vorangehende Dauerelemente interpretiert werden. Bei Bedarf wird eine
* Zahl (von Einheiten) von links mit der Nullziffer aufgefüllt. Ein
* Einheitensymbol kann eine beliebige Zahl von numerischen Platzhaltern
* "#" vorangestellt haben (>= 0). Die Summe aus minimaler Breite
* und der Anzahl der #-Zeichen definiert die maximale Breite, die ein
* Dauerelement numerisch haben darf.
*
* Optionale Abschnitte regeln, daß der Interpretationsvorgang
* bei Fehlern nicht sofort abbricht, sondern mit dem nächsten
* Abschnitt fortsetzt und den fehlerhaften Abschnitt ignoriert. Seit
* v2.3 gilt auch, daß optionale Abschnitte nur dann etwas
* ausgeben, wenn es darin irgendeine von {code 0} verschiedene
* Dauerkomponente gibt.
*
* Erweiterung seit Version v2.0: Pluralformen
*
* Jeder in geschweifte Klammern gefasste Ausdruck symbolisiert
* eine Kombination aus Betrag, Trennzeichen und pluralisierten
* Einheitsnamen und hat folgende Syntax:
*
* {[symbol]:[separator]:[locale]:[CATEGORY=LITERAL][:...]}
*
* Das Symbol ist eines von folgenden Zeichen: I, C, E, Y, Q, M,
* W, D, h, m, s, f (Bedeutung siehe Tabelle)
*
* Danach folgen Trennzeichen, abgetrennt durch einen Doppelpunkt.
* Eine leere Zeichenkette ist auch zulässig. Danach folgt eine
* Lokalisierungsangabe zum Bestimmen der Pluralregeln in der Form
* [language]-[country]-[variant], zum Beispiel "de-DE" oder
* "en_US". Mindestens muß die Sprache vorhanden sein.
* Der Unterstrich wird neben dem Minuszeichen ebenfalls interpretiert.
* Schließlich folgt eine Sequenz von Name-Wert-Paaren in
* der Form CATEGORY=LITERAL. Jede Kategoriebezeichnung ist der Name
* einer {@link PluralCategory Pluralkategorie}. Die Kategorie OTHER
* muß enthalten sein. Beispiel:
*
*
* Duration.Formatter<CalendarUnit> formatter =
* Duration.Formatter.ofPattern(
* CalendarUnit.class,
* "{D: :de:ONE=Tag:OTHER=Tage}");
* String s = formatter.format(Duration.of(3, DAYS));
* System.out.println(s); // output: 3 Tage
*
*
* Erweiterung seit Version v3.0: numerische Platzhalter
*
* Vor Version 3.0 war die maximale numerische Breite immer 18 Zeichen lang,
* nun immer die Summe aus minimaler Breite und der Anzahl der vorangehenden
* #-Zeichen. Beispiel:
*
*
* Duration.Formatter<CalendarUnit> formatter1 =
* Duration.Formatter.ofPattern(CalendarUnit.class, "D");
* formatter1.format(Duration.of(123, DAYS)); throws IllegalArgumentException
*
* Duration.Formatter<CalendarUnit> formatter2 =
* Duration.Formatter.ofPattern(CalendarUnit.class, "##D");
* String s = formatter2.format(Duration.of(123, DAYS));
* System.out.println(s); // output: 123
*
*
* @param generic unit type
* @param type reified unit type
* @param pattern format pattern
* @return new formatter instance
* @since 1.2
*/
public static Formatter ofPattern(
Class type,
String pattern
) {
int n = pattern.length();
List> stack = new ArrayList<>();
stack.add(new ArrayList<>());
int digits = 0;
for (int i = 0; i < n; i++) {
char c = pattern.charAt(i);
if (c == '#') {
digits++;
} else if (isSymbol(c)) {
int start = i++;
while ((i < n) && pattern.charAt(i) == c) {
i++;
}
addSymbol(c, i - start, digits, stack);
digits = 0;
i--;
} else if (digits > 0) {
throw new IllegalArgumentException("Char # must be followed by unit symbol.");
} else if (c == '\'') { // Literalsektion
int start = i++;
while (i < n) {
if (pattern.charAt(i) == '\'') {
if (
(i + 1 < n)
&& (pattern.charAt(i + 1) == '\'')
) {
i++;
} else {
break;
}
}
i++;
}
if (i >= n) {
throw new IllegalArgumentException(
"String literal in pattern not closed: " + pattern);
}
if (start + 1 == i) {
addLiteral('\'', stack);
} else {
String s = pattern.substring(start + 1, i);
addLiteral(s.replace("''", "'"), stack);
}
} else if (c == '[') {
startOptionalSection(stack);
} else if (c == ']') {
endOptionalSection(stack);
} else if (c == '.') {
lastOn(stack).add(new SeparatorItem('.', ','));
} else if (c == ',') {
lastOn(stack).add(new SeparatorItem(',', '.'));
} else if (c == '-') {
lastOn(stack).add(new SignItem(false));
} else if (c == '+') {
lastOn(stack).add(new SignItem(true));
} else if (c == '{') {
int start = ++i;
while ((i < n) && pattern.charAt(i) != '}') {
i++;
}
addPluralItem(pattern.substring(start, i), stack);
} else {
addLiteral(c, stack);
}
}
if (stack.size() > 1) {
throw new IllegalArgumentException(
"Open square bracket without closing one.");
} else if (stack.isEmpty()) {
throw new IllegalArgumentException("Empty or invalid pattern.");
}
return new Formatter<>(type, stack.get(0), pattern);
}
/**
* Yields the underlying format pattern.
*
* @return String
* @since 1.2
*/
/*[deutsch]
* Liefert das zugrundeliegende Formatmuster.
*
* @return String
* @since 1.2
*/
public String getPattern() {
return this.pattern;
}
/**
* Yields the associated reified unit type.
*
* @return Class
* @since 1.2
*/
/*[deutsch]
* Liefert den zugehörigen Zeiteinheitstyp.
*
* @return Class
* @since 1.2
*/
public Class getType() {
return this.type;
}
/**
* Creates a textual output of given duration.
*
* @param duration duration object
* @return textual representation of duration
* @throws IllegalArgumentException if some aspects of duration
* prevents printing (for example too many nanoseconds)
* @since 1.2
*/
/*[deutsch]
* Erzeugt eine textuelle Ausgabe der angegebenen Dauer.
*
* @param duration duration object
* @return textual representation of duration
* @throws IllegalArgumentException if some aspects of duration
* prevents printing (for example too many nanoseconds)
* @since 1.2
*/
public String format(Duration> duration) {
StringBuilder buffer = new StringBuilder();
try {
this.print(duration, buffer);
} catch (IOException ex) {
throw new AssertionError(ex); // should never happen
}
return buffer.toString();
}
/**
* Creates a textual output of given temporal amount.
*
* @param threeten temporal amount
* @return textual representation of duration
* @throws IllegalArgumentException if some aspects of temporal amount
* prevents printing (for example mixed signs)
* @see Duration#from(TemporalAmount)
* @since 4.0
*/
/*[deutsch]
* Erzeugt eine textuelle Ausgabe des angegebenen Zeitbetrags.
*
* @param threeten temporal amount
* @return textual representation of duration
* @throws IllegalArgumentException if some aspects of temporal amount
* prevents printing (for example mixed signs)
* @see Duration#from(TemporalAmount)
* @since 4.0
*/
public String format(TemporalAmount threeten) {
return this.format(Duration.from(threeten));
}
/**
* Creates a textual output of given duration and writes to
* the buffer.
*
* @param duration duration object
* @param buffer I/O-buffer where the result is written to
* @throws IllegalArgumentException if some aspects of duration
* prevents printing (for example too many nanoseconds)
* @throws IOException if writing into buffer fails
* @since 1.2
*/
/*[deutsch]
* Erzeugt eine textuelle Ausgabe der angegebenen Dauer und
* schreibt sie in den Puffer.
*
* @param duration duration object
* @param buffer I/O-buffer where the result is written to
* @throws IllegalArgumentException if some aspects of duration
* prevents printing (for example too many nanoseconds)
* @throws IOException if writing into buffer fails
* @since 1.2
*/
public void print(
Duration> duration,
Appendable buffer
) throws IOException {
for (FormatItem item : this.items) {
item.print(duration, buffer);
}
}
/**
* Creates a textual output of given temporal amount and writes to
* the buffer.
*
* @param threeten temporal amount
* @param buffer I/O-buffer where the result is written to
* @throws IllegalArgumentException if some aspects of temporal amount
* prevents printing (for example mixed signs)
* @throws IOException if writing into buffer fails
* @see Duration#from(TemporalAmount)
* @since 4.0
*/
/*[deutsch]
* Erzeugt eine textuelle Ausgabe des angegebenen Zeitbetrags und
* schreibt sie in den Puffer.
*
* @param threeten temporal amount
* @param buffer I/O-buffer where the result is written to
* @throws IllegalArgumentException if some aspects of temporal amount
* prevents printing (for example mixed signs)
* @throws IOException if writing into buffer fails
* @see Duration#from(TemporalAmount)
* @since 4.0
*/
public void print(
TemporalAmount threeten,
Appendable buffer
) throws IOException {
this.print(Duration.from(threeten), buffer);
}
/**
* Equivalent to {@code parse(text, 0)}.
*
* @param text custom textual representation to be parsed
* @return parsed duration
* @throws ParseException (for example in case of mixed signs)
* @since 1.2
* @see #parse(CharSequence, int)
*/
/*[deutsch]
* Äquivalent zu {@code parse(text, 0)}.
*
* @param text custom textual representation to be parsed
* @return parsed duration
* @throws ParseException (for example in case of mixed signs)
* @since 1.2
* @see #parse(CharSequence, int)
*/
public Duration parse(CharSequence text) throws ParseException {
return this.parse(text, 0);
}
/**
* Analyzes given text according to format pattern and parses the
* text to a duration.
*
* @param text custom textual representation to be parsed
* @param offset start position for the parser
* @return parsed duration
* @throws ParseException (for example in case of mixed signs)
* @since 1.2
*/
/*[deutsch]
* Interpretiert den angegebenen Text entsprechend dem
* voreingestellten Formatmuster als Dauer.
*
* @param text custom textual representation to be parsed
* @param offset start position for the parser
* @return parsed duration
* @throws ParseException (for example in case of mixed signs)
* @since 1.2
*/
public Duration parse(
CharSequence text,
int offset
) throws ParseException {
int pos = offset;
Map