Skip to content

Commit e1426d9

Browse files
committed
Merge pull request #948 from swankjesse/jwilson_0926_rfc3339
Support any number of millisecond digits. Also leap seconds.
2 parents 25a37d7 + abd505e commit e1426d9

File tree

2 files changed

+95
-5
lines changed

2 files changed

+95
-5
lines changed

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

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package com.fasterxml.jackson.databind.util;
22

3-
import java.util.*;
4-
import java.text.ParsePosition;
53
import java.text.ParseException;
4+
import java.text.ParsePosition;
5+
import java.util.*;
66

77
/**
88
* Utilities methods for manipulating dates in iso8601 format. This is much much faster and GC friendly than using SimpleDateFormat so
@@ -200,9 +200,15 @@ public static Date parse(String date, ParsePosition pos) throws ParseException {
200200
char c = date.charAt(offset);
201201
if (c != 'Z' && c != '+' && c != '-') {
202202
seconds = parseInt(date, offset, offset += 2);
203+
if (seconds > 59 && seconds < 63) seconds = 59; // truncate up to 3 leap seconds
203204
// milliseconds can be optional in the format
204205
if (checkOffset(date, offset, '.')) {
205-
milliseconds = parseInt(date, offset += 1, offset += 3);
206+
offset += 1;
207+
int endOffset = indexOfNonDigit(date, offset + 1); // assume at least one digit
208+
int parseEndOffset = Math.min(endOffset, offset + 3); // parse up to 3 digits
209+
int fraction = parseInt(date, offset, parseEndOffset);
210+
milliseconds = (int) (Math.pow(10, 3 - (parseEndOffset - offset)) * fraction);
211+
offset = endOffset;
206212
}
207213
}
208214
}
@@ -340,4 +346,15 @@ private static void padInt(StringBuilder buffer, int value, int length) {
340346
}
341347
buffer.append(strValue);
342348
}
349+
350+
/**
351+
* Returns the index of the first character in the string that is not a digit, starting at offset.
352+
*/
353+
private static int indexOfNonDigit(String string, int offset) {
354+
for (int i = offset; i < string.length(); i++) {
355+
char c = string.charAt(i);
356+
if (c < '0' || c > '9') return i;
357+
}
358+
return string.length();
359+
}
343360
}

src/test/java/com/fasterxml/jackson/databind/util/ISO8601UtilsTest.java

Lines changed: 75 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
package com.fasterxml.jackson.databind.util;
22

3+
import com.fasterxml.jackson.databind.BaseMapTest;
4+
35
import java.text.ParseException;
46
import java.text.ParsePosition;
57
import java.util.Calendar;
68
import java.util.Date;
79
import java.util.GregorianCalendar;
810
import java.util.TimeZone;
9-
10-
import com.fasterxml.jackson.databind.BaseMapTest;
11+
import java.util.concurrent.TimeUnit;
1112

