Skip to content

Commit 4f13c4e

Browse files
authored
Fixes #5429, format/parse of large ISO-8601 by StdDateFormat not round-tripping (#5441)
1 parent 269b248 commit 4f13c4e

File tree

3 files changed

+71
-24
lines changed

3 files changed

+71
-24
lines changed

release-notes/VERSION-2.x

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ Project: jackson-databind
2828
(fix by @cowtowncoder, w/ Claude code)
2929
#5413: Add/support forward reference resolution for array values
3030
(contributed by Hélios G)
31+
#5429: Formatting and Parsing of Large ISO-8601 Dates is inconsistent
32+
(reported by @DavTurns)
3133

3234
2.20.2 (not yet released)
3335

src/main/java/com/fasterxml/jackson/databind/util/StdDateFormat.java

Lines changed: 28 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -49,11 +49,14 @@ public class StdDateFormat
4949

5050
protected final static Pattern PATTERN_PLAIN = Pattern.compile(PATTERN_PLAIN_STR);
5151

52+
// [databind#5429]: Extended year format (4+ digits, optional +/- prefix)
53+
protected final static String PATTERN_YEAR_STR = "(?:[+-]?\\d{4,})";
54+
5255
protected final static Pattern PATTERN_ISO8601;
5356
static {
5457
Pattern p = null;
5558
try {
56-
p = Pattern.compile(PATTERN_PLAIN_STR
59+
p = Pattern.compile(PATTERN_YEAR_STR + "[-]\\d\\d[-]\\d\\d"
5760
+"[T]\\d\\d[:]\\d\\d(?:[:]\\d\\d)?" // hours, minutes, optional seconds
5861
+"(\\.\\d+)?" // optional second fractions
5962
+"(Z|[+-]\\d\\d(?:[:]?\\d\\d)?)?" // optional timeoffset/Z
@@ -607,13 +610,17 @@ public int hashCode() {
607610
*/
608611
protected boolean looksLikeISO8601(String dateStr)
609612
{
610-
if (dateStr.length() >= 7 // really need 10, but...
611-
&& Character.isDigit(dateStr.charAt(0))
612-
&& Character.isDigit(dateStr.charAt(3))
613-
&& dateStr.charAt(4) == '-'
614-
&& Character.isDigit(dateStr.charAt(5))
615-
) {
616-
return true;
613+
if (dateStr.length() >= 7) { // really need 10, but...
614+
final char c = dateStr.charAt(0);
615+
// [databind#5429]: extended year may have +/- prefix
616+
if (c == '+' || c == '-') {
617+
return (dateStr.length() >= 11)
618+
&& Character.isDigit(dateStr.charAt(1));
619+
}
620+
return Character.isDigit(c)
621+
&& Character.isDigit(dateStr.charAt(3))
622+
&& dateStr.charAt(4) == '-'
623+
&& Character.isDigit(dateStr.charAt(5));
617624
}
618625
return false;
619626
}
@@ -670,8 +677,20 @@ protected Date _parseAsISO8601(String dateStr, ParsePosition bogus)
670677
} else {
671678
Matcher m = PATTERN_ISO8601.matcher(dateStr);
672679
if (m.matches()) {
673-
// Important! START with optional time zone; otherwise Calendar will explode
680+
// [databind#5429]: handle extended year (5+ digits with optional +/- prefix)
681+
// by locating where year ends (first hyphen for year-month separator)
682+
int yearEnd = (dateStr.charAt(0) == '-') ? dateStr.indexOf('-', 1) : dateStr.indexOf('-');
683+
int year = (yearEnd <= 4) ? _parse4D(dateStr, 0)
684+
: Integer.parseInt(dateStr.substring(0, yearEnd));
685+
final int offset = yearEnd - 4; // adjustment for extended year
686+
int month = _parse2D(dateStr, 5 + offset)-1;
687+
int day = _parse2D(dateStr, 8 + offset);
688+
int hour = _parse2D(dateStr, 11 + offset);
689+
int minute = _parse2D(dateStr, 14 + offset);
690+
int seconds = ((totalLen > (16 + offset)) && dateStr.charAt(16 + offset) == ':')
691+
? _parse2D(dateStr, 17 + offset) : 0;
674692

693+
// Important! START with optional time zone; otherwise Calendar will explode
675694
int start = m.start(2);
676695
int end = m.end(2);
677696
int len = end-start;
@@ -691,21 +710,6 @@ protected Date _parseAsISO8601(String dateStr, ParsePosition bogus)
691710
cal.set(Calendar.DST_OFFSET, 0);
692711
}
693712

694-
int year = _parse4D(dateStr, 0);
695-
int month = _parse2D(dateStr, 5)-1;
696-
int day = _parse2D(dateStr, 8);
697-
698-
// So: 10 chars for date, then `T`, so starts at 11
699-
int hour = _parse2D(dateStr, 11);
700-
int minute = _parse2D(dateStr, 14);
701-
702-
// Seconds are actually optional... so
703-
int seconds;
704-
if ((totalLen > 16) && dateStr.charAt(16) == ':') {
705-
seconds = _parse2D(dateStr, 17);
706-
} else {
707-
seconds = 0;
708-
}
709713
cal.set(year, month, day, hour, minute, seconds);
710714

711715
// Optional milliseconds
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.fasterxml.jackson.databind.deser.jdk;
2+
3+
import java.util.Date;
4+
5+
import org.junit.jupiter.api.Test;
6+
7+
import com.fasterxml.jackson.databind.*;
8+
import com.fasterxml.jackson.databind.json.JsonMapper;
9+
import com.fasterxml.jackson.databind.testutil.DatabindTestUtil;
10+
11+
import static org.junit.jupiter.api.Assertions.assertEquals;
12+
13+
public class DateRoundtrip5429Test extends DatabindTestUtil
14+
{
15+
private final ObjectMapper MAPPER = JsonMapper.builder()
16+
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
17+
.build();
18+
19+
@Test
20+
void testDateRoundTripWithMaxValue() throws Exception {
21+
22+
Date original = new Date(Long.MAX_VALUE);
23+
String json = MAPPER.writeValueAsString(original);
24+
Date parsed = MAPPER.readValue(json, Date.class);
25+
26+
assertEquals(original.getTime(), parsed.getTime());
27+
// but also check actual serialization
28+
assertEquals(q("+292278994-08-17T07:12:55.807+00:00"), json);
29+
}
30+
31+
@Test
32+
void testDateRoundTripWithMinValue() throws Exception {
33+
Date original = new Date(Long.MIN_VALUE);
34+
String json = MAPPER.writeValueAsString(original);
35+
Date parsed = MAPPER.readValue(json, Date.class);
36+
37+
assertEquals(original.getTime(), parsed.getTime());
38+
// but also check actual serialization
39+
assertEquals(q("-292269054-12-02T16:47:04.192+00:00"), json);
40+
}
41+
}

0 commit comments

Comments
 (0)