All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.time4j.Duration Maven / Gradle / Ivy

There is a newer version: 4.38
Show newest version
/*
 * -----------------------------------------------------------------------
 * 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 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 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 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 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 timespan, long[] sums ) { long months = sums[0]; long days = sums[1]; long secs = sums[2]; long nanos = sums[3]; for (Item 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
SymbolDescription
+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
SymbolBeschreibung
+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 unitsToValues = new HashMap<>(); for (FormatItem item : this.items) { int reply = item.parse(unitsToValues, text, pos); if (reply < 0) { throw new ParseException("Cannot parse: " + text, ~reply); } else { pos = reply; } } Long sign = unitsToValues.remove(SIGN_KEY); boolean negative = ((sign != null) && (sign.longValue() < 0)); Map map = new HashMap<>(); for (Object key : unitsToValues.keySet()) { if (this.type.isInstance(key)) { map.put(this.type.cast(key), unitsToValues.get(key)); } else { throw new ParseException( "Duration type mismatched: " + unitsToValues, pos); } } return Duration.create(map, negative); } private static boolean isSymbol(char c) { return ( ((c >= 'A') && (c <= 'Z')) || ((c >= 'a') && (c <= 'z')) ); } private static void addSymbol( char symbol, int count, int digits, List> stack ) { IsoUnit unit = getUnit(symbol); List items = stack.get(stack.size() - 1); if (symbol == 'f') { if (digits > 0) { throw new IllegalArgumentException("Combination of # and f-symbol not allowed."); } else { items.add(new FractionItem(0, count)); } } else { items.add(new NumberItem(0, count, count + digits, unit)); } } private static IsoUnit getUnit(char symbol) { IsoUnit unit; switch (symbol) { case 'I': unit = MILLENNIA; break; case 'C': unit = CENTURIES; break; case 'E': unit = DECADES; break; case 'Y': unit = YEARS; break; case 'Q': unit = QUARTERS; break; case 'M': unit = MONTHS; break; case 'W': unit = WEEKS; break; case 'D': unit = DAYS; break; case 'h': unit = HOURS; break; case 'm': unit = MINUTES; break; case 's': unit = SECONDS; break; case 'f': unit = NANOS; break; default: throw new IllegalArgumentException( "Unsupported pattern symbol: " + symbol); } return unit; } private static void addLiteral( char literal, List> stack ) { addLiteral(String.valueOf(literal), stack); } private static void addLiteral( String literal, List> stack ) { lastOn(stack).add(new LiteralItem(literal)); } private static void addPluralItem( String pluralInfo, List> stack ) { String[] parts = pluralInfo.split(":"); if ( (parts.length > 9) || (parts.length < 4) ) { throw new IllegalArgumentException( "Plural information has wrong format: " + pluralInfo); } IsoUnit unit; if (parts[0].length() == 1) { unit = getUnit(parts[0].charAt(0)); } else { throw new IllegalArgumentException( "Plural information has wrong symbol: " + pluralInfo); } String[] localInfo = parts[2].split("-|_"); String lang = localInfo[0]; Locale loc; if (localInfo.length > 1) { String country = localInfo[1]; if (localInfo.length > 2) { String variant = localInfo[2]; if (localInfo.length > 3) { throw new IllegalArgumentException( "Plural information has wrong locale: " + pluralInfo); } else { loc = new Locale(lang, country, variant); } } else { loc = new Locale(lang, country); } } else { loc = new Locale(lang); } Map pluralForms = new EnumMap<>(PluralCategory.class); PluralRules rules = PluralRules.of(loc, NumberType.CARDINALS); for (int i = 3; i < parts.length; i++) { String[] formInfo = parts[i].split("="); if (formInfo.length == 2) { pluralForms.put( PluralCategory.valueOf(formInfo[0]), formInfo[1]); } else { throw new IllegalArgumentException( "Plural information has wrong format: " + pluralInfo); } } if (pluralForms.isEmpty()) { throw new IllegalArgumentException( "Missing plural forms: " + pluralInfo); } else if (!pluralForms.containsKey(PluralCategory.OTHER)) { throw new IllegalArgumentException( "Missing plural category OTHER: " + pluralInfo); } lastOn(stack).add(new PluralItem(unit, parts[1], rules, pluralForms)); } private static void startOptionalSection(List> stack) { stack.add(new ArrayList<>()); } private static void endOptionalSection(List> stack) { int last = stack.size() - 1; if (last < 1) { throw new IllegalArgumentException( "Closing square bracket without open one."); } List items = stack.remove(last); stack.get(last - 1).add(new OptionalSectionItem(items)); } private static List lastOn(List> stack) { return stack.get(stack.size() - 1); } } private abstract static class FormatItem { //~ Instanzvariablen ---------------------------------------------- private final int reserved; //~ Konstruktoren ------------------------------------------------- FormatItem(int reserved){ super(); this.reserved = reserved; } //~ Methoden ------------------------------------------------------ boolean isZero(Duration duration) { return true; } abstract void print( Duration duration, Appendable buffer ) throws IOException; abstract int parse( Map unitsToValues, CharSequence text, int pos ); int getReserved() { return this.reserved; } abstract int getMinWidth(); abstract FormatItem update(int reserved); } private static class NumberItem extends FormatItem { //~ Instanzvariablen ---------------------------------------------- private final int minWidth; private final int maxWidth; private final IsoUnit unit; //~ Konstruktoren ------------------------------------------------- private NumberItem( int reserved, int minWidth, int maxWidth, IsoUnit unit ) { super(reserved); if (minWidth < 1 || minWidth > 18) { throw new IllegalArgumentException("Min width out of bounds: " + minWidth); } else if (maxWidth < minWidth) { throw new IllegalArgumentException("Max width smaller than min width."); } else if (maxWidth > 18) { throw new IllegalArgumentException("Max width out of bounds: " + maxWidth); } else if (unit == null) { throw new NullPointerException("Missing unit."); } this.minWidth = minWidth; this.maxWidth = maxWidth; this.unit = unit; } //~ Methoden ------------------------------------------------------ @Override void print( Duration duration, Appendable buffer ) throws IOException { String num = String.valueOf(duration.getPartialAmount(this.unit)); if (num.length() > this.maxWidth) { throw new IllegalArgumentException("Too many digits for: " + this.unit + " [" + duration + "]"); } for (int i = this.minWidth - num.length(); i > 0; i--) { buffer.append('0'); } buffer.append(num); } @Override int parse( Map unitsToValues, CharSequence text, int start ) { long total = 0; int pos = start; for (int i = start, n = text.length() - this.getReserved(); i < n; i++) { char c = text.charAt(i); if ((c >= '0') && (c <= '9')) { if (i - start >= this.maxWidth) { break; } int digit = (c - '0'); total = total * 10 + digit; pos++; } else { break; } } if (pos == start) { return ~start; // digits expected } Long value = Long.valueOf(total); Object old = unitsToValues.put(this.unit, value); if ((old == null) || old.equals(value)) { return pos; } else { return ~start; // ambivalent parsing } } @Override int getMinWidth() { return this.minWidth; } @Override FormatItem update(int reserved) { return new NumberItem(reserved, this.minWidth, this.maxWidth, this.unit); } @Override boolean isZero(Duration duration) { return (this.getAmount(duration) == 0); } long getAmount(Duration duration) { return duration.getPartialAmount(this.unit); } IsoUnit getUnit() { return this.unit; } } private static class FractionItem extends FormatItem { //~ Instanzvariablen ---------------------------------------------- private final int width; //~ Konstruktoren ------------------------------------------------- private FractionItem( int reserved, int width ) { super(reserved); if (width < 1 || width > 9) { throw new IllegalArgumentException( "Fraction width out of bounds: " + width); } this.width = width; } //~ Methoden ------------------------------------------------------ @Override void print( Duration duration, Appendable buffer ) throws IOException { String num = String.valueOf(duration.getPartialAmount(NANOS)); int len = num.length(); if (len > 9) { throw new IllegalArgumentException( "Too many nanoseconds, consider normalization: " + duration); } StringBuilder sb = new StringBuilder(); for (int i = 0; i < 9 - len; i++) { sb.append('0'); } sb.append(num); buffer.append(sb.toString().substring(0, this.width)); } @Override int parse( Map unitsToValues, CharSequence text, int start ) { StringBuilder fraction = new StringBuilder(); int pos = start; for ( int i = start, n = Math.min(text.length() - this.getReserved(), start + this.width); i < n; i++ ) { char c = text.charAt(i); if ((c >= '0') && (c <= '9')) { fraction.append(c); pos++; } else { break; } } if (pos == start) { return ~start; // digits expected } for (int i = 0, n = pos - start; i < 9 - n; i++) { fraction.append('0'); } Long value = Long.valueOf(Long.parseLong(fraction.toString())); Object old = unitsToValues.put(NANOS, value); if ((old == null) || old.equals(value)) { return pos; } else { return ~start; // ambivalent parsing } } @Override int getMinWidth() { return this.width; } @Override FormatItem update(int reserved) { return new FractionItem(reserved, width); } @Override boolean isZero(Duration duration) { return (duration.getPartialAmount(NANOS) == 0); } } private static class PluralItem extends FormatItem { //~ Instanzvariablen ---------------------------------------------- private final NumberItem numItem; private final FormatItem sepItem; private final PluralRules rules; private final Map pluralForms; private final int minWidth; //~ Konstruktoren ------------------------------------------------- private PluralItem( IsoUnit unit, String separator, PluralRules rules, Map pluralForms ) { super(0); this.numItem = new NumberItem(0, 1, 18, unit); this.sepItem = new LiteralItem(separator, true); this.rules = rules; this.pluralForms = pluralForms; int width = Integer.MAX_VALUE; for (String s : pluralForms.values()) { if (s.length() < width) { width = s.length(); } } this.minWidth = width; } private PluralItem( int reserved, NumberItem numItem, FormatItem sepItem, PluralRules rules, Map pluralForms, int minWidth ) { super(reserved); this.numItem = numItem; this.sepItem = sepItem; this.rules = rules; this.pluralForms = pluralForms; this.minWidth = minWidth; } //~ Methoden ------------------------------------------------------ @Override void print( Duration duration, Appendable buffer ) throws IOException { this.numItem.print(duration, buffer); this.sepItem.print(duration, buffer); PluralCategory category = this.rules.getCategory(this.numItem.getAmount(duration)); buffer.append(this.pluralForms.get(category)); } @Override int parse( Map unitsToValues, CharSequence text, int pos ) { int start = pos; pos = this.numItem.parse(unitsToValues, text, pos); if (pos < 0) { return pos; } pos = this.sepItem.parse(unitsToValues, text, pos); if (pos < 0) { return pos; } long value = unitsToValues.get(this.numItem.getUnit()).longValue(); String s = this.pluralForms.get(this.rules.getCategory(value)); int n = s.length(); if (pos + n > text.length() - this.getReserved()) { return ~start; } for (int i = 0; i < n; i++) { if (s.charAt(i) != text.charAt(pos + i)) { return ~start; } } return pos + n; } @Override int getMinWidth() { return this.minWidth; } @Override FormatItem update(int reserved) { return new PluralItem(reserved, this.numItem, this.sepItem, this.rules, this.pluralForms, this.minWidth); } @Override boolean isZero(Duration duration) { return this.numItem.isZero(duration); } } private static class SeparatorItem extends FormatItem { //~ Instanzvariablen ---------------------------------------------- private final char separator; private final char alt; //~ Konstruktoren ------------------------------------------------- private SeparatorItem( char separator, char alt ) { this(0, separator, alt); } private SeparatorItem( int reserved, char separator, char alt ) { super(reserved); this.separator = separator; this.alt = alt; } //~ Methoden ------------------------------------------------------ @Override void print( Duration duration, Appendable buffer ) throws IOException { buffer.append(this.separator); } @Override int parse( Map unitsToValues, CharSequence text, int start ) { if (start >= text.length() - this.getReserved()) { return ~start; // end of text } char c = text.charAt(start); if ((c != this.separator) && (c != this.alt)) { return ~start; // decimal separator expected } return start + 1; } @Override int getMinWidth() { return 1; } @Override FormatItem update(int reserved) { return new SeparatorItem(reserved, this.separator, this.alt); } } private static class LiteralItem extends FormatItem { //~ Instanzvariablen ---------------------------------------------- private final String literal; //~ Konstruktoren ------------------------------------------------- private LiteralItem(String literal) { this(literal, false); } private LiteralItem( String literal, boolean withEmpty ) { super(0); if (!withEmpty && literal.isEmpty()) { throw new IllegalArgumentException("Literal is empty."); } this.literal = literal; } private LiteralItem( int reserved, String literal ) { super(reserved); this.literal = literal; } //~ Methoden ------------------------------------------------------ @Override void print( Duration duration, Appendable buffer ) throws IOException { buffer.append(this.literal); } @Override int parse( Map unitsToValues, CharSequence text, int start ) { int end = start + this.literal.length(); if (end > text.length() - this.getReserved()) { return ~start; // end of line } for (int i = start; i < end; i++) { if (text.charAt(i) != this.literal.charAt(i - start)) { return ~start; // literal expected } } return end; } @Override int getMinWidth() { return this.literal.length(); } @Override FormatItem update(int reserved) { return new LiteralItem(reserved, this.literal); } } private static class SignItem extends FormatItem { //~ Instanzvariablen ---------------------------------------------- private final boolean always; //~ Konstruktoren ------------------------------------------------- private SignItem(boolean always) { super(0); this.always = always; } private SignItem( int reserved, boolean always ) { super(reserved); this.always = always; } //~ Methoden ------------------------------------------------------ @Override void print( Duration duration, Appendable buffer ) throws IOException { if (this.always) { buffer.append(duration.isNegative() ? '-' : '+'); } else if (duration.isNegative()) { buffer.append('-'); } } @Override int parse( Map unitsToValues, CharSequence text, int start ) { if (start >= text.length() - this.getReserved()) { if (this.always) { return ~start; // sign expected } else { Long old = unitsToValues.put(SIGN_KEY, Long.valueOf(1)); if ((old != null) && (old.longValue() != 1)) { return ~start; // mixed signs } return start; } } char c = text.charAt(start); Long sign = Long.valueOf(1); int ret = start; if (this.always) { if (c == '+') { ret = start + 1; } else if (c == '-') { sign = Long.valueOf(-1); ret = start + 1; } else { return ~start; // sign expected } } else { if (c == '+') { return ~start; // positive sign not allowed } else if (c == '-') { sign = Long.valueOf(-1); ret = start + 1; } } Long old = unitsToValues.put(SIGN_KEY, sign); if ((old != null) && (old.longValue() != sign.longValue())) { return ~start; // mixed signs } return ret; } @Override int getMinWidth() { return (this.always ? 1 : 0); } @Override FormatItem update(int reserved) { return new SignItem(reserved, this.always); } } private static class OptionalSectionItem extends FormatItem { //~ Instanzvariablen ---------------------------------------------- private final List items; //~ Konstruktoren ------------------------------------------------- private OptionalSectionItem(List items) { super(0); if (items.isEmpty()) { throw new IllegalArgumentException( "Optional section is empty."); } this.items = Collections.unmodifiableList(items); } //~ Methoden ------------------------------------------------------ @Override void print( Duration duration, Appendable buffer ) throws IOException { if (!this.isZero(duration)) { for (FormatItem item : this.items) { item.print(duration, buffer); } } } @Override int parse( Map unitsToValues, CharSequence text, int start ) { int pos = start; Map store = new HashMap<>(unitsToValues); for (FormatItem item : this.items) { int reply = item.parse(store, text, pos); if (reply < 0) { return start; } else { pos = reply; } } unitsToValues.putAll(store); return pos; } @Override int getMinWidth() { return 0; } @Override FormatItem update(int reserved) { List tmp = new ArrayList<>(this.items); int n = tmp.size(); for (int i = n - 1; i >= 0; i--) { FormatItem item = tmp.get(i); tmp.set(i, item.update(reserved)); reserved += item.getMinWidth(); } return new OptionalSectionItem(tmp); } @Override boolean isZero(Duration duration) { for (FormatItem item : this.items) { if (!item.isZero(duration)) { return false; } } return true; } } private static class Metric extends AbstractMetric> { //~ Konstruktoren ------------------------------------------------- @SafeVarargs private Metric(U... units) { super((units.length > 1), units); } //~ Methoden ------------------------------------------------------ @Override protected Duration createEmptyTimeSpan() { return ofZero(); } @Override protected Duration createTimeSpan( List> items, boolean negative ) { return new Duration<>(items, negative); } @Override @SuppressWarnings("unchecked") protected TimeSpan.Item resolve(TimeSpan.Item item) { IsoUnit unit = item.getUnit(); if (unit.equals(ClockUnit.MILLIS)) { return Item.of(Math.multiplyExact(item.getAmount(), 1_000_000L), (U) ClockUnit.NANOS); } else if (unit.equals(ClockUnit.MICROS)) { return Item.of(Math.multiplyExact(item.getAmount(), 1_000L), (U) ClockUnit.NANOS); } return item; } } private static class LengthComparator > implements Comparator> { //~ Instanzvariablen ---------------------------------------------- private final T base; //~ Konstruktoren ------------------------------------------------- private LengthComparator(T base) { super(); if (base == null) { throw new NullPointerException("Missing base time point."); } this.base = base; } //~ Methoden ------------------------------------------------------ @Override public int compare( Duration d1, Duration d2 ) { boolean sign1 = d1.isNegative(); boolean sign2 = d2.isNegative(); if (sign1 && !sign2) { return -1; } else if (!sign1 && sign2) { return 1; } else if (d1.isEmpty() && d2.isEmpty()) { return 0; } return this.base.plus(d1).compareTo(this.base.plus(d2)); } } private static class ApproximateNormalizer implements Normalizer { //~ Instanzvariablen ---------------------------------------------- private final boolean daysToWeeks; private final int steps; private final ClockUnit unit; //~ Konstruktoren ------------------------------------------------- ApproximateNormalizer(boolean daysToWeeks) { super(); this.daysToWeeks = daysToWeeks; this.steps = 1; this.unit = null; } ApproximateNormalizer( int steps, ClockUnit unit ) { super(); if (steps < 1) { throw new IllegalArgumentException( "Step width is not positive: " + steps); } else if (unit.compareTo(SECONDS) > 0) { throw new IllegalArgumentException("Unsupported unit."); } this.daysToWeeks = false; this.steps = steps; this.unit = unit; } //~ Methoden ------------------------------------------------------ @Override public Duration normalize(TimeSpan dur) { double total = 0.0; IsoUnit umax = null; for (int i = 0, n = dur.getTotalLength().size(); i < n; i++) { Item item = dur.getTotalLength().get(i); total += (item.getAmount() * item.getUnit().getLength()); if ((umax == null) && (item.getAmount() > 0)) { umax = item.getUnit(); } } if (umax == null) { return Duration.ofZero(); } // rounding applied IsoUnit u = ((this.unit == null) ? umax : this.unit); double len = u.getLength() * this.steps; long value = (long) (total + (len / 2.0)); // half-up int lint = (int) Math.floor(len); value = MathUtils.floorDivide(value, lint) * lint; // standard normalization int y, m, w, d, h, min = 0, s = 0; y = safeCast(value / YEARS.getLength()); value -= y * YEARS.getLength(); m = safeCast(value / MONTHS.getLength()); value -= m * MONTHS.getLength(); w = safeCast(value / WEEKS.getLength()); value -= w * WEEKS.getLength(); d = safeCast(value / DAYS.getLength()); value -= d * DAYS.getLength(); h = safeCast(value / HOURS.getLength()); if (HOURS.equals(this.unit)) { h = (h / this.steps); h *= this.steps; } else { value -= h * HOURS.getLength(); min = safeCast(value / MINUTES.getLength()); if (MINUTES.equals(this.unit)) { min = (min / this.steps); min *= this.steps; } else { // seconds value -= min * MINUTES.getLength(); s = safeCast(value / SECONDS.getLength()); s = (s / this.steps); s *= this.steps; } } d += w * 7; w = 0; // special case of max unit if (this.unit == null) { if (y > 0) { m = d = h = min = s = 0; } else if (m > 0) { d = h = min = s = 0; } else if (d > 0) { if ((d >= 7) && this.daysToWeeks) { w = (int) ((d + 3.5) / 7); d = 0; } h = min = s = 0; } else if (h > 0) { min = s = 0; } else if (min > 0) { s = 0; } if (w > 0) { IsoUnit weekUnit = CalendarUnit.WEEKS; return Duration.of(dur.isNegative() ? -w : w, weekUnit); } } // build Duration duration = Duration.ofPositive() .years(y).months(m).days(d) .hours(h).minutes(min).seconds(s).build(); if (dur.isNegative()) { duration = duration.inverse(); } return duration; } private static int safeCast(double num) { if (num < Integer.MIN_VALUE || num > Integer.MAX_VALUE) { throw new ArithmeticException("Out of range: " + num); } else { return (int) num; } } } }