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);
}
}
}