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 adc2c2ffe5d..f794645d654 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 5de5362fc1d..5dd837e0379 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