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


The following commit(s) were added to refs/heads/master by this push:
     new 75086d398 keep DateUtils.ceiling and round inside a DST overlap hour 
(#1842)
75086d398 is described below

commit 75086d398acd1e5c64340e7276d775fbc4d767f5
Author: alhuda <[email protected]>
AuthorDate: Tue Oct 27 17:54:41 2026 +0530

    keep DateUtils.ceiling and round inside a DST overlap hour (#1842)
    
    modify() removes the fields below the target field in two ways: the
    LANG-59 block subtracts milliseconds, seconds and minutes from the epoch
    time, but only when the value rounds down, and the field loop then clears
    whatever is left with Calendar.set(). Calendar.set() recomputes the
    instant from the wall-clock fields, and GregorianCalendar resolves a wall
    time inside a DST fall-back overlap to standard time, so ceiling and
    round of an instant in the daylight pass of the repeated hour were moved
    into the standard-time pass an hour later. Remove the three sub-hour
    fields arithmetically in the loop as well, so the instant keeps its
    offset; DATE and larger fields still use Calendar.set() for wall-clock
    semantics, and truncate is unchanged.
---
 .../org/apache/commons/lang3/time/DateUtils.java   | 17 ++++++++++++-
 .../apache/commons/lang3/time/DateUtilsTest.java   | 29 ++++++++++++++++++++++
 2 files changed, 45 insertions(+), 1 deletion(-)

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 2f354037e..1272f4f4e 100644
--- a/src/main/java/org/apache/commons/lang3/time/DateUtils.java
+++ b/src/main/java/org/apache/commons/lang3/time/DateUtils.java
@@ -1347,7 +1347,22 @@ private static Calendar modify(final Calendar val, final 
int field, final Modify
             }
             // We need to remove this field
             if (offset != 0) {
-                val.set(aField[0], val.get(aField[0]) - offset);
+                switch (aField[0]) {
+                // Remove the sub-hour fields arithmetically, like the LANG-59 
block above: Calendar.set() resolves
+                // an ambiguous wall time in a DST overlap to standard time, 
shifting the result by an hour.
+                case Calendar.MILLISECOND:
+                    val.setTimeInMillis(val.getTimeInMillis() - offset);
+                    break;
+                case Calendar.SECOND:
+                    val.setTimeInMillis(val.getTimeInMillis() - offset * 
MILLIS_PER_SECOND);
+                    break;
+                case Calendar.MINUTE:
+                    val.setTimeInMillis(val.getTimeInMillis() - offset * 
MILLIS_PER_MINUTE);
+                    break;
+                default:
+                    val.set(aField[0], val.get(aField[0]) - offset);
+                    break;
+                }
             }
         }
         throw new IllegalArgumentException("The field " + field + " is not 
supported");
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 594dbe6dd..f2482d716 100644
--- a/src/test/java/org/apache/commons/lang3/time/DateUtilsTest.java
+++ b/src/test/java/org/apache/commons/lang3/time/DateUtilsTest.java
@@ -1807,6 +1807,35 @@ void testTruncateLang59() {
         assertEquals(oct31MDT, DateUtils.round(oct31_01_02_03_04MDT, 
Calendar.DATE), "Round Calendar.DATE");
     }
 
+    /**
+     * Tests that ceiling and round keep the offset of a time inside a DST 
overlap when a field below the target is at or above its halfway
+     * point, which modify() removes with Calendar.set() rather than the 
LANG-59 arithmetic used for the round-down case.
+     */
+    @DefaultTimeZone("Europe/London")
+    @Test
+    void testCeilingAndRoundDstOverlap() {
+        // 2024-10-27 01:00-02:00 BST repeats as 01:00-02:00 GMT; 
ZonedDateTime.of() picks the earlier offset, BST.
+        final ZonedDateTime bst = ZonedDateTime.of(2024, 10, 27, 1, 8, 58, 
245_000_000, ZoneId.of("Europe/London"));
+        assertEquals(ZoneOffset.ofHours(1), bst.getOffset());
+        final ZonedDateTime gmt = bst.withLaterOffsetAtOverlap();
+        assertEquals(ZoneOffset.UTC, gmt.getOffset());
+        for (final ZonedDateTime zdt : new ZonedDateTime[] { bst, gmt }) {
+            final Date date = Date.from(zdt.toInstant());
+            final ZonedDateTime minute = zdt.truncatedTo(ChronoUnit.MINUTES);
+            final ZonedDateTime hour = zdt.truncatedTo(ChronoUnit.HOURS);
+            
assertEquals(Date.from(zdt.truncatedTo(ChronoUnit.SECONDS).plusSeconds(1).toInstant()),
 DateUtils.ceiling(date, Calendar.SECOND), "Ceiling Calendar.SECOND " + zdt);
+            assertEquals(Date.from(minute.plusMinutes(1).toInstant()), 
DateUtils.ceiling(date, Calendar.MINUTE), "Ceiling Calendar.MINUTE " + zdt);
+            assertEquals(Date.from(minute.plusMinutes(1).toInstant()), 
DateUtils.round(date, Calendar.MINUTE), "Round Calendar.MINUTE " + zdt);
+            assertEquals(Date.from(hour.plusHours(1).toInstant()), 
DateUtils.ceiling(date, Calendar.HOUR_OF_DAY), "Ceiling Calendar.HOUR_OF_DAY " 
+ zdt);
+            assertEquals(Date.from(hour.plusHours(1).toInstant()), 
DateUtils.ceiling(date, Calendar.HOUR), "Ceiling Calendar.HOUR " + zdt);
+            assertEquals(Date.from(hour.toInstant()), DateUtils.round(date, 
Calendar.HOUR_OF_DAY), "Round Calendar.HOUR_OF_DAY " + zdt);
+            // 38 minutes past rounds up to the next hour
+            final Date late = Date.from(zdt.withMinute(38).toInstant());
+            assertEquals(Date.from(hour.plusHours(1).toInstant()), 
DateUtils.round(late, Calendar.HOUR_OF_DAY), "Round up Calendar.HOUR_OF_DAY " + 
zdt);
+            assertEquals(Date.from(hour.plusHours(1).toInstant()), 
DateUtils.round(late, Calendar.HOUR), "Round up Calendar.HOUR " + zdt);
+        }
+    }
+
     /**
      * Tests the calendar iterator for week ranges
      */

Reply via email to