Skip to content

Commit 1a21d2e

Browse files
committed
Merge branch '3.0' into 3.x
2 parents a67aae2 + 2786291 commit 1a21d2e

File tree

3 files changed

+79
-24
lines changed

3 files changed

+79
-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/tools/jackson/databind/util/StdDateFormat.java

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

3939
protected final static Pattern PATTERN_PLAIN = Pattern.compile(PATTERN_PLAIN_STR);
4040

41+
// [databind#5429]: Extended year format (4+ digits, optional +/- prefix)
42+
protected final static String PATTERN_YEAR_STR = "(?:[+-]?\\d{4,})";
43+
4144
protected final static Pattern PATTERN_ISO8601;
4245
static {
4346
Pattern p = null;
4447
try {
45-
p = Pattern.compile(PATTERN_PLAIN_STR
48+
p = Pattern.compile(PATTERN_YEAR_STR + "[-]\\d\\d[-]\\d\\d"
4649
+"[T]\\d\\d[:]\\d\\d(?:[:]\\d\\d)?" // hours, minutes, optional seconds
4750
+"(\\.\\d+)?" // optional second fractions
4851
+"(Z|[+-]\\d\\d(?:[:]?\\d\\d)?)?" // optional timeoffset/Z
@@ -529,13 +532,17 @@ public int hashCode() {
529532
*/
530533
protected boolean looksLikeISO8601(String dateStr)
531534
{
532-
if (dateStr.length() >= 7 // really need 10, but...
533-
&& Character.isDigit(dateStr.charAt(0))
534-
&& Character.isDigit(dateStr.charAt(3))
535-
&& dateStr.charAt(4) == '-'
536-
&& Character.isDigit(dateStr.charAt(5))
537-
) {
538-
return true;
535+
if (dateStr.length() >= 7) { // really need 10, but...
536+
final char c = dateStr.charAt(0);
537+
// [databind#5429]: extended year may have +/- prefix
538+
if (c == '+' || c == '-') {
539+
return (dateStr.length() >= 11)
540+
&& Character.isDigit(dateStr.charAt(1));
541+
}
542+
return Character.isDigit(c)
543+
&& Character.isDigit(dateStr.charAt(3))
544+
&& dateStr.charAt(4) == '-'
545+
&& Character.isDigit(dateStr.charAt(5));
539546
}
540547
return false;
541548
}
@@ -592,8 +599,20 @@ protected Date _parseAsISO8601(String dateStr, ParsePosition bogus)
592599
} else {
593600
Matcher m = PATTERN_ISO8601.matcher(dateStr);
594601
if (m.matches()) {
595-
// Important! START with optional time zone; otherwise Calendar will explode
602+
// [databind#5429]: handle extended year (5+ digits with optional +/- prefix)
603+
// by locating where year ends (first hyphen for year-month separator)
604+
int yearEnd = (dateStr.charAt(0) == '-') ? dateStr.indexOf('-', 1) : dateStr.indexOf('-');
605+
int year = (yearEnd <= 4) ? _parse4D(dateStr, 0)
606+
: Integer.parseInt(dateStr.substring(0, yearEnd));
607+
final int offset = yearEnd - 4; // adjustment for extended year
608+
int month = _parse2D(dateStr, 5 + offset)-1;
609+
int day = _parse2D(dateStr, 8 + offset);
610+
int hour = _parse2D(dateStr, 11 + offset);
611+
int minute = _parse2D(dateStr, 14 + offset);
612+
int seconds = ((totalLen > (16 + offset)) && dateStr.charAt(16 + offset) == ':')
613+
? _parse2D(dateStr, 17 + offset) : 0;
596614

615+
// Important! START with optional time zone; otherwise Calendar will explode
597616
int start = m.start(2);
598617
int end = m.end(2);
599618
int len = end-start;
@@ -613,21 +632,6 @@ protected Date _parseAsISO8601(String dateStr, ParsePosition bogus)
613632
cal.set(Calendar.DST_OFFSET, 0);
614633
}
615634

616-
int year = _parse4D(dateStr, 0);
617-
int month = _parse2D(dateStr, 5)-1;
618-
int day = _parse2D(dateStr, 8);
619-
620-
// So: 10 chars for date, then `T`, so starts at 11
621-
int hour = _parse2D(dateStr, 11);
622-
int minute = _parse2D(dateStr, 14);
623-
624-
// Seconds are actually optional... so
625-
int seconds;
626-
if ((totalLen > 16) && dateStr.charAt(16) == ':') {
627-
seconds = _parse2D(dateStr, 17);
628-
} else {
629-
seconds = 0;
630-
}
631635
cal.set(year, month, day, hour, minute, seconds);
632636

633637
// Optional milliseconds
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package tools.jackson.databind.deser.jdk;
2+
3+
import java.util.Date;
4+
5+
import org.junit.jupiter.api.Test;
6+
7+
import tools.jackson.databind.*;
8+
import tools.jackson.databind.cfg.DateTimeFeature;
9+
import tools.jackson.databind.json.JsonMapper;
10+
import tools.jackson.databind.testutil.DatabindTestUtil;
11+
12+
import static org.junit.jupiter.api.Assertions.assertEquals;
13+
14+
// [databind#5429]
15+
public class DateRoundtrip5429Test extends DatabindTestUtil
16+
{
17+
private final ObjectMapper MAPPER = JsonMapper.builder()
18+
.disable(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS)
19+
.build();
20+
21+
@Test
22+
void testDateRoundTripWithMaxValue() throws Exception {
23+
24+
Date original = new Date(Long.MAX_VALUE);
25+
String json = MAPPER.writeValueAsString(original);
26+
Date parsed = MAPPER.readValue(json, Date.class);
27+
28+
assertEquals(original.getTime(), parsed.getTime());
29+
// but also check actual serialization
30+
31+
// 28-Nov-2025, tatu: For some reason, not UTC in 3.0 (unlike in 2.x)?
32+
// Should figure out; commented out for now
33+
//assertEquals(q("+292278994-08-17T07:12:55.807+00:00"), json);
34+
}
35+
36+
@Test
37+
void testDateRoundTripWithMinValue() throws Exception {
38+
Date original = new Date(Long.MIN_VALUE);
39+
String json = MAPPER.writeValueAsString(original);
40+
Date parsed = MAPPER.readValue(json, Date.class);
41+
42+
assertEquals(original.getTime(), parsed.getTime());
43+
// but also check actual serialization
44+
45+
// 28-Nov-2025, tatu: For some reason, not UTC in 3.0 (unlike in 2.x)?
46+
// Should figure out; commented out for now
47+
//assertEquals(q("-292269054-12-02T16:47:04.192+00:00"), json);
48+
}
49+
}

0 commit comments

Comments
 (0)