From 7e2106b850ea65866dc97d24d108c7fe1ea64c8c Mon Sep 17 00:00:00 2001 From: Arjen Poutsma Date: Thu, 24 Feb 2022 14:46:26 +0100 Subject: [PATCH] Refactor roll forward in CronField Before this commit, CronField.Type::rollForward added temporal units to reach the higher order field. This caused issues with DST, where the added amount of hours was either too small or too large. This commit refactors the implementation so that it now adds one to the higher order field, and reset the current field to the minimum value. Closes gh-28095 --- .../scheduling/support/CronField.java | 34 ++++++++----------- .../support/CronExpressionTests.java | 8 +++++ 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java b/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java index adc2c2ffe5..f794645d65 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java @@ -18,6 +18,7 @@ package org.springframework.scheduling.support; import java.time.DateTimeException; import java.time.temporal.ChronoField; +import java.time.temporal.ChronoUnit; import java.time.temporal.Temporal; import java.time.temporal.ValueRange; import java.util.function.BiFunction; @@ -168,22 +169,25 @@ abstract class CronField { * day-of-month, month, day-of-week. */ protected enum Type { - NANO(ChronoField.NANO_OF_SECOND), - SECOND(ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND), - MINUTE(ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND), - HOUR(ChronoField.HOUR_OF_DAY, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND), - DAY_OF_MONTH(ChronoField.DAY_OF_MONTH, ChronoField.HOUR_OF_DAY, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND), - MONTH(ChronoField.MONTH_OF_YEAR, ChronoField.DAY_OF_MONTH, ChronoField.HOUR_OF_DAY, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND), - DAY_OF_WEEK(ChronoField.DAY_OF_WEEK, ChronoField.HOUR_OF_DAY, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND); + NANO(ChronoField.NANO_OF_SECOND, ChronoUnit.SECONDS), + SECOND(ChronoField.SECOND_OF_MINUTE, ChronoUnit.MINUTES, ChronoField.NANO_OF_SECOND), + MINUTE(ChronoField.MINUTE_OF_HOUR, ChronoUnit.HOURS, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND), + HOUR(ChronoField.HOUR_OF_DAY, ChronoUnit.DAYS, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND), + DAY_OF_MONTH(ChronoField.DAY_OF_MONTH, ChronoUnit.MONTHS, ChronoField.HOUR_OF_DAY, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND), + MONTH(ChronoField.MONTH_OF_YEAR, ChronoUnit.YEARS, ChronoField.DAY_OF_MONTH, ChronoField.HOUR_OF_DAY, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND), + DAY_OF_WEEK(ChronoField.DAY_OF_WEEK, ChronoUnit.WEEKS, ChronoField.HOUR_OF_DAY, ChronoField.MINUTE_OF_HOUR, ChronoField.SECOND_OF_MINUTE, ChronoField.NANO_OF_SECOND); private final ChronoField field; + private final ChronoUnit higherOrder; + private final ChronoField[] lowerOrders; - Type(ChronoField field, ChronoField... lowerOrders) { + Type(ChronoField field, ChronoUnit higherOrder, ChronoField... lowerOrders) { this.field = field; + this.higherOrder = higherOrder; this.lowerOrders = lowerOrders; } @@ -266,17 +270,9 @@ abstract class CronField { * @return the rolled forward temporal */ public > T rollForward(T temporal) { - int current = get(temporal); - ValueRange range = temporal.range(this.field); - long amount = range.getMaximum() - current + 1; - T result = this.field.getBaseUnit().addTo(temporal, amount); - current = get(result); - range = result.range(this.field); - // adjust for daylight savings - if (current != range.getMinimum()) { - result = this.field.adjustInto(result, range.getMinimum()); - } - return result; + T result = this.higherOrder.addTo(temporal, 1); + ValueRange range = result.range(this.field); + return this.field.adjustInto(result, range.getMinimum()); } /** diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java b/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java index 5de5362fc1..5dd837e037 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/support/CronExpressionTests.java @@ -1344,6 +1344,14 @@ class CronExpressionTests { actual = cronExpression.next(last); assertThat(actual).isNotNull(); assertThat(actual).isEqualTo(expected); + + cronExpression = CronExpression.parse("0 5 0 * * *"); + + last = ZonedDateTime.parse("2019-10-27T01:05+02:00[Europe/Amsterdam]"); + expected = ZonedDateTime.parse("2019-10-28T00:05+01:00[Europe/Amsterdam]"); + actual = cronExpression.next(last); + assertThat(actual).isNotNull(); + assertThat(actual).isEqualTo(expected); } @Test