You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@groovy.apache.org by pa...@apache.org on 2018/03/21 16:44:57 UTC

[03/14] groovy git commit: Fix upto/downto with custom TemporalUnit arg edge cases. Add Period and Duration methods.

Fix upto/downto with custom TemporalUnit arg edge cases. Add Period and Duration methods.


Project: http://git-wip-us.apache.org/repos/asf/groovy/repo
Commit: http://git-wip-us.apache.org/repos/asf/groovy/commit/92378be9
Tree: http://git-wip-us.apache.org/repos/asf/groovy/tree/92378be9
Diff: http://git-wip-us.apache.org/repos/asf/groovy/diff/92378be9

Branch: refs/heads/GROOVY_2_6_X
Commit: 92378be9fce96d6cd125966ad926854b82d0bcee
Parents: 99db9bf
Author: Joe Wolf <jo...@gmail.com>
Authored: Sun Mar 4 16:55:56 2018 -0500
Committer: paulk <pa...@asert.com.au>
Committed: Thu Mar 22 00:41:46 2018 +1000

----------------------------------------------------------------------
 .../groovy/runtime/DateTimeGroovyMethods.java   | 136 ++++++++++--
 .../runtime/DefaultGroovyStaticMethods.java     |  57 +++++-
 src/test/groovy/DateTimeTest.groovy             | 205 ++++++++++++++-----
 3 files changed, 327 insertions(+), 71 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/groovy/blob/92378be9/src/main/java/org/codehaus/groovy/runtime/DateTimeGroovyMethods.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/codehaus/groovy/runtime/DateTimeGroovyMethods.java b/src/main/java/org/codehaus/groovy/runtime/DateTimeGroovyMethods.java
index 947e368..97d7df7 100644
--- a/src/main/java/org/codehaus/groovy/runtime/DateTimeGroovyMethods.java
+++ b/src/main/java/org/codehaus/groovy/runtime/DateTimeGroovyMethods.java
@@ -23,12 +23,17 @@ import groovy.lang.GroovyRuntimeException;
 
 import java.time.*;
 import java.time.chrono.ChronoLocalDate;
+import java.time.chrono.ChronoPeriod;
 import java.time.format.DateTimeFormatter;
 import java.time.format.FormatStyle;
 import java.time.format.TextStyle;
 import java.time.temporal.*;
 import java.util.*;
 
