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 ebe58c633 Keep FastDateFormat able to parse the '+hh:mm' offsets its 
own 'Z' patterns format (#1857)
ebe58c633 is described below

commit ebe58c633749184ab2c6256a1091e70ca0fde5bc
Author: Gary Gregory <[email protected]>
AuthorDate: Mon Nov 2 17:20:47 2026 -0500

    Keep FastDateFormat able to parse the '+hh:mm' offsets its own 'Z'
    patterns format (#1857)
    
    Clarify FastDateFormat time zone patterns and extend regression tests
    
    Document ZZ and longer Z patterns, including zero-offset formatting and
    differences from SimpleDateFormat. Test fractional-hour offsets, legacy
    time zone inputs, cache clearing, and complete input consumption.
---
 src/changes/changes.xml                            |  1 +
 .../apache/commons/lang3/time/FastDateFormat.java  | 10 +++-
 .../apache/commons/lang3/time/FastDateParser.java  |  8 ++-
 .../apache/commons/lang3/time/FastDatePrinter.java |  7 ++-
 .../lang3/time/FastDateFormat_ZRoundTripTest.java  | 58 ++++++++++++++++++++--
 5 files changed, 73 insertions(+), 11 deletions(-)

diff --git a/src/changes/changes.xml b/src/changes/changes.xml
index 7d3451fca..0589812b5 100644
--- a/src/changes/changes.xml
+++ b/src/changes/changes.xml
@@ -87,6 +87,7 @@ The <action> type attribute can be add,update,fix,remove.
     <action issue="LANG-1423" type="fix" dev="ggregory" due-to="Gary Gregory, 
Reassoonn">Javadoc: Include wrapping operations in StringUtils overview 
(#1850).</action>
     <action issue="LANG-980"  type="fix" dev="ggregory" due-to="Gary Gregory, 
Sebb">Use enum constants for duration format units in 
DurationFormatUtils.</action>
     <action issue="LANG-1198" type="fix" dev="ggregory" due-to="Gary Gregory, 
Reassoonn">Javadoc: Clarify stripEnd character-set semantics (#1851).</action>
+    <action                   type="fix" dev="ggregory" due-to="Gary Gregory, 
simpleqt">Keep FastDateFormat able to parse the '+hh:mm' offsets its own 'Z' 
patterns format (#1857).</action>
     <!-- ADD -->
     <action issue="LANG-1790" type="add" dev="ggregory" due-to="Mert Kirpici, 
Ivan Šarić, Gary Gregory">Add optional FIFO ordering for TimedSemaphore 
blocking acquisitions.</action>
     <!-- UPDATE -->
diff --git a/src/main/java/org/apache/commons/lang3/time/FastDateFormat.java 
b/src/main/java/org/apache/commons/lang3/time/FastDateFormat.java
index b1bf48748..2c3f7c8a8 100644
--- a/src/main/java/org/apache/commons/lang3/time/FastDateFormat.java
+++ b/src/main/java/org/apache/commons/lang3/time/FastDateFormat.java
@@ -70,8 +70,14 @@
  * </p>
  *
  * <p>
- * In addition, the pattern {@code 'ZZ'} has been made to represent ISO 8601 
extended format time zones (for example, {@code +08:00} or {@code -11:00}). 
This introduces
- * a minor incompatibility with Java 1.4, but at a gain of useful 
functionality.
+ * The pattern {@code 'ZZ'} represents ISO 8601 extended format time zones 
(for example, {@code +08:00} or {@code -11:00}), printing {@code Z} for
+ * a zero offset. Three or more {@code 'Z'} letters also print colon-separated 
offsets, but print {@code +00:00} for a zero offset. These patterns differ
+ * from {@link SimpleDateFormat}.
+ * </p>
+ *
+ * <p>
+ * When parsing, {@code 'ZZ'} accepts {@code Z} or a colon-separated numeric 
offset. Three or more {@code 'Z'} letters accept colon-separated numeric
+ * offsets as well as the RFC822 offsets, GMT offsets, and localized time zone 
names accepted by a single {@code 'Z'}.
  * </p>
  *
  * <p>
diff --git a/src/main/java/org/apache/commons/lang3/time/FastDateParser.java 
b/src/main/java/org/apache/commons/lang3/time/FastDateParser.java
index 0f0a177fc..7eb294528 100644
--- a/src/main/java/org/apache/commons/lang3/time/FastDateParser.java
+++ b/src/main/java/org/apache/commons/lang3/time/FastDateParser.java
@@ -71,7 +71,13 @@
  * </p>
  *
  * <p>
- * Only parsing is supported by this class, but all patterns are compatible 
with SimpleDateFormat.
+ * Only parsing is supported by this class. Patterns are compatible with 
{@link SimpleDateFormat}, except for time zone extensions described below.
+ * </p>
+ *
+ * <p>
+ * The pattern {@code 'ZZ'} accepts {@code Z} for a zero offset or an ISO 8601 
extended numeric offset such as {@code +08:00}. Three or more {@code 'Z'}
+ * letters accept colon-separated numeric offsets in addition to the RFC822 
offsets, GMT offsets, and localized time zone names accepted by a single
+ * {@code 'Z'}. The patterns {@code 'z'} and a single {@code 'Z'} do not 
accept bare colon-separated numeric offsets.
  * </p>
  *
  * <p>
diff --git a/src/main/java/org/apache/commons/lang3/time/FastDatePrinter.java 
b/src/main/java/org/apache/commons/lang3/time/FastDatePrinter.java
index ab42fab78..3592b0fb7 100644
--- a/src/main/java/org/apache/commons/lang3/time/FastDatePrinter.java
+++ b/src/main/java/org/apache/commons/lang3/time/FastDatePrinter.java
@@ -79,10 +79,9 @@
  * </p>
  *
  * <p>
- * In addition, the pattern {@code 'ZZ'} has been made to represent
- * ISO 8601 extended format time zones (for example, {@code +08:00} or {@code 
-11:00}).
- * This introduces a minor incompatibility with Java 1.4, but at a gain of
- * useful functionality.
+ * The pattern {@code 'ZZ'} represents ISO 8601 extended format time zones 
(for example, {@code +08:00} or {@code -11:00}), printing {@code Z} for
+ * a zero offset. Three or more {@code 'Z'} letters also print colon-separated 
offsets, but print {@code +00:00} for a zero offset. These patterns differ
+ * from {@link SimpleDateFormat}.
  * </p>
  *
  * <p>
diff --git 
a/src/test/java/org/apache/commons/lang3/time/FastDateFormat_ZRoundTripTest.java
 
b/src/test/java/org/apache/commons/lang3/time/FastDateFormat_ZRoundTripTest.java
index b22ea9f4e..42b06fe35 100644
--- 
a/src/test/java/org/apache/commons/lang3/time/FastDateFormat_ZRoundTripTest.java
+++ 
b/src/test/java/org/apache/commons/lang3/time/FastDateFormat_ZRoundTripTest.java
@@ -18,6 +18,8 @@
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
 
 import java.text.ParsePosition;
 import java.util.Date;
@@ -26,6 +28,8 @@
 
 import org.apache.commons.lang3.AbstractLangTest;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
 import org.junitpioneer.jupiter.ReadsDefaultLocale;
 import org.junitpioneer.jupiter.ReadsDefaultTimeZone;
 
@@ -38,13 +42,33 @@ class FastDateFormat_ZRoundTripTest extends 
AbstractLangTest {
 
     private static final TimeZone NEW_YORK = 
TimeZones.getTimeZone("America/New_York");
 
-    private static Date parse(final String pattern, final String text) {
+    private static Date parse(final DateParser parser, final String text) {
         final ParsePosition parsePosition = new ParsePosition(0);
-        final Date date = FastDateFormat.getInstance(pattern, NEW_YORK, 
Locale.US).parse(text, parsePosition);
-        assertNotNull(date, () -> pattern + " failed to parse '" + text + "' 
(error index " + parsePosition.getErrorIndex() + ")");
+        final Date date = parser.parse(text, parsePosition);
+        assertNotNull(date, () -> parser.getPattern() + " failed to parse '" + 
text + "' (error index " + parsePosition.getErrorIndex() + ")");
+        assertEquals(text.length(), parsePosition.getIndex(), 
parser.getPattern());
         return date;
     }
 
+    private static Date parse(final String pattern, final String text) {
+        return parse(FastDateFormat.getInstance(pattern, NEW_YORK, Locale.US), 
text);
+    }
+
+    @Test
+    @ReadsDefaultLocale
+    @ReadsDefaultTimeZone
+    void testClearExtendedTimeZoneCache() {
+        final Object strategy = 
FastDateParser.TimeZoneStrategy.getIsoExtendedStrategy(Locale.US);
+        assertSame(strategy, 
FastDateParser.TimeZoneStrategy.getIsoExtendedStrategy(Locale.US));
+        final FastDateParser parser = new FastDateParser("yyyy-MM-ddZZZ", 
NEW_YORK, Locale.US);
+        final String text = "2023-10-27+05:30";
+        final Date expected = parse("yyyy-MM-ddZZ", text);
+        FastDateParser.clear();
+        assertNotSame(strategy, 
FastDateParser.TimeZoneStrategy.getIsoExtendedStrategy(Locale.US));
+        assertEquals(expected, parse(parser, text));
+        assertEquals(expected, parse(new FastDateParser("yyyy-MM-ddZZZ", 
NEW_YORK, Locale.US), text));
+    }
+
     @Test
     @ReadsDefaultLocale
     @ReadsDefaultTimeZone
@@ -53,6 +77,32 @@ void testParseExplicitColonOffset() {
         assertEquals(parse("yyyy-MM-ddZZ", "2023-10-27-05:30"), 
parse("yyyy-MM-ddZZZ", "2023-10-27-05:30"));
     }
 
+    @ParameterizedTest
+    @CsvSource({"+0530, +05:30", "-0330, -03:30", "GMT+05:30, +05:30", "PST, 
-08:00", "Pacific Standard Time, -08:00", "UTC, +00:00"})
+    @ReadsDefaultLocale
+    @ReadsDefaultTimeZone
+    void testParseLegacyTimeZones(final String zone, final String offset) {
+        assertEquals(parse("yyyy-MM-ddZZ", "2023-10-27" + offset), 
parse("yyyy-MM-ddZZZ", "2023-10-27" + zone));
+    }
+
+    @ParameterizedTest
+    @CsvSource({"UTC, +00:00", "GMT+05:30, +05:30", "GMT-03:30, -03:30", 
"GMT+12:45, +12:45"})
+    @ReadsDefaultLocale
+    @ReadsDefaultTimeZone
+    void testRoundTripOffsets(final String zoneId, final String offset) {
+        final Date date = new Date(1698446400123L);
+        final TimeZone zone = TimeZones.getTimeZone(zoneId);
+        for (int zCount = 2; zCount <= 8; zCount++) {
+            final String pattern = "yyyy-MM-dd HH:mm:ss.SSS" + 
"ZZZZZZZZ".substring(0, zCount);
+            final FastDateFormat format = FastDateFormat.getInstance(pattern, 
zone, Locale.US);
+            final String text = format.format(date);
+            final String expectedOffset = zCount == 2 && "UTC".equals(zoneId) 
? "Z" : offset;
+            final String localDateTime = 
FastDateFormat.getInstance("yyyy-MM-dd HH:mm:ss.SSS", zone, 
Locale.US).format(date);
+            assertEquals(localDateTime + expectedOffset, text, pattern);
+            assertEquals(date, parse(format, text), pattern);
+        }
+    }
+
     @Test
     @ReadsDefaultLocale
     @ReadsDefaultTimeZone
@@ -72,7 +122,7 @@ void testRoundTripThreeOrMoreZ() {
             final FastDateFormat format = FastDateFormat.getInstance(pattern, 
NEW_YORK, Locale.US);
             final String text = format.format(new Date(1698446400000L));
             assertEquals("2023-10-27-04:00", text, pattern);
-            assertEquals(parse("yyyy-MM-ddZZ", "2023-10-27-04:00"), 
parse(pattern, text), pattern);
+            assertEquals(parse("yyyy-MM-ddZZ", "2023-10-27-04:00"), 
parse(format, text), pattern);
         }
     }
 }

Reply via email to