From 5ce066cb729dfc11b43c7f13d7a181180976685d Mon Sep 17 00:00:00 2001 From: Meno Hochschild Date: Mon, 4 Nov 2019 13:33:21 +0100 Subject: [PATCH] reversible time metric --- .../src/main/java/net/time4j/Duration.java | 25 ++- .../src/main/java/net/time4j/MachineTime.java | 17 +- .../net/time4j/engine/AbstractDuration.java | 94 +++++--- .../net/time4j/engine/AbstractMetric.java | 205 ++++++++++++------ .../java/net/time4j/engine/TimeMetric.java | 25 +++ 5 files changed, 259 insertions(+), 107 deletions(-) diff --git a/time4j-android/src/main/java/net/time4j/Duration.java b/time4j-android/src/main/java/net/time4j/Duration.java index f578b7a..e627412 100644 --- a/time4j-android/src/main/java/net/time4j/Duration.java +++ b/time4j-android/src/main/java/net/time4j/Duration.java @@ -567,7 +567,7 @@ public static Duration ofClockUnits( * * @param generic unit type * @param units time units to be used in calculation - * @return immutable metric for calculating a duration in given units + * @return reversible immutable metric for calculating a duration in given units * @throws IllegalArgumentException if no time unit is given or * if there are unit duplicates */ @@ -604,7 +604,7 @@ public static Duration ofClockUnits( * * @param generic unit type * @param units time units to be used in calculation - * @return immutable metric for calculating a duration in given units + * @return reversible immutable metric for calculating a duration in given units * @throws IllegalArgumentException if no time unit is given or * if there are unit duplicates */ @@ -620,7 +620,7 @@ public static TimeMetric> in(U... units) { *

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, + * @return reversible immutable metric for calculating a duration in years, * months and days * @see #in(IsoUnit[]) in(U[]) * @see CalendarUnit#YEARS @@ -634,7 +634,7 @@ public static TimeMetric> in(U... units) { * Zeiteinheiten so weit wie möglich in große Einheiten * umgerechnet.

* - * @return immutable metric for calculating a duration in years, + * @return reversible immutable metric for calculating a duration in years, * months and days * @see #in(IsoUnit[]) in(U[]) * @see CalendarUnit#YEARS @@ -653,7 +653,7 @@ public static TimeMetric> inYearsMonthsDays *

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 + * @return reversible immutable metric for calculating a duration in clock units * @see #in(IsoUnit[]) in(U[]) * @see ClockUnit#HOURS * @see ClockUnit#MINUTES @@ -667,7 +667,7 @@ public static TimeMetric> inYearsMonthsDays * Zeiteinheiten so weit wie möglich in große Einheiten * umgerechnet.

* - * @return immutable metric for calculating a duration in clock units + * @return reversible immutable metric for calculating a duration in clock units * @see #in(IsoUnit[]) in(U[]) * @see ClockUnit#HOURS * @see ClockUnit#MINUTES @@ -686,7 +686,7 @@ public static TimeMetric> inClockUnits() { *

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 + * @return reversible immutable metric for calculating a duration in week-based years, weeks and days * @see #in(IsoUnit[]) in(U[]) * @see CalendarUnit#weekBasedYears() * @see CalendarUnit#WEEKS @@ -699,7 +699,7 @@ public static TimeMetric> inClockUnits() { *

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 + * @return reversible immutable metric for calculating a duration in week-based years, weeks and days * @see #in(IsoUnit[]) in(U[]) * @see CalendarUnit#weekBasedYears() * @see CalendarUnit#WEEKS @@ -730,7 +730,7 @@ public static TimeMetric> inWeekBasedUnits() * * @param tz timezone * @param units time units to be used in calculation - * @return zonal metric for calculating a duration in given units + * @return non-reversible 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 @@ -755,7 +755,7 @@ public static TimeMetric> inWeekBasedUnits() * * @param tz timezone * @param units time units to be used in calculation - * @return zonal metric for calculating a duration in given units + * @return non-reversible 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 @@ -4072,6 +4072,11 @@ Duration between( } + @Override + public TimeMetric> reversible() { + throw new UnsupportedOperationException("Not reversible."); + } + private int getOffset(ChronoEntity entity) { return this.tz.getStrategy().getOffset( diff --git a/time4j-android/src/main/java/net/time4j/MachineTime.java b/time4j-android/src/main/java/net/time4j/MachineTime.java index 0c7e307..91f9c65 100644 --- a/time4j-android/src/main/java/net/time4j/MachineTime.java +++ b/time4j-android/src/main/java/net/time4j/MachineTime.java @@ -101,34 +101,34 @@ public final class MachineTime new MachineTime(0, 0, UTC); /** - * Metric on the POSIX scale (without leap seconds). + * Reversible metric on the POSIX scale (without leap seconds). * * @since 2.0 */ /*[deutsch] - * Metrik auf der POSIX-Skala (ohne Schaltsekunden). + * Umkehrbare Metrik auf der POSIX-Skala (ohne Schaltsekunden). * * @since 2.0 */ public static final TimeMetric> ON_POSIX_SCALE = - new Metric(POSIX); + new Metric(POSIX); /** - *

Metric on the UTC scale (inclusive leap seconds).

+ *

Reversible metric on the UTC scale (inclusive leap seconds).

* *

Time points before 1972 are not supported.

* * @since 2.0 */ /*[deutsch] - *

Metrik auf der UTC-Skala (inklusive Schaltsekunden).

+ *

Umkehrbare Metrik auf der UTC-Skala (inklusive Schaltsekunden).

* *

Zeitpunkte vor 1972 werden nicht unterstützt.

* * @since 2.0 */ public static final TimeMetric> ON_UTC_SCALE = - new Metric(UTC); + new Metric(UTC); private static final long serialVersionUID = -4150291820807606229L; @@ -1737,6 +1737,11 @@ > MachineTime between( } + @Override + public TimeMetric> reversible() { + return this; + } + } } diff --git a/time4j-android/src/main/java/net/time4j/engine/AbstractDuration.java b/time4j-android/src/main/java/net/time4j/engine/AbstractDuration.java index fdbe814..59563d5 100644 --- a/time4j-android/src/main/java/net/time4j/engine/AbstractDuration.java +++ b/time4j-android/src/main/java/net/time4j/engine/AbstractDuration.java @@ -39,7 +39,7 @@ * given time point argument in the future. *
  • Negative duration => All contained time units will be * subtracted in the reversed order from the smallest to the largest - * units. Convertible units will be consolidated in one step. The + * units. Convertible units will be consolidated in one step. The * new time point is relative to given time point argument in the * past.
  • * @@ -71,15 +71,33 @@ * {@code t2.until(t1).equals(t1.until(t2).inverse()) == true} * * - *

    Note: The THIRD INVARIANCE - * {@code t1.plus(t1.until(t2)).minus(t1.until(t2)).equals(t1) == true} - * is often INVALID. A counter example where this invariance is - * violated is given with following dates: - * {t1, t2} = {[2011-05-31], [2011-07-01]}. But if the additional - * condition is required that the day of month is never after 28th - * of a month then this third invariance can be guaranteed. - * Therefore it is recommended to avoid dates near the end of - * month in addition.

    + *

    Following condition only holds if either the day-of-month of any involved date is + * smaller than 28 or if a reversible metric is used:

    + * + *
    • THIRD INVARIANCE: + * {@code t2.minus(t1.until(t2)).equals(t1) == true} + *
    + * + *

    Note: Usually the third invariance is NOT valid. A counter example + * is given with following dates: {t1, t2} = {[2011-03-31], [2011-07-01]} => P3M1D (using + * the standard metric) because of [2011-07-01] - P3M1D = [2011-03-30]. Example for using + * a reversible metric:

    + * + *
    + *     PlainDate d1 = PlainDate.of(2011, 3, 31);
    + *     PlainDate d2 = PlainDate.of(2011, 7, 1);
    + *
    + *     TimeMetric<CalendarUnit, Duration<CalendarUnit>> metric =
    + *        Duration.inYearsMonthsDays().reversible();
    + *     Duration<CalendarUnit> duration =
    + *        metric.between(d1, d2); // P2M31D
    + *     Duration<CalendarUnit> invDur =
    + *        metric.between(d2, d1); // -P2M31D
    + *
    + *     boolean firstInvariance = d1.plus(duration).equals(d2); // true
    + *     boolean secondInvariance = invDur.equals(duration.inverse()); // true
    + *     boolean thirdInvariance = d2.minus(duration).equals(d1); // true
    + * 
    * *
    * @@ -100,7 +118,7 @@ * equivalent relation holds (paying attention to non-commutativity * and given the side conditions to compute the duration without * remainder completely and to consider a minus-operation as - * equalizing a plus-operation (with t1-day-of-month <= 28):

    + * equalizing a plus-operation:

    * *
      *
    • [t1] - [months] - [days] = [t2]
    • @@ -124,15 +142,16 @@ * even if the day of month is the first day of month: t2.minus(P1M30D) * would not yield t1 but [2013-01-29]. Surely, the sign-dependent * execution of addition steps cannot completely guarantee the third - * invariance but it can guarantee it at least for all days in original - * date until the 28th of month.

      + * invariance in case of factory-created durations but it can guarantee + * it at least for all days in original date until the 28th of month.

      * *

      Furthermore the specified algorithm ensures the second invariance * {@code Duration([t1, t2]) = -Duration([t2, t1])} which expresses - * a physical property of any duration. The second invariance means - * that the sign of a duration can only qualify if the first time point - * is before the second time point or other way around. The sign must - * not qualify the always positive length of a duration itself however.

      + * a physical property of any duration as a directed temporal amount. + * The second invariance means that the sign of a duration can only + * qualify if the first time point is before the second time point or + * other way around. The sign must not qualify the always positive length + * of a duration itself however.

      *
    * * @author Meno Hochschild @@ -191,14 +210,34 @@ * {@code t2.until(t1).equals(t1.until(t2).inverse()) == true} * * - *

    Zu beachten: Allgemein gilt die DRITTE INVARIANZ - * {@code t1.plus(t1.until(t2)).minus(t1.until(t2)).equals(t1) == true} - * NICHT. Ein Gegenbeispiel ist mit {t1, t2} = {[2011-05-31], [2011-07-01]} - * gegeben. Wird aber hier als zusätzliche Randbedingung verlangt, - * daß etwa der Tag des Monats nicht nach dem 28. liegt, dann gilt - * diese Invarianz doch noch. Es wid daher empfohlen, bei der Addition von - * Monaten möglichst Datumsangaben zu vermeiden, die am Ende eines - * Monats liegen.

    + *

    Die folgende Invarianzbedingung gilt nur, wenn entweder der betroffene + * Tag des Monats in den beteiligten Datumsangaben kleiner als 28 ist oder + * wenn eine umkehrbare Metrik verwendet wird:

    + * + *
    • DRITTE INVARIANZ: + * {@code t2.minus(t1.until(t2)).equals(t1) == true} + *
    + * + *

    Note: Gewöhnlich ist die dritte Invarianzbedingung nicht gültig. + * Ein Gegenbeispiel ist mit folgenden Werten gegeben: {t1, t2} = {[2011-03-31], [2011-07-01]} + * => P3M1D (im Fall der Standardmetrik) wegen [2011-07-01] - P3M1D = [2011-03-30]. Beispiel + * zur Verwendung einer umkehrbaren Metrik:

    + * + *
    + *     PlainDate d1 = PlainDate.of(2011, 3, 31);
    + *     PlainDate d2 = PlainDate.of(2011, 7, 1);
    + *
    + *     TimeMetric<CalendarUnit, Duration<CalendarUnit>> metric =
    + *        Duration.inYearsMonthsDays().reversible();
    + *     Duration<CalendarUnit> duration =
    + *        metric.between(d1, d2); // P2M31D
    + *     Duration<CalendarUnit> invDur =
    + *        metric.between(d2, d1); // -P2M31D
    + *
    + *     boolean firstInvariance = d1.plus(duration).equals(d2); // true
    + *     boolean secondInvariance = invDur.equals(duration.inverse()); // true
    + *     boolean thirdInvariance = d2.minus(duration).equals(d1); // true
    + * 
    * *
    * @@ -240,8 +279,9 @@ * Tag des Monats im Ausgangsdatum der erste ist. Denn: t2.minus(P1M30D) * ergäbe dann nicht t1, sondern [2013-01-29]. Zwar kann die * vorzeichenabhängige Ausführung der Rechenschritte nicht - * vollständig die dritte Invarianz garantieren, aber wenigstens - * doch für alle Tage im Ausgangsdatum bis zum 28. eines Monats.

    + * vollständig die dritte Invarianz garantieren (im Fall von nicht + * berechneten Dauer-Objekten), aber wenigstens doch für alle Tage + * im Ausgangsdatum bis zum 28. eines Monats.

    * *

    Gleichzeitig hilft der Time4J-Algorithmus die Invarianz * {@code Duration([t1, t2]) = -Duration([t2, t1])} einzuhalten, diff --git a/time4j-android/src/main/java/net/time4j/engine/AbstractMetric.java b/time4j-android/src/main/java/net/time4j/engine/AbstractMetric.java index 6b1f1ba..fcb48ba 100644 --- a/time4j-android/src/main/java/net/time4j/engine/AbstractMetric.java +++ b/time4j-android/src/main/java/net/time4j/engine/AbstractMetric.java @@ -40,6 +40,9 @@ * the duration will be normalized such that small units will be * converted to larger units if possible.

    * + *

    This metric can be changed to a reversible one by calling {@code reversible()} + * (third invariance in {@code AbstractDuration}.

    + * * @param generic type of time unit ({@code ChronoUnit}) * @param

    generic type of duration result * @author Meno Hochschild @@ -59,18 +62,21 @@ * in der Regel normalisiert, also kleine Zeiteinheiten so weit wie * möglich in große Einheiten umgerechnet.

    * + *

    Diese Metrik kann mittels Aufruf von {@code reversible()} umkehrbar gemacht werden + * (dritte Invarianzbedingung in {@code AbstractDuration}.

    + * * @param generic type of time unit ({@code ChronoUnit}) * @param

    generic type of duration result * @author Meno Hochschild * @see AbstractDuration */ -public abstract class AbstractMetric - > - implements TimeMetric, Comparator { +public abstract class AbstractMetric> + implements TimeMetric, Comparator { //~ Statische Felder/Initialisierungen -------------------------------- private static final int MIO = 1000000; + private static final double LENGTH_OF_FORTNIGHT = 86400.0 * 14; //~ Instanzvariablen -------------------------------------------------- @@ -105,9 +111,10 @@ public abstract class AbstractMetric * @throws IllegalArgumentException if any time unit is given more than * once or if there is no time unit at all */ + @SafeVarargs protected AbstractMetric( - boolean normalizing, - U... units + boolean normalizing, + U... units ) { super(); @@ -120,9 +127,11 @@ protected AbstractMetric( Collections.sort(list, this); for (int i = 0, n = list.size(); i < n; i++) { + U unit = list.get(i); + for (int j = i + 1; j < n; j++) { - if (list.get(i).equals(list.get(j))) { - throw new IllegalArgumentException("Duplicate unit: " + list.get(i)); + if (unit.equals(list.get(j))) { + throw new IllegalArgumentException("Duplicate unit: " + unit); } } } @@ -158,8 +167,19 @@ public int compare(U u1, U u2) { @Override public > P between( - T start, - T end + T start, + T end + ) { + + return this.between(start, end, -1); + + } + + @SuppressWarnings("unchecked") + private > P between( + T start, + T end, + int monthIndex ) { if (end.equals(start)) { @@ -180,26 +200,19 @@ public > P between( List> resultList = new ArrayList>(10); TimeAxis engine = start.getChronology(); - U unit = null; - long amount = 0; int index = 0; int endIndex = this.sortedUnits.size(); while (index < endIndex) { - // Nächste Subtraktion vorbereiten - if (amount > 0) { - t1 = t1.plus(amount, unit); - } - // Aktuelle Zeiteinheit bestimmen - unit = this.sortedUnits.get(index); + U unit = this.sortedUnits.get(index); if ( - (this.getLength(engine, unit) < 1.0) - && (index < endIndex - 1) - ) { - amount = 0; // Millis oder Mikros vor Nanos nicht berechnen (maximal eine fraktionale Einheit) + (this.getLength(engine, unit) < 1.0) + && (index < endIndex - 1) + ) { + // Millis oder Mikros vor Nanos nicht berechnen (maximal eine fraktionale Einheit) } else { // konvertierbare Einheiten zusammenfassen int k = index + 1; @@ -209,9 +222,9 @@ public > P between( U nextUnit = this.sortedUnits.get(k); factor *= this.getFactor(engine, unit, nextUnit); if ( - (factor < MIO) - && engine.isConvertible(unit, nextUnit) - ) { + (factor < MIO) + && engine.isConvertible(unit, nextUnit) + ) { unit = nextUnit; } else { break; @@ -221,15 +234,29 @@ public > P between( index = k - 1; // Differenz in einer Einheit berechnen - amount = t1.until(t2, unit); - - if (amount > 0) { - resultList.add(this.resolve(TimeSpan.Item.of(amount, unit))); - } else if (amount < 0) { + long amount = t1.until(t2, unit); + + if (amount < 0) { throw new IllegalStateException( - "Implementation error: " - + "Cannot compute timespan " - + "due to illegal negative timespan amounts."); + "Implementation error: " + + "Cannot compute timespan " + + "due to illegal negative timespan amounts."); + } + + // Dauerkomponente erzeugen, mit Monatsendekorrektur ggf. Betrag verkleinern + while (amount > 0) { + T temp = t1.plus(amount, unit); + if ( + (index > monthIndex) + || (index == endIndex - 1) + || temp.minus(amount, unit).equals(t1) + ) { + t1 = temp; + resultList.add(this.resolve(TimeSpan.Item.of(amount, unit))); + break; + } else { + amount--; // avoid possible end-of-month-correction + } } } index++; @@ -243,6 +270,13 @@ public > P between( } + @Override + public TimeMetric reversible() { + + return new ReversalMetric(this); + + } + /** *

    Creates an empty time span.

    * @@ -273,8 +307,8 @@ public > P between( * @return new time span */ protected abstract P createTimeSpan( - List> items, - boolean negative + List> items, + boolean negative ); /** @@ -297,10 +331,11 @@ protected TimeSpan.Item resolve(TimeSpan.Item item) { } + @SuppressWarnings("unchecked") private > void normalize( - TimeAxis engine, - List sortedUnits, - List> resultList + TimeAxis engine, + List sortedUnits, + List> resultList ) { Comparator comparator = engine.unitComparator(); @@ -311,11 +346,11 @@ private > void normalize( U nextUnit = sortedUnits.get(i - 1); long factor = this.getFactor(engine, nextUnit, currentUnit); if ( - (factor < MIO) - && engine.isConvertible(nextUnit, currentUnit) - ) { + (factor < MIO) + && engine.isConvertible(nextUnit, currentUnit) + ) { TimeSpan.Item currentItem = - getItem(resultList, currentUnit); + getItem(resultList, currentUnit); if (currentItem != null) { long currentValue = currentItem.getAmount(); long overflow = currentValue / factor; @@ -327,17 +362,15 @@ private > void normalize( putItem(resultList, comparator, a, currentUnit); } TimeSpan.Item nextItem = - getItem(resultList, nextUnit); + getItem(resultList, nextUnit); if (nextItem == null) { putItem(resultList, comparator, overflow, nextUnit); } else { putItem( - resultList, - comparator, - MathUtils.safeAdd( - nextItem.getAmount(), - overflow), - nextUnit + resultList, + comparator, + MathUtils.safeAdd(nextItem.getAmount(), overflow), + nextUnit ); } } @@ -349,8 +382,8 @@ private > void normalize( } private static TimeSpan.Item getItem( - List> items, - U unit + List> items, + U unit ) { for (int i = 0, n = items.size(); i < n; i++) { @@ -365,10 +398,10 @@ private static TimeSpan.Item getItem( } private static void putItem( - List> items, - Comparator comparator, - long amount, - U unit + List> items, + Comparator comparator, + long amount, + U unit ) { TimeSpan.Item item = TimeSpan.Item.of(amount, unit); @@ -381,9 +414,9 @@ private static void putItem( items.set(i, item); return; } else if ( - (insert == i) - && (comparator.compare(u, unit) < 0) - ) { + (insert == i) + && (comparator.compare(u, unit) < 0) + ) { insert++; } } @@ -393,8 +426,8 @@ private static void putItem( } private static void removeItem( - List> items, - U unit + List> items, + U unit ) { for (int i = 0, n = items.size(); i < n; i++) { @@ -407,9 +440,9 @@ private static void removeItem( } private > long getFactor( - TimeAxis engine, - U unit1, - U unit2 + TimeAxis engine, + U unit1, + U unit2 ) { double d1 = this.getLength(engine, unit1); @@ -418,13 +451,57 @@ private > long getFactor( } + @SuppressWarnings("unchecked") private > double getLength( - TimeAxis engine, - U unit + TimeAxis engine, + U unit ) { return engine.getLength(unit); } + //~ Innere Klassen ---------------------------------------------------- + + private static class ReversalMetric> + implements TimeMetric { + + //~ Instanzvariablen ---------------------------------------------- + + private final AbstractMetric delegate; + private final int monthIndex; + + //~ Konstruktoren ------------------------------------------------- + + private ReversalMetric(AbstractMetric delegate) { + super(); + + this.delegate = delegate; + int mi = -1; + + for (int i = this.delegate.sortedUnits.size() - 1; i >= 0; i--) { + U unit = this.delegate.sortedUnits.get(i); + + if (Double.compare(unit.getLength(), LENGTH_OF_FORTNIGHT) > 0) { + mi = i; + break; + } + } + + this.monthIndex = mi; + } + + //~ Methoden ------------------------------------------------------ + + @Override + public > P between(T start, T end) { + return this.delegate.between(start, end, this.monthIndex); + } + + @Override + public TimeMetric reversible() { + return this; + } + } + } diff --git a/time4j-android/src/main/java/net/time4j/engine/TimeMetric.java b/time4j-android/src/main/java/net/time4j/engine/TimeMetric.java index 907a879..98fc79c 100644 --- a/time4j-android/src/main/java/net/time4j/engine/TimeMetric.java +++ b/time4j-android/src/main/java/net/time4j/engine/TimeMetric.java @@ -71,4 +71,29 @@ > P between( T end ); + /** + *

    Obtains a modified metric which has reversible characteristics.

    + * + *

    Usually metrics are not reversible by default. The default implementation throws + * an UnsupportedOperationException. Overriding implementations should + * document the details of reversal characteristics.

    + * + * @return modified reversible time metric + * @throws UnsupportedOperationException if not supported + * @since 5.5 + */ + /*[deutsch] + *

    Liefert eine veränderte Metrik mit umkehrbaren Eigenschaften.

    + * + *

    Normalerweise sind Metriken nicht per se umkehrbar. Die Standardimplementierung + * wirft eine UnsupportedOperationException. Implementierungen, die diese + * Methode überschreiben, sollten die Einzelheiten der Umkehrbarkeit einer Metrik + * beschreiben.

    + * + * @return modified reversible time metric + * @throws UnsupportedOperationException if not supported + * @since 5.5 + */ + TimeMetric reversible(); + }