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 <[email protected]> Authored: Sun Mar 4 16:55:56 2018 -0500 Committer: paulk <[email protected]> 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 + } }
