Skip to content

Commit d28c947

Browse files
committed
Merge pull request #552 from geronimo-iia/master
iso 8601 date format
2 parents 6a70b50 + 2735000 commit d28c947

File tree

2 files changed

+181
-74
lines changed

2 files changed

+181
-74
lines changed

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

Lines changed: 83 additions & 68 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

@@ -20,26 +24,24 @@ public class ISO8601Utils {
2024
*/
2125
private static final TimeZone TIMEZONE_GMT = TimeZone.getTimeZone(GMT_ID);
2226

23-
/*
24-
/**********************************************************
25-
/* Static factories
26-
/**********************************************************
27-
*/
28-
27+
/* ********************************************************* */
28+
/* Static factories */
29+
/* ********************************************************* */
30+
2931
/**
3032
* Accessor for static GMT timezone instance.
3133
*/
32-
public static TimeZone timeZoneGMT() { return TIMEZONE_GMT; }
34+
public static TimeZone timeZoneGMT() {
35+
return TIMEZONE_GMT;
36+
}
37+
38+
/* ********************************************************* */
39+
/* Formatting */
40+
/* ********************************************************* */
3341

34-
/*
35-
/**********************************************************
36-
/* Formatting
37-
/**********************************************************
38-
*/
39-
4042
/**
4143
* Format a date into 'yyyy-MM-ddThh:mm:ssZ' (GMT timezone, no milliseconds precision)
42-
*
44+
*
4345
* @param date the date to format
4446
* @return the date formatted as 'yyyy-MM-ddThh:mm:ssZ'
4547
*/
@@ -49,8 +51,8 @@ public static String format(Date date) {
4951

5052
/**
5153
* Format a date into 'yyyy-MM-ddThh:mm:ss[.sss]Z' (GMT timezone)
52-
*
53-
* @param date the date to format
54+
*
55+
* @param date the date to format
5456
* @param millis true to include millis precision otherwise false
5557
* @return the date formatted as 'yyyy-MM-ddThh:mm:ss[.sss]Z'
5658
*/
@@ -60,10 +62,10 @@ public static String format(Date date, boolean millis) {
6062

6163
/**
6264
* Format date into yyyy-MM-ddThh:mm:ss[.sss][Z|[+-]hh:mm]
63-
*
64-
* @param date the date to format
65+
*
66+
* @param date the date to format
6567
* @param millis true to include millis precision otherwise false
66-
* @param tz timezone to use for the formatting (GMT will produce 'Z')
68+
* @param tz timezone to use for the formatting (GMT will produce 'Z')
6769
* @return the date formatted as yyyy-MM-ddThh:mm:ss[.sss][Z|[+-]hh:mm]
6870
*/
6971
public static String format(Date date, boolean millis, TimeZone tz) {
@@ -107,55 +109,73 @@ public static String format(Date date, boolean millis, TimeZone tz) {
107109
return formatted.toString();
108110
}
109111

110-
/*
111-
/**********************************************************
112-
/* Parsing
113-
/**********************************************************
114-
*/
112+
/* ********************************************************* */
113+
/* Parsing */
114+
/* ********************************************************* */
115115

116116
/**
117-
* Parse a date from ISO-8601 formatted string. It expects a format yyyy-MM-ddThh:mm:ss[.sss][Z|[+-]hh:mm]
118-
*
117+
* Parse a date from ISO-8601 formatted string. It expects a format
118+
* [yyyy-MM-dd|yyyyMMdd][T(hh:mm[:ss[.sss]]|hhmm[ss[.sss]])]?[Z|[+-]hh:mm]]
119+
*
119120
* @param date ISO string to parse in the appropriate format.
120121
* @param pos The position to start parsing from, updated to where parsing stopped.
121122
* @return the parsed date
122123
* @throws ParseException if the date is not in the appropriate format
123124
*/
124-
public static Date parse(String date, ParsePosition pos) throws ParseException
125-
{
125+
public static Date parse(String date, ParsePosition pos) throws ParseException {
126126
Exception fail = null;
127127
try {
128128
int offset = pos.getIndex();
129129

130130
// extract year
131131
int year = parseInt(date, offset, offset += 4);
132-
checkOffset(date, offset, '-');
132+
if (checkOffset(date, offset, '-')) {
133+
offset += 1;
134+
}
133135

134136
// extract month
135-
int month = parseInt(date, offset += 1, offset += 2);
136-
checkOffset(date, offset, '-');
137+
int month = parseInt(date, offset, offset += 2);
138+
if (checkOffset(date, offset, '-')) {
139+
offset += 1;
140+
}
137141

138142
// 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, ':');
143+
int day = parseInt(date, offset, offset += 2);
144+
// default time value
145+
int hour = 0;
146+
int minutes = 0;
147+
int seconds = 0;
148+
int milliseconds = 0; // always use 0 otherwise returned date will include millis of current time
149+
if (checkOffset(date, offset, 'T')) {
145150

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

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);
157+
minutes = parseInt(date, offset, offset += 2);
158+
if (checkOffset(date, offset, ':')) {
159+
offset += 1;
160+
}
161+
// second and milliseconds can be optional
162+
if (date.length() > offset) {
163+
char c = date.charAt(offset);
164+
if (c != 'Z' && c != '+' && c != '-') {
165+
seconds = parseInt(date, offset, offset += 2);
166+
// milliseconds can be optional in the format
167+
if (checkOffset(date, offset, '.')) {
168+
milliseconds = parseInt(date, offset += 1, offset += 3);
169+
}
170+
}
171+
}
155172
}
156173

157174
// extract timezone
158175
String timezoneId;
176+
if (date.length() <= offset) {
177+
throw new IndexOutOfBoundsException("No time zone indicator ");
178+
}
159179
char timezoneIndicator = date.charAt(offset);
160180
if (timezoneIndicator == '+' || timezoneIndicator == '-') {
161181
String timezoneOffset = date.substring(offset);
@@ -185,41 +205,37 @@ public static Date parse(String date, ParsePosition pos) throws ParseException
185205

186206
pos.setIndex(offset);
187207
return calendar.getTime();
188-
//If we get a ParseException it'll already have the right message/offset.
189-
//Other exception types can convert here.
208+
// If we get a ParseException it'll already have the right message/offset.
209+
// Other exception types can convert here.
190210
} catch (IndexOutOfBoundsException e) {
191211
fail = e;
192212
} catch (NumberFormatException e) {
193213
fail = e;
194214
} catch (IllegalArgumentException e) {
195215
fail = e;
196216
}
197-
String input = (date == null) ? null : ('"'+date+"'");
198-
throw new ParseException("Failed to parse date ["+input
199-
+"]: "+fail.getMessage(), pos.getIndex());
217+
String input = (date == null) ? null : ('"' + date + "'");
218+
throw new ParseException("Failed to parse date [" + input + "]: " + fail.getMessage(), pos.getIndex());
200219
}
201220

202221
/**
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
222+
* Check if the expected character exist at the given offset in the value.
223+
*
224+
* @param value the string to check at the specified offset
225+
* @param offset the offset to look for the expected character
207226
* @param expected the expected character
208-
* @throws IndexOutOfBoundsException if the expected character is not found
227+
* @return true if the expected character exist at the given offset
209228
*/
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-
}
229+
private static boolean checkOffset(String value, int offset, char expected) {
230+
return value.length() > offset ? (value.charAt(offset) == expected) : false;
215231
}
216232

217233
/**
218234
* Parse an integer located between 2 given offsets in a string
219-
*
220-
* @param value the string to parse
235+
*
236+
* @param value the string to parse
221237
* @param beginIndex the start index for the integer in the string
222-
* @param endIndex the end index for the integer in the string
238+
* @param endIndex the end index for the integer in the string
223239
* @return the int
224240
* @throws NumberFormatException if the value is not a number
225241
*/
@@ -251,9 +267,9 @@ private static int parseInt(String value, int beginIndex, int endIndex) throws N
251267

252268
/**
253269
* Zero pad a number to a specified length
254-
*
270+
*
255271
* @param buffer buffer to use for padding
256-
* @param value the integer value to pad if necessary.
272+
* @param value the integer value to pad if necessary.
257273
* @param length the length of the string we should zero pad
258274
*/
259275
private static void padInt(StringBuilder buffer, int value, int length) {
@@ -264,4 +280,3 @@ private static void padInt(StringBuilder buffer, int value, int length) {
264280
buffer.append(strValue);
265281
}
266282
}
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)