+import static java.time.temporal.ChronoUnit.DAYS;
+import static java.time.temporal.ChronoUnit.MONTHS;
+import static java.time.temporal.ChronoUnit.YEARS;
+
 /**
  * This class defines new Groovy methods which appear on normal JDK
  * Date/Time API (java.time) classes inside the Groovy environment.
@@ -47,9 +52,9 @@ public class DateTimeGroovyMethods {
      */
     private static Map<Class<? extends Temporal>, TemporalUnit> DEFAULT_UNITS = new HashMap<>();
     static {
-        DEFAULT_UNITS.put(ChronoLocalDate.class, ChronoUnit.DAYS);
-        DEFAULT_UNITS.put(YearMonth.class, ChronoUnit.MONTHS);
-        DEFAULT_UNITS.put(Year.class, ChronoUnit.YEARS);
+        DEFAULT_UNITS.put(ChronoLocalDate.class, DAYS);
+        DEFAULT_UNITS.put(YearMonth.class, MONTHS);
+        DEFAULT_UNITS.put(Year.class, YEARS);
     }
 
     /**
@@ -69,14 +74,15 @@ public class DateTimeGroovyMethods {
      * Truncates a nanosecond value to milliseconds. No rounding.
      */
     private static int millisFromNanos(int nanos) {
-       return nanos / 1_000_000;
+        return nanos / 1_000_000;
     }
 
     /* ******** java.time.temporal.Temporal extension methods ******** */
 
     /**
-     * Iterates from the this to {@code to}, inclusive, incrementing by one unit each iteration, calling the
-     * closure once per iteration. The closure may accept a single {@link java.time.temporal.Temporal} argument.
+     * Iterates from this to the {@code to} {@link java.time.temporal.Temporal}, inclusive, incrementing by one
+     * unit each iteration, calling the closure once per iteration. The closure may accept a single
+     * {@link java.time.temporal.Temporal} argument.
      * <p>
      * The particular unit incremented by depends on the specific sub-type of {@link java.time.temporal.Temporal}.
      * Most sub-types use a unit of {@link java.time.temporal.ChronoUnit#SECONDS} except for
@@ -97,8 +103,8 @@ public class DateTimeGroovyMethods {
     }
 
     /**
-     * Iterates from this to {@code to}, inclusive, incrementing by one {@code unit} each iteration,
-     * calling the closure once per iteration. The closure may accept a single
+     * Iterates from this to the {@code to} {@link java.time.temporal.Temporal}, inclusive, incrementing by one
+     * {@code unit} each iteration, calling the closure once per iteration. The closure may accept a single
      * {@link java.time.temporal.Temporal} argument.
      *
      * If the unit is too large to iterate to the second Temporal exactly, such as iterating from two LocalDateTimes
@@ -113,8 +119,8 @@ public class DateTimeGroovyMethods {
      * @since 3.0
      */
     public static void upto(Temporal from, Temporal to, TemporalUnit unit, Closure closure) {
-        if (from.until(to, unit) >= 0) {
-            for (Temporal i = from; i.until(to, unit) >= 0; i = i.plus(1, unit)) {
+        if (isUptoEligible(from, to)) {
+            for (Temporal i = from; isUptoEligible(i, to); i = i.plus(1, unit)) {
                 closure.call(i);
             }
         } else {
@@ -124,10 +130,26 @@ public class DateTimeGroovyMethods {
     }
 
     /**
+     * Returns true if the {@code from} can be iterated up to {@code to}.
+     */
+    private static boolean isUptoEligible(Temporal from, Temporal to) {
+        switch ((ChronoUnit) defaultUnitFor(from)) {
+            case YEARS:
+                return isNonnegative(DefaultGroovyStaticMethods.between(null, (Year) from, (Year) to));
+            case MONTHS:
+                return isNonnegative(DefaultGroovyStaticMethods.between(null, (YearMonth) from, (YearMonth) to));
+            case DAYS:
+                return isNonnegative(ChronoPeriod.between((ChronoLocalDate) from, (ChronoLocalDate) to));
+            default:
+                return isNonnegative(Duration.between(from, to));
+        }
+    }
+
+    /**
      * Iterates from this to the {@code to} {@link java.time.temporal.Temporal}, inclusive, decrementing by one
      * unit each iteration, calling the closure once per iteration. The closure may accept a single
      * {@link java.time.temporal.Temporal} argument.
-     *
+     * <p>
      * The particular unit decremented by depends on the specific sub-type of {@link java.time.temporal.Temporal}.
      * Most sub-types use a unit of {@link java.time.temporal.ChronoUnit#SECONDS} except for
      * <ul>
@@ -163,8 +185,8 @@ public class DateTimeGroovyMethods {
      * @since 3.0
      */
     public static void downto(Temporal from, Temporal to, TemporalUnit unit, Closure closure) {
-        if (from.until(to, unit) <= 0) {
-            for (Temporal i = from; i.until(to, unit) <= 0; i = i = i.minus(1, unit)) {
+        if (isDowntoEligible(from, to)) {
+            for (Temporal i = from; isDowntoEligible(i, to); i = i.minus(1, unit)) {
                 closure.call(i);
             }
         } else {
@@ -174,6 +196,22 @@ public class DateTimeGroovyMethods {
     }
 
     /**
+     * Returns true if the {@code from} can be iterated down to {@code to}.
+     */
+    private static boolean isDowntoEligible(Temporal from, Temporal to) {
+        switch ((ChronoUnit) defaultUnitFor(from)) {
+            case YEARS:
+                return isNonpositive(DefaultGroovyStaticMethods.between(null, (Year) from, (Year) to));
+            case MONTHS:
+                return isNonpositive(DefaultGroovyStaticMethods.between(null, (YearMonth) from, (YearMonth) to));
+            case DAYS:
+                return isNonpositive(ChronoPeriod.between((ChronoLocalDate) from, (ChronoLocalDate) to));
+            default:
+                return isNonpositive(Duration.between(from, to));
+        }
+    }
+
+    /**
      * Returns a {@link java.time.Duration} of time between this (inclusive) and {@code other} (exclusive).
      *
      * @param self  a Temporal
@@ -315,6 +353,39 @@ public class DateTimeGroovyMethods {
         return self.dividedBy(scalar);
     }
 
+    /**
+     * Returns true if this duration is positive, excluding zero.
+     *
+     * @param self a Duration
+     * @return true if positive
+     * @since 3.0
+     */
+    public static boolean isPositive(final Duration self) {
+        return !self.isZero() && !self.isNegative();
+    }
+
+    /**
+     * Returns true if this duration is zero or positive.
+     *
+     * @param self a Duration
+     * @return true if nonnegative
+     * @since 3.0
+     */
+    public static boolean isNonnegative(final Duration self) {
+        return self.isZero() || !self.isNegative();
+    }
+
+    /**
+     * Returns true if this duration is zero or negative.
+     *
+     * @param self a Duration
+     * @return true if nonpositive
+     * @since 3.0
+     */
+    public static boolean isNonpositive(final Duration self) {
+        return self.isZero() || self.isNegative();
+    }
+
     /* ******** java.time.Instant extension methods ******** */
 
     /**
@@ -613,7 +684,7 @@ public class DateTimeGroovyMethods {
      * @since 3.0
      */
     public static LocalDateTime clearTime(final LocalDateTime self) {
-        return self.truncatedTo(ChronoUnit.DAYS);
+        return self.truncatedTo(DAYS);
     }
 
     /**
@@ -959,7 +1030,7 @@ public class DateTimeGroovyMethods {
      * @since 3.0
      */
     public static OffsetDateTime clearTime(final OffsetDateTime self) {
-        return self.truncatedTo(ChronoUnit.DAYS);
+        return self.truncatedTo(DAYS);
     }
 
     /**
@@ -1254,6 +1325,39 @@ public class DateTimeGroovyMethods {
         return self.multipliedBy(scalar);
     }
 
+    /**
+     * Returns true if this period is positive, excluding zero.
+     *
+     * @param self a ChronoPeriod
+     * @return true if positive
+     * @since 3.0
+     */
+    public static boolean isPositive(final ChronoPeriod self) {
+        return !self.isZero() && !self.isNegative();
+    }
+
+    /**
+     * Returns true if this period is zero or positive.
+     *
+     * @param self a ChronoPeriod
+     * @return true if nonnegative
+     * @since 3.0
+     */
+    public static boolean isNonnegative(final ChronoPeriod self) {
+        return self.isZero() || !self.isNegative();
+    }
+
+    /**
+     * Returns true if this period is zero or negative.
+     *
+     * @param self a ChronoPeriod
+     * @return true if nonpositive
+     * @since 3.0
+     */
+    public static boolean isNonpositive(final ChronoPeriod self) {
+        return self.isZero() || self.isNegative();
+    }
+
     /* ******** java.time.Year extension methods ******** */
 
     /**
@@ -1514,7 +1618,7 @@ public class DateTimeGroovyMethods {
      * @since 3.0
      */
     public static ZonedDateTime clearTime(final ZonedDateTime self) {
-        return self.truncatedTo(ChronoUnit.DAYS);
+        return self.truncatedTo(DAYS);
     }
 
     /**

http://git-wip-us.apache.org/repos/asf/groovy/blob/92378be9/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyStaticMethods.java
----------------------------------------------------------------------
diff --git a/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyStaticMethods.java b/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyStaticMethods.java
index 55d3d07..0ad2f33 100644
--- a/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyStaticMethods.java
+++ b/src/main/java/org/codehaus/groovy/runtime/DefaultGroovyStaticMethods.java
@@ -299,15 +299,15 @@ public class DefaultGroovyStaticMethods {
         return tempFile;
     }
 
-  /**
-   * Get the current time in seconds
-   *
-   * @param self   placeholder variable used by Groovy categories; ignored for default static methods
-   * @return  the difference, measured in seconds, between
-   *          the current time and midnight, January 1, 1970 UTC.
-   * @see     System#currentTimeMillis()
-   */
-  public static long currentTimeSeconds(System self){
+    /**
+     * Get the current time in seconds
+     *
+     * @param self   placeholder variable used by Groovy categories; ignored for default static methods
+     * @return  the difference, measured in seconds, between
+     *          the current time and midnight, January 1, 1970 UTC.
+     * @see     System#currentTimeMillis()
+     */
+    public static long currentTimeSeconds(System self){
     return System.currentTimeMillis() / 1000;
   }
 
@@ -476,4 +476,43 @@ public class DefaultGroovyStaticMethods {
         return DateTimeGroovyMethods.getOffset(ZoneId.systemDefault());
     }
 
+    /**
+     * Obtains a Period consisting of the number of years between two {@link java.time.Year} instances.
+     * The months and days of the Period will be zero.
+     * The result of this method can be a negative period if the end is before the start.
+     *
+     * @param type           placeholder variable used by Groovy categories; ignored for default static methods
+     * @param startInclusive the start {@link java.time.Year}, inclusive, not null
+     * @param endExclusive   the end {@link java.time.Year}, exclusive, not null
+     * @return a Period between the years
+     * @see java.time.Period#between(LocalDate, LocalDate)
+     */
+    public static Period between(final Period type, Year startInclusive, Year endExclusive) {
+        MonthDay now = MonthDay.of(Month.JANUARY, 1);
+        return Period.between(
+                DateTimeGroovyMethods.leftShift(startInclusive, now),
+                DateTimeGroovyMethods.leftShift(endExclusive, now))
+                .withDays(0)
+                .withMonths(0);
+    }
+
+    /**
+     * Obtains a Period consisting of the number of years and months between two {@link java.time.YearMonth} instances.
+     * The days of the Period will be zero.
+     * The result of this method can be a negative period if the end is before the start.
+     *
+     * @param type           placeholder variable used by Groovy categories; ignored for default static methods
+     * @param startInclusive the start {@link java.time.YearMonth}, inclusive, not null
+     * @param endExclusive   the end {@link java.time.YearMonth}, exclusive, not null
+     * @return a Period between the year/months
+     * @see java.time.Period#between(LocalDate, LocalDate)
+     */
+    public static Period between(final Period type, YearMonth startInclusive, YearMonth endExclusive) {
+        int dayOfMonth = 1;
+        return Period.between(
+                DateTimeGroovyMethods.leftShift(startInclusive, dayOfMonth),
+                DateTimeGroovyMethods.leftShift(endExclusive, dayOfMonth))
+                .withDays(0);
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/groovy/blob/92378be9/src/test/groovy/DateTimeTest.groovy
----------------------------------------------------------------------
diff --git a/src/test/groovy/DateTimeTest.groovy b/src/test/groovy/DateTimeTest.groovy
index 088f460..46eae17 100644
--- a/src/test/groovy/DateTimeTest.groovy
+++ b/src/test/groovy/DateTimeTest.groovy
@@ -170,6 +170,22 @@ class DateTimeTest extends GroovyTestCase {
         assert (duration * 2).seconds == 120
     }
 
+    void testDurationIsPositiveIsNonnegativeIsNonpositive() {
+        def pos = Duration.ofSeconds(10)
+        assert pos.isPositive() == true
+        assert pos.isNonpositive() == false
+        assert pos.isNonnegative() == true
+
+        def neg = Duration.ofSeconds(-10)
+        assert neg.isPositive() == false
+        assert neg.isNonpositive() == true
+        assert neg.isNonnegative() == false
+
+        assert Duration.ZERO.isPositive() == false
+        assert Duration.ZERO.isNonpositive() == true
+        assert Duration.ZERO.isNonnegative() == true
+    }
+
     void testPeriodPositiveNegative() {
         def positivePeriod = Period.of(1,2,3)
         Period madeNegative = -positivePeriod
@@ -192,6 +208,22 @@ class DateTimeTest extends GroovyTestCase {
         assert doublePeriod.days == 2
     }
 
+    void testPeriodIsPositiveIsNonnegativeIsNonpositive() {
+        def pos = Period.ofDays(10)
+        assert pos.isPositive() == true
+        assert pos.isNonpositive() == false
+        assert pos.isNonnegative() == true
+
+        def neg = Period.ofDays(-10)
+        assert neg.isPositive() == false
+        assert neg.isNonpositive() == true
+        assert neg.isNonnegative() == false
+
+        assert Period.ZERO.isPositive() == false
+        assert Period.ZERO.isNonpositive() == true
+        assert Period.ZERO.isNonnegative() == true
+    }
+
     void testTemporalGetAt() {
         def epoch = Instant.ofEpochMilli(0)
         assert epoch[ChronoField.INSTANT_SECONDS] == 0
@@ -235,50 +267,66 @@ class DateTimeTest extends GroovyTestCase {
         assert yearMonthPeriod.months == 2
     }
 
-    void testUptoDowntoWithSecondsDefaultUnit() {
+    void testUptoSelfWithDefaultUnit() {
         def epoch = Instant.ofEpochMilli(0)
 
-        int uptoSelfIterations = 0
+        int iterations = 0
         epoch.upto(epoch) {
-            ++uptoSelfIterations
-            assert it == epoch  : 'upto closure should be provided with arg'
+            ++iterations
+            assert it == epoch: 'upto closure should be provided with arg'
         }
-        assert uptoSelfIterations == 1 : 'Iterating upto same value should call closure once'
+        assert iterations == 1: 'Iterating upto same value should call closure once'
+    }
 
-        int downtoSelfIterations = 0
+    void testDowntoSelfWithDefaultUnit() {
+        def epoch = Instant.ofEpochMilli(0)
+        int iterations = 0
         epoch.downto(epoch) {
-            ++downtoSelfIterations
-            assert it == epoch : 'downto closure should be provided with arg'
+            ++iterations
+            assert it == epoch: 'downto closure should be provided with arg'
         }
-        assert downtoSelfIterations == 1 : 'Iterating downto same value should call closure once'
+        assert iterations == 1: 'Iterating downto same value should call closure once'
+    }
+
+    void testUptoWithSecondsDefaultUnit() {
+        def epoch = Instant.ofEpochMilli(0)
 
-        int uptoPlusOneIterations = 0
-        Instant endUp = null
+        int iterations = 0
+        Instant end = null
         epoch.upto(epoch + 1) {
-            ++uptoPlusOneIterations
-            endUp = it
+            ++iterations
+            end = it
         }
-        assert uptoPlusOneIterations == 2 : 'Iterating upto Temporal+1 value should call closure twice'
-        assert endUp.epochSecond == 1 : 'Unexpected upto final value'
+        assert iterations == 2: 'Iterating upto Temporal+1 value should call closure twice'
+        assert end.epochSecond == 1: 'Unexpected upto final value'
+    }
+
+    void testDowntoWithSecondsDefaultUnit() {
+        def epoch = Instant.ofEpochMilli(0)
 
-        int downtoPlusOneIterations = 0
-        Instant endDown = null
+        int iterations = 0
+        Instant end = null
         epoch.downto(epoch - 1) {
-            ++downtoPlusOneIterations
-            endDown = it
+            ++iterations
+            end = it
         }
-        assert downtoPlusOneIterations == 2 : 'Iterating downto Temporal+1 value should call closure twice'
-        assert endDown.epochSecond == -1 : 'Unexpected downto final value'
+        assert iterations == 2 : 'Iterating downto Temporal+1 value should call closure twice'
+        assert end.epochSecond == -1 : 'Unexpected downto final value'
     }
 
-    void testUptoDowntoWithYearsDefaultUnit() {
-        // non-ChronoUnit.SECOND iterations
+    void testUptoWithYearsDefaultUnit() {
         def endYear = null
         Year.of(1970).upto(Year.of(1971)) { year -> endYear = year }
         assert endYear.value == 1971
     }
 
-    void testUptoDownWithMonthsDefaultUnit() {
+    void testDowntoWithYearsDefaultUnit() {
+        def endYear = null
+        Year.of(1971).downto(Year.of(1970)) { year -> endYear = year }
+        assert endYear.value == 1970
+    }
+
+    void testUptoWithMonthsDefaultUnit() {
         def endYearMonth = null
         YearMonth.of(1970, Month.JANUARY).upto(YearMonth.of(1970, Month.FEBRUARY)) { yearMonth ->
             endYearMonth = yearMonth
@@ -286,21 +334,42 @@ class DateTimeTest extends GroovyTestCase {
         assert endYearMonth.month == Month.FEBRUARY
     }
 
-    void testUptoDowntoWithDaysDefaultUnit() {
+    void testDowntoWithMonthsDefaultUnit() {
+        def endYearMonth = null
+        YearMonth.of(1970, Month.FEBRUARY).downto(YearMonth.of(1970, Month.JANUARY)) { yearMonth ->
+            endYearMonth = yearMonth
+        }
+        assert endYearMonth.month == Month.JANUARY
+    }
+
+    void testUptoWithDaysDefaultUnit() {
         def endLocalDate = null
-        LocalDate.of(1970, Month.JANUARY, 1).upto(LocalDate.of(1970, Month.JANUARY, 2)) { localDate ->
+        LocalDate.of(1970, Month.JANUARY, 1).upto(LocalDate.of(1970, Month.JANUARY, 2)) {  localDate ->
             endLocalDate = localDate
         }
         assert endLocalDate.dayOfMonth == 2
     }
 
-    void testUptoDowntoWithIllegalReversedArguments() {
+    void testDowntoWithDaysDefaultUnit() {
+        def endLocalDate = null
+        LocalDate.of(1970, Month.JANUARY, 2).downto(LocalDate.of(1970, Month.JANUARY, 1)) {  localDate ->
+            endLocalDate = localDate
+        }
+        assert endLocalDate.dayOfMonth == 1
+    }
+
+    void testUptoWithIllegalReversedArguments() {
         def epoch = Instant.ofEpochMilli(0)
         try {
             epoch.upto(epoch - 1) {
                 fail('upto() should fail when passed earlier arg')
             }
-        } catch (GroovyRuntimeException e) {}
+        } catch (GroovyRuntimeException e) {
+        }
+    }
+
+    void testDowntoWithIllegalReversedArguments() {
+        def epoch = Instant.ofEpochMilli(0)
         try {
             epoch.downto(epoch + 1) {
                 fail('downto() should fail when passed earlier arg')
@@ -308,27 +377,56 @@ class DateTimeTest extends GroovyTestCase {
         } catch (GroovyRuntimeException e) {}
     }
 
-    void testUptoDowntoWithCustomUnit() {
-        LocalDateTime ldt1 = LocalDateTime.of(2018, Month.FEBRUARY, 11, 22, 9, 34)
-        LocalDateTime ldt2 = ldt1.plusMinutes(1)
+    void testUptoSelfWithCustomUnit() {
+        def today = LocalDate.now()
 
-        int upIterations = 0
-        LocalDateTime endUp = null
-        ldt1.upto(ldt2, ChronoUnit.DAYS) {
-            ++upIterations
-            endUp = it
+        int iterations = 0
+        today.upto(today, ChronoUnit.MONTHS) {
+            ++iterations
+            assert it == today: 'upto closure should be provided with arg'
         }
-        assert upIterations == 2
-        assert endUp.dayOfMonth == 12 : "Upto should have iterated by DAYS"
-
-        int downIterations = 0
-        LocalDateTime endDown = null
-        ldt2.downto(ldt1, ChronoUnit.YEARS) {
-            ++downIterations
-            endDown = it
+        assert iterations == 1: 'Iterating upto same value should call closure once'
+    }
+
+    void testDowntoSelfWithCustomUnit() {
+        def today = LocalDate.now()
+
+        int iterations = 0
+        today.downto(today, ChronoUnit.MONTHS) {
+            ++iterations
+            assert it == today: 'downto closure should be provided with arg'
         }
-        assert downIterations == 2
-        assert endDown.year == 2017 : "Downto should have iterated by YEARS"
+        assert iterations == 1: 'Iterating downto same value should call closure once'
+    }
+
+    void testUptoWithCustomUnit() {
+        LocalDateTime from = LocalDateTime.of(2018, Month.FEBRUARY, 11, 22, 9, 34)
+        // one second beyond one iteration
+        LocalDateTime to = from.plusDays(1).plusSeconds(1)
+
+        int iterations = 0
+        LocalDateTime end = null
+        from.upto(to, ChronoUnit.DAYS) {
+            ++iterations
+            end = it
+        }
+        assert iterations == 2
+        assert end.dayOfMonth == 12: "Upto should have iterated by DAYS twice"
+    }
+
+    void testDowntoWithCustomUnit() {
+        LocalDateTime from = LocalDateTime.of(2018, Month.FEBRUARY, 11, 22, 9, 34)
+        // one day beyond one iteration
+        LocalDateTime to = from.minusYears(1).minusDays(1)
+
+        int iterations = 0
+        LocalDateTime end = null
+        from.downto(to, ChronoUnit.YEARS) {
+            ++iterations
+            end = it
+        }
+        assert iterations == 2
+        assert end.year == 2017 : "Downto should have iterated by YEARS twice"
     }
 
     void testInstantToDateToCalendar() {
@@ -677,4 +775,19 @@ class DateTimeTest extends GroovyTestCase {
         assert [zdt.hour, zdt.minute, zdt.second] == [21, 43, 03]
         assert zdt.nano == 2 * 1e6
     }
+
+    void testPeriodBetweenYears() {
+        def period = Period.between(Year.of(2000), Year.of(2010))
+        assert period.years == 10
+        assert period.months == 0
+        assert period.days == 0
+    }
+
+    void testPeriodBetweenYearMonths() {
+        def period = Period.between(YearMonth.of(2018, Month.MARCH), YearMonth.of(2016, Month.APRIL))
+
+        assert period.years == -1
+        assert period.months == -11
+        assert period.days == 0
+    }
 }