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&lt;X&gt; to 
Class&lt;?&gt; 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);

Reply via email to