1213
/**
1314
* @see ISO8601Utils
@@ -126,4 +127,76 @@ public void testParseOptional() throws java.text.ParseException {
126127
d = ISO8601Utils.parse("2007-08-13T21:51+02:00", new ParsePosition(0));
127128
assertEquals(dateZeroSecondAndMillis, d);
128129
}
130+
131+
public void testParseRfc3339Examples() throws java.text.ParseException {
132+
// Two digit milliseconds.
133+
Date d = ISO8601Utils.parse("1985-04-12T23:20:50.52Z", new ParsePosition(0));
134+
assertEquals(newDate(1985, 4, 12, 23, 20, 50, 520, 0), d);
135+
136+
d = ISO8601Utils.parse("1996-12-19T16:39:57-08:00", new ParsePosition(0));
137+
assertEquals(newDate(1996, 12, 19, 16, 39, 57, 0, -8 * 60), d);
138+
139+
// Truncated leap second.
140+
d = ISO8601Utils.parse("1990-12-31T23:59:60Z", new ParsePosition(0));
141+
assertEquals(newDate(1990, 12, 31, 23, 59, 59, 0, 0), d);
142+
143+
// Truncated leap second.
144+
d = ISO8601Utils.parse("1990-12-31T15:59:60-08:00", new ParsePosition(0));
145+
assertEquals(newDate(1990, 12, 31, 15, 59, 59, 0, -8 * 60), d);
146+
147+
// Two digit milliseconds.
148+
d = ISO8601Utils.parse("1937-01-01T12:00:27.87+00:20", new ParsePosition(0));
149+
assertEquals(newDate(1937, 1, 1, 12, 0, 27, 870, 20), d);
150+
}
151+
152+
public void testFractionalSeconds() throws java.text.ParseException {
153+
Date d = ISO8601Utils.parse("1970-01-01T00:00:00.9Z", new ParsePosition(0));
154+
assertEquals(newDate(1970, 1, 1, 0, 0, 0, 900, 0), d);
155+
156+
d = ISO8601Utils.parse("1970-01-01T00:00:00.09Z", new ParsePosition(0));
157+
assertEquals(newDate(1970, 1, 1, 0, 0, 0, 90, 0), d);
158+
159+
d = ISO8601Utils.parse("1970-01-01T00:00:00.009Z", new ParsePosition(0));
160+
assertEquals(newDate(1970, 1, 1, 0, 0, 0, 9, 0), d);
161+
162+
d = ISO8601Utils.parse("1970-01-01T00:00:00.0009Z", new ParsePosition(0));
163+
assertEquals(newDate(1970, 1, 1, 0, 0, 0, 0, 0), d);
164+
165+
d = ISO8601Utils.parse("1970-01-01T00:00:00.2147483647Z", new ParsePosition(0));
166+
assertEquals(newDate(1970, 1, 1, 0, 0, 0, 214, 0), d);
167+
168+
d = ISO8601Utils.parse("1970-01-01T00:00:00.2147483648Z", new ParsePosition(0));
169+
assertEquals(newDate(1970, 1, 1, 0, 0, 0, 214, 0), d);
170+
171+
d = ISO8601Utils.parse("1970-01-01T00:00:00.9+02:00", new ParsePosition(0));
172+
assertEquals(newDate(1970, 1, 1, 0, 0, 0, 900, 2 * 60), d);
173+
174+
d = ISO8601Utils.parse("1970-01-01T00:00:00.09+02:00", new ParsePosition(0));
175+
assertEquals(newDate(1970, 1, 1, 0, 0, 0, 90, 2 * 60), d);
176+
177+
d = ISO8601Utils.parse("1970-01-01T00:00:00.009+02:00", new ParsePosition(0));
178+
assertEquals(newDate(1970, 1, 1, 0, 0, 0, 9, 2 * 60), d);
179+
180+
d = ISO8601Utils.parse("1970-01-01T00:00:00.0009+02:00", new ParsePosition(0));
181+
assertEquals(newDate(1970, 1, 1, 0, 0, 0, 0, 2 * 60), d);
182+
183+
d = ISO8601Utils.parse("1970-01-01T00:00:00.2147483648+02:00", new ParsePosition(0));
184+
assertEquals(newDate(1970, 1, 1, 0, 0, 0, 214, 2 * 60), d);
185+
}
186+
187+
public void testDecimalWithoutDecimalPointButNoFractionalSeconds() throws java.text.ParseException {
188+
try {
189+
ISO8601Utils.parse("1970-01-01T00:00:00.Z", new ParsePosition(0));
190+
fail();
191+
} catch (ParseException expected) {
192+
}
193+
}
194+
195+
private Date newDate(int year, int month, int day, int hour,
196+
int minute, int second, int millis, int timezoneOffsetMinutes) {
197+
Calendar calendar = new GregorianCalendar(TimeZone.getTimeZone("GMT"));
198+
calendar.set(year, month - 1, day, hour, minute, second);
199+
calendar.set(Calendar.MILLISECOND, millis);
200+
return new Date(calendar.getTimeInMillis() - TimeUnit.MINUTES.toMillis(timezoneOffsetMinutes));
201+
}
129202
}

0 commit comments

Comments
 (0)