This is an automated email from the ASF dual-hosted git repository. garydgregory pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/commons-lang.git
commit fea9a785363ccb96f2afd8c399b205c8f04b27f0 Author: Gary Gregory <[email protected]> AuthorDate: Fri Oct 16 10:13:57 2026 -0400 [LANG-796] Clarify DateUtils calendar arithmetic and daylight saving time behavior Document calendar-based day, week, month, and year addition versus elapsed-time hour, minute, second, and millisecond addition. Test forward and backward day addition and 24-hour addition across both DST transitions. Preserve null-time-zone coverage and the active default time zone during test setup. --- src/changes/changes.xml | 1 + .../org/apache/commons/lang3/time/DateUtils.java | 54 +++++++++++++++++++- .../apache/commons/lang3/time/DateUtilsTest.java | 58 ++++++++++++++++++++-- 3 files changed, 107 insertions(+), 6 deletions(-) diff --git a/src/changes/changes.xml b/src/changes/changes.xml index 553be6ef5..77562c073 100644 --- a/src/changes/changes.xml +++ b/src/changes/changes.xml @@ -60,6 +60,7 @@ The <action> type attribute can be add,update,fix,remove. <action type="fix" dev="ggregory" due-to="XenoAmess, Gary Gregory">Fix TypeUtils.isAssignable rejecting Class<X> to Class<?> assignments (#1820).</action> <action issue="LANG-1837" type="fix" dev="ggregory" due-to="Aosen Xiong, Gary Gregory">Prevent null returns from lazily initialized string caches.</action> <action type="fix" dev="ggregory" due-to="alhuda, Gary Gregory">Escape CharSequence values in ToStringStyle.JSON_STYLE (#1821).</action> + <action issue="LANG-796" type="fix" dev="ggregory" due-to="gaurav kumar pandey, Gary Gregory">Clarify DateUtils calendar arithmetic and daylight saving time behavior.</action> <!-- ADD --> <!-- UPDATE --> </release> diff --git a/src/main/java/org/apache/commons/lang3/time/DateUtils.java b/src/main/java/org/apache/commons/lang3/time/DateUtils.java index 1b6faec43..226aaffc8 100644 --- a/src/main/java/org/apache/commons/lang3/time/DateUtils.java +++ b/src/main/java/org/apache/commons/lang3/time/DateUtils.java @@ -51,8 +51,13 @@ * <p> * Several methods are provided for adding to {@link Date} objects, of the form * {@code addXXX(Date date, int amount)}. It is important to note these methods - * use a {@link Calendar} internally (with default time zone and locale) and may - * be affected by changes to daylight saving time (DST). + * use the default time zone and locale. Adding days, weeks, months, or years performs + * calendar-based arithmetic which may be affected by changes to daylight saving time (DST). + * For instance, adding one day across a DST change will result in a calendar day that may + * represent 23 or 25 hours rather than exactly 86,400,000 milliseconds. Callers needing + * fixed elapsed-time arithmetic should consider using {@link java.time.Instant} or + * direct millisecond calculations. Adding hours, minutes, seconds, or milliseconds adds the + * corresponding elapsed time, so the local clock time may change across a DST transition. * </p> * * @since 2.0 @@ -214,6 +219,10 @@ private enum ModifyType { * Adds to a date returning a new object. * The original {@link Date} is unchanged. * + * <p> + * This method uses the default time zone and locale. Calendar-based addition may be affected by daylight saving time (DST) transitions. + * </p> + * * @param date The date, not null. * @param calendarField The calendar field to add to. * @param amount The amount to add, may be negative. @@ -232,6 +241,12 @@ private static Date add(final Date date, final int calendarField, final int amou * Adds a number of days to a date returning a new object. * The original {@link Date} is unchanged. * + * <p> + * This method adds calendar days using the default time zone and locale. + * Daylight saving time (DST) transitions may change the elapsed time. For example, adding one day may add 23 or 25 hours + * rather than a fixed 24-hour (86,400,000 millisecond) duration. + * </p> + * * @param date The date, not null. * @param amount The amount to add, may be negative. * @return The new {@link Date} with the amount added. @@ -245,6 +260,11 @@ public static Date addDays(final Date date, final int amount) { * Adds a number of hours to a date returning a new object. * The original {@link Date} is unchanged. * + * <p> + * This method adds elapsed hours using the default time zone and locale. + * Daylight saving time (DST) transitions do not change the elapsed time added, but may affect the resulting local clock time. + * </p> + * * @param date The date, not null. * @param amount The amount to add, may be negative. * @return The new {@link Date} with the amount added. @@ -258,6 +278,11 @@ public static Date addHours(final Date date, final int amount) { * Adds a number of milliseconds to a date returning a new object. * The original {@link Date} is unchanged. * + * <p> + * This method adds elapsed milliseconds using the default time zone and locale. + * Daylight saving time (DST) transitions do not change the elapsed time added, but may affect the resulting local clock time. + * </p> + * * @param date The date, not null. * @param amount The amount to add, may be negative. * @return The new {@link Date} with the amount added. @@ -271,6 +296,11 @@ public static Date addMilliseconds(final Date date, final int amount) { * Adds a number of minutes to a date returning a new object. * The original {@link Date} is unchanged. * + * <p> + * This method adds elapsed minutes using the default time zone and locale. + * Daylight saving time (DST) transitions do not change the elapsed time added, but may affect the resulting local clock time. + * </p> + * * @param date The date, not null. * @param amount The amount to add, may be negative. * @return The new {@link Date} with the amount added. @@ -284,6 +314,11 @@ public static Date addMinutes(final Date date, final int amount) { * Adds a number of months to a date returning a new object. * The original {@link Date} is unchanged. * + * <p> + * This method adds calendar months using the default time zone and locale. + * Daylight saving time (DST) transitions may change the elapsed time. + * </p> + * * @param date The date, not null. * @param amount The amount to add, may be negative. * @return The new {@link Date} with the amount added. @@ -297,6 +332,11 @@ public static Date addMonths(final Date date, final int amount) { * Adds a number of seconds to a date returning a new object. * The original {@link Date} is unchanged. * + * <p> + * This method adds elapsed seconds using the default time zone and locale. + * Daylight saving time (DST) transitions do not change the elapsed time added, but may affect the resulting local clock time. + * </p> + * * @param date The date, not null. * @param amount The amount to add, may be negative. * @return The new {@link Date} with the amount added. @@ -310,6 +350,11 @@ public static Date addSeconds(final Date date, final int amount) { * Adds a number of weeks to a date returning a new object. * The original {@link Date} is unchanged. * + * <p> + * This method adds calendar weeks using the default time zone and locale. + * Daylight saving time (DST) transitions may change the elapsed time. + * </p> + * * @param date The date, not null. * @param amount The amount to add, may be negative. * @return The new {@link Date} with the amount added. @@ -323,6 +368,11 @@ public static Date addWeeks(final Date date, final int amount) { * Adds a number of years to a date returning a new object. * The original {@link Date} is unchanged. * + * <p> + * This method adds calendar years using the default time zone and locale. + * Daylight saving time (DST) transitions may change the elapsed time. + * </p> + * * @param date The date, not null. * @param amount The amount to add, may be negative. * @return The new {@link Date} with the amount added. diff --git a/src/test/java/org/apache/commons/lang3/time/DateUtilsTest.java b/src/test/java/org/apache/commons/lang3/time/DateUtilsTest.java index e8b2ab6e6..594dbe6dd 100644 --- a/src/test/java/org/apache/commons/lang3/time/DateUtilsTest.java +++ b/src/test/java/org/apache/commons/lang3/time/DateUtilsTest.java @@ -44,6 +44,7 @@ import java.util.Locale; import java.util.NoSuchElementException; import java.util.TimeZone; +import java.util.concurrent.TimeUnit; import java.util.stream.Stream; import org.apache.commons.lang3.AbstractLangTest; @@ -57,13 +58,17 @@ import org.junitpioneer.jupiter.DefaultLocale; import org.junitpioneer.jupiter.DefaultTimeZone; import org.junitpioneer.jupiter.ReadsDefaultLocale; +import org.junitpioneer.jupiter.ReadsDefaultTimeZone; import org.junitpioneer.jupiter.WritesDefaultLocale; +import org.junitpioneer.jupiter.WritesDefaultTimeZone; /** * Tests {@link DateUtils}. */ @ReadsDefaultLocale @WritesDefaultLocale +@ReadsDefaultTimeZone +@WritesDefaultTimeZone class DateUtilsTest extends AbstractLangTest { private static final Date MAX_DATE = new Date(Long.MAX_VALUE); @@ -189,7 +194,7 @@ private static Stream<Arguments> testToLocalDateTimeTimeZone() { Arguments.of( LocalDateTime.ofInstant( java.sql.Timestamp.valueOf("2023-03-12 02:30:00").toInstant(), - TimeZone.getDefault().toZoneId() + TimeZones.GMT.toZoneId() ), java.sql.Timestamp.valueOf("2023-03-12 02:30:00"), null @@ -263,6 +268,7 @@ private void assertDate(final Date date, final int year, final int month, final } @BeforeEach public void setUp() throws Exception { + final TimeZone defaultZone = TimeZone.getDefault(); dateParser = new SimpleDateFormat("MMM dd, yyyy", Locale.ENGLISH); dateTimeParser = new SimpleDateFormat("MMM dd, yyyy H:mm:ss.SSS", Locale.ENGLISH); dateAmPm1 = dateTimeParser.parse("February 3, 2002 01:10:00.000"); @@ -282,8 +288,8 @@ public void setUp() throws Exception { date7 = dateTimeParser.parse("March 30, 2003 02:40:00.000"); date8 = dateTimeParser.parse("October 26, 2003 05:30:45.000"); } finally { - dateTimeParser.setTimeZone(TIME_ZONE_DEFAULT); - TimeZone.setDefault(TIME_ZONE_DEFAULT); + dateTimeParser.setTimeZone(defaultZone); + TimeZone.setDefault(defaultZone); } date9 = dateTimeParser.parse("March 30, 2003 01:10:00.000"); calAmPm1 = Calendar.getInstance(); @@ -313,7 +319,7 @@ public void setUp() throws Exception { cal8 = Calendar.getInstance(); cal8.setTime(date8); } finally { - TimeZone.setDefault(TIME_ZONE_DEFAULT); + TimeZone.setDefault(defaultZone); } } @@ -337,6 +343,50 @@ void testAddDays() throws Exception { assertNullPointerException(() -> DateUtils.addDays(null, 0)); } + @DefaultTimeZone("Europe/Brussels") + @Test + void testAddDaysDaylightSavingTime() { + final TimeZone brusselsTz = TimeZones.getTimeZone("Europe/Brussels"); + + // 2012-03-25 00:00:00 CET: Brussels spring DST transition (23-hour day) + final Calendar calSpring = Calendar.getInstance(brusselsTz); + calSpring.clear(); + calSpring.set(2012, Calendar.MARCH, 25, 0, 0, 0); + final Date inputSpring = calSpring.getTime(); + + // In Europe/Brussels: calendar day advances to 2012-03-26 00:00:00 CEST (23 hours elapsed) + final Date outputSpring = DateUtils.addDays(inputSpring, 1); + assertEquals(TimeUnit.HOURS.toMillis(23), outputSpring.getTime() - inputSpring.getTime()); + + final Calendar calSpringResult = Calendar.getInstance(brusselsTz); + calSpringResult.setTime(outputSpring); + assertEquals(2012, calSpringResult.get(Calendar.YEAR)); + assertEquals(Calendar.MARCH, calSpringResult.get(Calendar.MONTH)); + assertEquals(26, calSpringResult.get(Calendar.DAY_OF_MONTH)); + assertEquals(0, calSpringResult.get(Calendar.HOUR_OF_DAY)); + assertEquals(inputSpring, DateUtils.addDays(outputSpring, -1)); + assertEquals(TimeUnit.HOURS.toMillis(24), DateUtils.addHours(inputSpring, 24).getTime() - inputSpring.getTime()); + + // 2012-10-28 00:00:00 CEST: Brussels autumn DST transition (25-hour day) + final Calendar calAutumn = Calendar.getInstance(brusselsTz); + calAutumn.clear(); + calAutumn.set(2012, Calendar.OCTOBER, 28, 0, 0, 0); + final Date inputAutumn = calAutumn.getTime(); + + // In Europe/Brussels: calendar day advances to 2012-10-29 00:00:00 CET (25 hours elapsed) + final Date outputAutumn = DateUtils.addDays(inputAutumn, 1); + assertEquals(TimeUnit.HOURS.toMillis(25), outputAutumn.getTime() - inputAutumn.getTime()); + + final Calendar calAutumnResult = Calendar.getInstance(brusselsTz); + calAutumnResult.setTime(outputAutumn); + assertEquals(2012, calAutumnResult.get(Calendar.YEAR)); + assertEquals(Calendar.OCTOBER, calAutumnResult.get(Calendar.MONTH)); + assertEquals(29, calAutumnResult.get(Calendar.DAY_OF_MONTH)); + assertEquals(0, calAutumnResult.get(Calendar.HOUR_OF_DAY)); + assertEquals(inputAutumn, DateUtils.addDays(outputAutumn, -1)); + assertEquals(TimeUnit.HOURS.toMillis(24), DateUtils.addHours(inputAutumn, 24).getTime() - inputAutumn.getTime()); + } + @Test void testAddHours() throws Exception { Date result = DateUtils.addHours(BASE_DATE, 0);
