Skip to content

iso 8601 date format #552

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 23, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
151 changes: 83 additions & 68 deletions src/main/java/com/fasterxml/jackson/databind/util/ISO8601Utils.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@
import java.text.ParseException;

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

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

/*
/**********************************************************
/* Static factories
/**********************************************************
*/

/* ********************************************************* */
/* Static factories */
/* ********************************************************* */

/**
* Accessor for static GMT timezone instance.
*/
public static TimeZone timeZoneGMT() { return TIMEZONE_GMT; }
public static TimeZone timeZoneGMT() {
return TIMEZONE_GMT;
}

/* ********************************************************* */
/* Formatting */
/* ********************************************************* */

/*
/**********************************************************
/* Formatting
/**********************************************************
*/

/**
* Format a date into 'yyyy-MM-ddThh:mm:ssZ' (GMT timezone, no milliseconds precision)
*
*
* @param date the date to format
* @return the date formatted as 'yyyy-MM-ddThh:mm:ssZ'
*/
Expand All @@ -49,8 +51,8 @@ public static String format(Date date) {

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

/**
* Format date into yyyy-MM-ddThh:mm:ss[.sss][Z|[+-]hh:mm]
*
* @param date the date to format
*
* @param date the date to format
* @param millis true to include millis precision otherwise false
* @param tz timezone to use for the formatting (GMT will produce 'Z')
* @param tz timezone to use for the formatting (GMT will produce 'Z')
* @return the date formatted as yyyy-MM-ddThh:mm:ss[.sss][Z|[+-]hh:mm]
*/
public static String format(Date date, boolean millis, TimeZone tz) {
Expand Down Expand Up @@ -107,55 +109,73 @@ public static String format(Date date, boolean millis, TimeZone tz) {
return formatted.toString();
}

/*
/**********************************************************
/* Parsing
/**********************************************************
*/
/* ********************************************************* */
/* Parsing */
/* ********************************************************* */

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

// extract year
int year = parseInt(date, offset, offset += 4);
checkOffset(date, offset, '-');
if (checkOffset(date, offset, '-')) {
offset += 1;
}

// extract month
int month = parseInt(date, offset += 1, offset += 2);
checkOffset(date, offset, '-');
int month = parseInt(date, offset, offset += 2);
if (checkOffset(date, offset, '-')) {
offset += 1;
}

// extract day
int day = parseInt(date, offset += 1, offset += 2);
checkOffset(date, offset, 'T');

// extract hours, minutes, seconds and milliseconds
int hour = parseInt(date, offset += 1, offset += 2);
checkOffset(date, offset, ':');
int day = parseInt(date, offset, offset += 2);
// default time value
int hour = 0;
int minutes = 0;
int seconds = 0;
int milliseconds = 0; // always use 0 otherwise returned date will include millis of current time
if (checkOffset(date, offset, 'T')) {

int minutes = parseInt(date, offset += 1, offset += 2);
checkOffset(date, offset, ':');
// extract hours, minutes, seconds and milliseconds
hour = parseInt(date, offset += 1, offset += 2);
if (checkOffset(date, offset, ':')) {
offset += 1;
}

int seconds = parseInt(date, offset += 1, offset += 2);
// milliseconds can be optional in the format
int milliseconds = 0; // always use 0 otherwise returned date will include millis of current time
if (date.charAt(offset) == '.') {
checkOffset(date, offset, '.');
milliseconds = parseInt(date, offset += 1, offset += 3);
minutes = parseInt(date, offset, offset += 2);
if (checkOffset(date, offset, ':')) {
offset += 1;
}
// second and milliseconds can be optional
if (date.length() > offset) {
char c = date.charAt(offset);
if (c != 'Z' && c != '+' && c != '-') {
seconds = parseInt(date, offset, offset += 2);
// milliseconds can be optional in the format
if (checkOffset(date, offset, '.')) {
milliseconds = parseInt(date, offset += 1, offset += 3);
}
}
}
}

// extract timezone
String timezoneId;
if (date.length() <= offset) {
throw new IndexOutOfBoundsException("No time zone indicator ");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'd be best to use some more refined exception, like ParseException of IllegalArgumentException. I know it's behavioral change, but IndexOutOfBounds is sort of low-level errror.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm agree with you.
But it's not an IndexOutOfBounds that client see, it's a ParseException because of line 217/218.

I can replace the line 177 by:
throw new ParseException("Failed to parse date ["' + date + "']: No time zone indicator", pos.getIndex());

But it break a little the logic of this function,

Let me known what your prefer

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah. Ok, so it is to keep handling working the way it did -- got it. I can see if I can tweak it after merge, but if it gets translated to different exception by code that is fine.

}
char timezoneIndicator = date.charAt(offset);
if (timezoneIndicator == '+' || timezoneIndicator == '-') {
String timezoneOffset = date.substring(offset);
Expand Down Expand Up @@ -185,41 +205,37 @@ public static Date parse(String date, ParsePosition pos) throws ParseException

pos.setIndex(offset);
return calendar.getTime();
//If we get a ParseException it'll already have the right message/offset.
//Other exception types can convert here.
// If we get a ParseException it'll already have the right message/offset.
// Other exception types can convert here.
} catch (IndexOutOfBoundsException e) {
fail = e;
} catch (NumberFormatException e) {
fail = e;
} catch (IllegalArgumentException e) {
fail = e;
}
String input = (date == null) ? null : ('"'+date+"'");
throw new ParseException("Failed to parse date ["+input
+"]: "+fail.getMessage(), pos.getIndex());
String input = (date == null) ? null : ('"' + date + "'");
throw new ParseException("Failed to parse date [" + input + "]: " + fail.getMessage(), pos.getIndex());
}

/**
* Check if the expected character exist at the given offset of the
*
* @param value the string to check at the specified offset
* @param offset the offset to look for the expected character
* Check if the expected character exist at the given offset in the value.
*
* @param value the string to check at the specified offset
* @param offset the offset to look for the expected character
* @param expected the expected character
* @throws IndexOutOfBoundsException if the expected character is not found
* @return true if the expected character exist at the given offset
*/
private static void checkOffset(String value, int offset, char expected) throws ParseException {
char found = value.charAt(offset);
if (found != expected) {
throw new ParseException("Expected '" + expected + "' character but found '" + found + "'", offset);
}
private static boolean checkOffset(String value, int offset, char expected) {
return value.length() > offset ? (value.charAt(offset) == expected) : false;
}

/**
* Parse an integer located between 2 given offsets in a string
*
* @param value the string to parse
*
* @param value the string to parse
* @param beginIndex the start index for the integer in the string
* @param endIndex the end index for the integer in the string
* @param endIndex the end index for the integer in the string
* @return the int
* @throws NumberFormatException if the value is not a number
*/
Expand Down Expand Up @@ -251,9 +267,9 @@ private static int parseInt(String value, int beginIndex, int endIndex) throws N

/**
* Zero pad a number to a specified length
*
*
* @param buffer buffer to use for padding
* @param value the integer value to pad if necessary.
* @param value the integer value to pad if necessary.
* @param length the length of the string we should zero pad
*/
private static void padInt(StringBuilder buffer, int value, int length) {
Expand All @@ -264,4 +280,3 @@ private static void padInt(StringBuilder buffer, int value, int length) {
buffer.append(strValue);
}
}

Original file line number Diff line number Diff line change
@@ -1,28 +1,39 @@
package com.fasterxml.jackson.databind.util;

import java.util.*;
import java.text.ParseException;
import java.text.ParsePosition;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.TimeZone;

import com.fasterxml.jackson.databind.BaseMapTest;
import com.fasterxml.jackson.databind.util.ISO8601Utils;

/**
* @see ISO8601Utils
*/
public class ISO8601UtilsTest extends BaseMapTest
{
public class ISO8601UtilsTest extends BaseMapTest {
private Date date;
private Date dateWithoutTime;
private Date dateZeroMillis;
private Date dateZeroSecondAndMillis;

@Override
public void setUp()
{
public void setUp() {
Calendar cal = new GregorianCalendar(2007, 8 - 1, 13, 19, 51, 23);
cal.setTimeZone(TimeZone.getTimeZone("GMT"));
cal.set(Calendar.MILLISECOND, 789);
date = cal.getTime();
cal.set(Calendar.MILLISECOND, 0);
dateZeroMillis = cal.getTime();
cal.set(Calendar.SECOND, 0);
dateZeroSecondAndMillis = cal.getTime();

cal = new GregorianCalendar(2007, 8 - 1, 13, 0, 0, 0);
cal.set(Calendar.MILLISECOND, 0);
cal.setTimeZone(TimeZone.getTimeZone("GMT"));
dateWithoutTime = cal.getTime();

}

public void testFormat() {
Expand Down Expand Up @@ -58,4 +69,85 @@ public void testParse() throws java.text.ParseException {
assertEquals(date, d);
}

public void testParseShortDate() throws java.text.ParseException {
Date d = ISO8601Utils.parse("20070813T19:51:23.789Z", new ParsePosition(0));
assertEquals(date, d);

d = ISO8601Utils.parse("20070813T19:51:23Z", new ParsePosition(0));
assertEquals(dateZeroMillis, d);

d = ISO8601Utils.parse("20070813T21:51:23.789+02:00", new ParsePosition(0));
assertEquals(date, d);
}

public void testParseShortTime() throws java.text.ParseException {
Date d = ISO8601Utils.parse("2007-08-13T195123.789Z", new ParsePosition(0));
assertEquals(date, d);

d = ISO8601Utils.parse("2007-08-13T195123Z", new ParsePosition(0));
assertEquals(dateZeroMillis, d);

d = ISO8601Utils.parse("2007-08-13T215123.789+02:00", new ParsePosition(0));
assertEquals(date, d);
}

public void testParseShortDateTime() throws java.text.ParseException {
Date d = ISO8601Utils.parse("20070813T195123.789Z", new ParsePosition(0));
assertEquals(date, d);

d = ISO8601Utils.parse("20070813T195123Z", new ParsePosition(0));
assertEquals(dateZeroMillis, d);

d = ISO8601Utils.parse("20070813T215123.789+02:00", new ParsePosition(0));
assertEquals(date, d);
}

public void testParseWithoutTime() throws ParseException {
Date d = ISO8601Utils.parse("2007-08-13Z", new ParsePosition(0));
assertEquals(dateWithoutTime, d);

d = ISO8601Utils.parse("20070813Z", new ParsePosition(0));
assertEquals(dateWithoutTime, d);

d = ISO8601Utils.parse("2007-08-13+00:00", new ParsePosition(0));
assertEquals(dateWithoutTime, d);

d = ISO8601Utils.parse("20070813+00:00", new ParsePosition(0));
assertEquals(dateWithoutTime, d);
}

public void testParseWithoutTimeAndTimeZoneMustFail() {
try {
ISO8601Utils.parse("2007-08-13", new ParsePosition(0));
fail();
} catch (ParseException p) {
}
try {
ISO8601Utils.parse("20070813", new ParsePosition(0));
fail();
} catch (ParseException p) {
}
try {
ISO8601Utils.parse("2007-08-13", new ParsePosition(0));
fail();
} catch (ParseException p) {
}
try {
ISO8601Utils.parse("20070813", new ParsePosition(0));
fail();
} catch (ParseException p) {
}
}


public void testParseOptional() throws java.text.ParseException {
Date d = ISO8601Utils.parse("2007-08-13T19:51Z", new ParsePosition(0));
assertEquals(dateZeroSecondAndMillis, d);

d = ISO8601Utils.parse("2007-08-13T1951Z", new ParsePosition(0));
assertEquals(dateZeroSecondAndMillis, d);

d = ISO8601Utils.parse("2007-08-13T21:51+02:00", new ParsePosition(0));
assertEquals(dateZeroSecondAndMillis, d);
}
}