Skip to content

Commit 3c2d409

Browse files
author
Jerome Guibert
committed
extends iso8601 format to support parse according expression: [yyyy-MM-dd|yyyyMMdd][T(hh:mm[:ss[.sss]]|hhmm[ss[.sss]])]?[Z|[+-]hh:mm]]
1 parent f95a927 commit 3c2d409

File tree

2 files changed

+176
-67
lines changed

2 files changed

+176
-67
lines changed

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

Lines changed: 78 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,12 @@
55
import java.text.ParseException;
66

77
/**
8-
* Utilities methods for manipulating dates in iso8601 format. This is much much faster and GC friendly than
9-
* using SimpleDateFormat so highly suitable if you (un)serialize lots of date objects.
8+
* Utilities methods for manipulating dates in iso8601 format. This is much much faster and GC friendly than using SimpleDateFormat so
9+
* highly suitable if you (un)serialize lots of date objects.
10+
*
11+
* Supported parse format: [yyyy-MM-dd|yyyyMMdd][T(hh:mm[:ss[.sss]]|hhmm[ss[.sss]])]?[Z|[+-]hh:mm]]
12+
*
13+
* @see http://www.w3.org/TR/NOTE-datetime
1014
*/
1115
public class ISO8601Utils {
1216

@@ -21,25 +25,25 @@ public class ISO8601Utils {
2125
private static final TimeZone TIMEZONE_GMT = TimeZone.getTimeZone(GMT_ID);
2226

2327
/*
24-
/**********************************************************
25-
/* Static factories
26-
/**********************************************************
28+
* /********************************************************** /* Static factories
29+
* /**********************************************************
2730
*/
28-
31+
2932
/**
3033
* Accessor for static GMT timezone instance.
3134
*/
32-
public static TimeZone timeZoneGMT() { return TIMEZONE_GMT; }
35+
public static TimeZone timeZoneGMT() {
36+
return TIMEZONE_GMT;
37+
}
3338

3439
/*
35-
/**********************************************************
36-
/* Formatting
37-
/**********************************************************
40+
* /********************************************************** /* Formatting
41+
* /**********************************************************
3842
*/
39-
43+
4044
/**
4145
* Format a date into 'yyyy-MM-ddThh:mm:ssZ' (GMT timezone, no milliseconds precision)
42-
*
46+
*
4347
* @param date the date to format
4448
* @return the date formatted as 'yyyy-MM-ddThh:mm:ssZ'
4549
*/
@@ -49,8 +53,8 @@ public static String format(Date date) {
4953

5054
/**
5155
* Format a date into 'yyyy-MM-ddThh:mm:ss[.sss]Z' (GMT timezone)
52-
*
53-
* @param date the date to format
56+
*
57+
* @param date the date to format
5458
* @param millis true to include millis precision otherwise false
5559
* @return the date formatted as 'yyyy-MM-ddThh:mm:ss[.sss]Z'
5660
*/
@@ -60,10 +64,10 @@ public static String format(Date date, boolean millis) {
6064

6165
/**
6266
* Format date into yyyy-MM-ddThh:mm:ss[.sss][Z|[+-]hh:mm]
63-
*
64-
* @param date the date to format
67+
*
68+
* @param date the date to format
6569
* @param millis true to include millis precision otherwise false
66-
* @param tz timezone to use for the formatting (GMT will produce 'Z')
70+
* @param tz timezone to use for the formatting (GMT will produce 'Z')
6771
* @return the date formatted as yyyy-MM-ddThh:mm:ss[.sss][Z|[+-]hh:mm]
6872
*/
6973
public static String format(Date date, boolean millis, TimeZone tz) {
@@ -108,54 +112,72 @@ public static String format(Date date, boolean millis, TimeZone tz) {
108112
}
109113

110114
/*
111-
/**********************************************************
112-
/* Parsing
113-
/**********************************************************
115+
* /********************************************************** /* Parsing
116+
* /**********************************************************
114117
*/
115118

116119
/**
117120
* Parse a date from ISO-8601 formatted string. It expects a format yyyy-MM-ddThh:mm:ss[.sss][Z|[+-]hh:mm]
118-
*
121+
*
119122
* @param date ISO string to parse in the appropriate format.
120123
* @param pos The position to start parsing from, updated to where parsing stopped.
121124
* @return the parsed date
122125
* @throws ParseException if the date is not in the appropriate format
123126
*/
124-
public static Date parse(String date, ParsePosition pos) throws ParseException
125-
{
127+
public static Date parse(String date, ParsePosition pos) throws ParseException {
126128
Exception fail = null;
127129
try {
128130
int offset = pos.getIndex();
129131

130132
// extract year
131133
int year = parseInt(date, offset, offset += 4);
132-
checkOffset(date, offset, '-');
134+
if (checkOffset(date, offset, '-')) {
135+
offset += 1;
136+
}
133137

134138
// extract month
135-
int month = parseInt(date, offset += 1, offset += 2);
136-
checkOffset(date, offset, '-');
139+
int month = parseInt(date, offset, offset += 2);
140+
if (checkOffset(date, offset, '-')) {
141+
offset += 1;
142+
}
137143

138144
// extract day
139-
int day = parseInt(date, offset += 1, offset += 2);
140-
checkOffset(date, offset, 'T');
141-
142-
// extract hours, minutes, seconds and milliseconds
143-
int hour = parseInt(date, offset += 1, offset += 2);
144-
checkOffset(date, offset, ':');
145+
int day = parseInt(date, offset, offset += 2);
146+
// default time value
147+
int hour = 0;
148+
int minutes = 0;
149+
int seconds = 0;
150+
int milliseconds = 0; // always use 0 otherwise returned date will include millis of current time
151+
if (checkOffset(date, offset, 'T')) {
145152

146-
int minutes = parseInt(date, offset += 1, offset += 2);
147-
checkOffset(date, offset, ':');
153+
// extract hours, minutes, seconds and milliseconds
154+
hour = parseInt(date, offset += 1, offset += 2);
155+
if (checkOffset(date, offset, ':')) {
156+
offset += 1;
157+
}
148158

149-
int seconds = parseInt(date, offset += 1, offset += 2);
150-
// milliseconds can be optional in the format
151-
int milliseconds = 0; // always use 0 otherwise returned date will include millis of current time
152-
if (date.charAt(offset) == '.') {
153-
checkOffset(date, offset, '.');
154-
milliseconds = parseInt(date, offset += 1, offset += 3);
159+
minutes = parseInt(date, offset, offset += 2);
160+
if (checkOffset(date, offset, ':')) {
161+
offset += 1;
162+
}
163+
// second and milliseconds can be optional
164+
if (date.length() > offset) {
165+
char c = date.charAt(offset);
166+
if (c != 'Z' && c != '+' && c != '-') {
167+
seconds = parseInt(date, offset, offset += 2);
168+
// milliseconds can be optional in the format
169+
if (checkOffset(date, offset, '.')) {
170+
milliseconds = parseInt(date, offset += 1, offset += 3);
171+
}
172+
}
173+
}
155174
}
156175

157176
// extract timezone
158177
String timezoneId;
178+
if (date.length() <= offset) {
179+
throw new IndexOutOfBoundsException("No time zone indicator ");
180+
}
159181
char timezoneIndicator = date.charAt(offset);
160182
if (timezoneIndicator == '+' || timezoneIndicator == '-') {
161183
String timezoneOffset = date.substring(offset);
@@ -185,41 +207,37 @@ public static Date parse(String date, ParsePosition pos) throws ParseException
185207

186208
pos.setIndex(offset);
187209
return calendar.getTime();
188-
//If we get a ParseException it'll already have the right message/offset.
189-
//Other exception types can convert here.
210+
// If we get a ParseException it'll already have the right message/offset.
211+
// Other exception types can convert here.
190212
} catch (IndexOutOfBoundsException e) {
191213
fail = e;
192214
} catch (NumberFormatException e) {
193215
fail = e;
194216
} catch (IllegalArgumentException e) {
195217
fail = e;
196218
}
197-
String input = (date == null) ? null : ('"'+date+"'");
198-
throw new ParseException("Failed to parse date ["+input
199-
+"]: "+fail.getMessage(), pos.getIndex());
219+
String input = (date == null) ? null : ('"' + date + "'");
220+
throw new ParseException("Failed to parse date [" + input + "]: " + fail.getMessage(), pos.getIndex());
200221
}
201222

202223
/**
203-
* Check if the expected character exist at the given offset of the
204-
*
205-
* @param value the string to check at the specified offset
206-
* @param offset the offset to look for the expected character
224+
* Check if the expected character exist at the given offset in the value.
225+
*
226+
* @param value the string to check at the specified offset
227+
* @param offset the offset to look for the expected character
207228
* @param expected the expected character
208-
* @throws IndexOutOfBoundsException if the expected character is not found
229+
* @return true if the expected character exist at the given offset
209230
*/
210-
private static void checkOffset(String value, int offset, char expected) throws ParseException {
211-
char found = value.charAt(offset);
212-
if (found != expected) {
213-
throw new ParseException("Expected '" + expected + "' character but found '" + found + "'", offset);
214-
}
231+
private static boolean checkOffset(String value, int offset, char expected) {
232+
return value.length() > offset ? (value.charAt(offset) == expected) : false;
215233
}
216234

217235
/**
218236
* Parse an integer located between 2 given offsets in a string
219-
*
220-
* @param value the string to parse
237+
*
238+
* @param value the string to parse
221239
* @param beginIndex the start index for the integer in the string
222-
* @param endIndex the end index for the integer in the string
240+
* @param endIndex the end index for the integer in the string
223241
* @return the int
224242
* @throws NumberFormatException if the value is not a number
225243
*/
@@ -251,9 +269,9 @@ private static int parseInt(String value, int beginIndex, int endIndex) throws N
251269

252270
/**
253271
* Zero pad a number to a specified length
254-
*
272+
*
255273
* @param buffer buffer to use for padding
256-
* @param value the integer value to pad if necessary.
274+
* @param value the integer value to pad if necessary.
257275
* @param length the length of the string we should zero pad
258276
*/
259277
private static void padInt(StringBuilder buffer, int value, int length) {
@@ -264,4 +282,3 @@ private static void padInt(StringBuilder buffer, int value, int length) {
264282
buffer.append(strValue);
265283
}
266284
}
267-

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

Lines changed: 98 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,39 @@
11
package com.fasterxml.jackson.databind.util;
22

3-
import java.util.*;
3+
import java.text.ParseException;
44
import java.text.ParsePosition;
5+
import java.util.Calendar;
6+
import java.util.Date;
7+
import java.util.GregorianCalendar;
8+
import java.util.TimeZone;
59

610
import com.fasterxml.jackson.databind.BaseMapTest;
7-
import com.fasterxml.jackson.databind.util.ISO8601Utils;
811

912
/**
1013
* @see ISO8601Utils
1114
*/
12-
public class ISO8601UtilsTest extends BaseMapTest
13-
{
15+
public class ISO8601UtilsTest extends BaseMapTest {
1416
private Date date;
17+
private Date dateWithoutTime;
1518
private Date dateZeroMillis;
19+
private Date dateZeroSecondAndMillis;
1620

1721
@Override
18-
public void setUp()
19-
{
22+
public void setUp() {
2023
Calendar cal = new GregorianCalendar(2007, 8 - 1, 13, 19, 51, 23);
2124
cal.setTimeZone(TimeZone.getTimeZone("GMT"));
2225
cal.set(Calendar.MILLISECOND, 789);
2326
date = cal.getTime();
2427
cal.set(Calendar.MILLISECOND, 0);
2528
dateZeroMillis = cal.getTime();
29+
cal.set(Calendar.SECOND, 0);
30+
dateZeroSecondAndMillis = cal.getTime();
31+
32+
cal = new GregorianCalendar(2007, 8 - 1, 13, 0, 0, 0);
33+
cal.set(Calendar.MILLISECOND, 0);
34+
cal.setTimeZone(TimeZone.getTimeZone("GMT"));
35+
dateWithoutTime = cal.getTime();
36+
2637
}
2738

2839
public void testFormat() {
@@ -58,4 +69,85 @@ public void testParse() throws java.text.ParseException {
5869
assertEquals(date, d);
5970
}
6071

72+
public void testParseShortDate() throws java.text.ParseException {
73+
Date d = ISO8601Utils.parse("20070813T19:51:23.789Z", new ParsePosition(0));
74+
assertEquals(date, d);
75+
76+
d = ISO8601Utils.parse("20070813T19:51:23Z", new ParsePosition(0));
77+
assertEquals(dateZeroMillis, d);
78+
79+
d = ISO8601Utils.parse("20070813T21:51:23.789+02:00", new ParsePosition(0));
80+
assertEquals(date, d);
81+
}
82+
83+
public void testParseShortTime() throws java.text.ParseException {
84+
Date d = ISO8601Utils.parse("2007-08-13T195123.789Z", new ParsePosition(0));
85+
assertEquals(date, d);
86+
87+
d = ISO8601Utils.parse("2007-08-13T195123Z", new ParsePosition(0));
88+
assertEquals(dateZeroMillis, d);
89+
90+
d = ISO8601Utils.parse("2007-08-13T215123.789+02:00", new ParsePosition(0));
91+
assertEquals(date, d);
92+
}
93+
94+
public void testParseShortDateTime() throws java.text.ParseException {
95+
Date d = ISO8601Utils.parse("20070813T195123.789Z", new ParsePosition(0));
96+
assertEquals(date, d);
97+
98+
d = ISO8601Utils.parse("20070813T195123Z", new ParsePosition(0));
99+
assertEquals(dateZeroMillis, d);
100+
101+
d = ISO8601Utils.parse("20070813T215123.789+02:00", new ParsePosition(0));
102+
assertEquals(date, d);
103+
}
104+
105+
public void testParseWithoutTime() throws ParseException {
106+
Date d = ISO8601Utils.parse("2007-08-13Z", new ParsePosition(0));
107+
assertEquals(dateWithoutTime, d);
108+
109+
d = ISO8601Utils.parse("20070813Z", new ParsePosition(0));
110+
assertEquals(dateWithoutTime, d);
111+
112+
d = ISO8601Utils.parse("2007-08-13+00:00", new ParsePosition(0));
113+
assertEquals(dateWithoutTime, d);
114+
115+
d = ISO8601Utils.parse("20070813+00:00", new ParsePosition(0));
116+
assertEquals(dateWithoutTime, d);
117+
}
118+
119+
public void testParseWithoutTimeAndTimeZoneMustFail() {
120+
try {
121+
ISO8601Utils.parse("2007-08-13", new ParsePosition(0));
122+
fail();
123+
} catch (ParseException p) {
124+
}
125+
try {
126+
ISO8601Utils.parse("20070813", new ParsePosition(0));
127+
fail();
128+
} catch (ParseException p) {
129+
}
130+
try {
131+
ISO8601Utils.parse("2007-08-13", new ParsePosition(0));
132+
fail();
133+
} catch (ParseException p) {
134+
}
135+
try {
136+
ISO8601Utils.parse("20070813", new ParsePosition(0));
137+
fail();
138+
} catch (ParseException p) {
139+
}
140+
}
141+
142+
143+
public void testParseOptional() throws java.text.ParseException {
144+
Date d = ISO8601Utils.parse("2007-08-13T19:51Z", new ParsePosition(0));
145+
assertEquals(dateZeroSecondAndMillis, d);
146+
147+
d = ISO8601Utils.parse("2007-08-13T1951Z", new ParsePosition(0));
148+
assertEquals(dateZeroSecondAndMillis, d);
149+
150+
d = ISO8601Utils.parse("2007-08-13T21:51+02:00", new ParsePosition(0));
151+
assertEquals(dateZeroSecondAndMillis, d);
152+
}
61153
}

0 commit comments

Comments
 (0)