Created
August 27, 2014 02:53
-
-
Save guileen/c1dc4cc63b8659952f90 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//// https://github.com/FasterXML/jackson-databind/blob/master/src/main/java/com/fasterxml/jackson/databind/util/ISO8601Utils.java | |
package com.fasterxml.jackson.databind.util; | |
import java.util.*; | |
import java.text.ParsePosition; | |
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. | |
*/ | |
public class ISO8601Utils { | |
/** | |
* ID to represent the 'GMT' string | |
*/ | |
private static final String GMT_ID = "GMT"; | |
/** | |
* The GMT timezone | |
*/ | |
private static final TimeZone TIMEZONE_GMT = TimeZone.getTimeZone(GMT_ID); | |
/* | |
/********************************************************** | |
/* Static factories | |
/********************************************************** | |
*/ | |
/** | |
* Accessor for static GMT timezone instance. | |
*/ | |
public static TimeZone timeZoneGMT() { return TIMEZONE_GMT; } | |
/* | |
/********************************************************** | |
/* 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' | |
*/ | |
public static String format(Date date) { | |
return format(date, false, TIMEZONE_GMT); | |
} | |
/** | |
* Format a date into 'yyyy-MM-ddThh:mm:ss[.sss]Z' (GMT timezone) | |
* | |
* @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' | |
*/ | |
public static String format(Date date, boolean millis) { | |
return format(date, millis, TIMEZONE_GMT); | |
} | |
/** | |
* Format date into yyyy-MM-ddThh:mm:ss[.sss][Z|[+-]hh:mm] | |
* | |
* @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') | |
* @return the date formatted as yyyy-MM-ddThh:mm:ss[.sss][Z|[+-]hh:mm] | |
*/ | |
public static String format(Date date, boolean millis, TimeZone tz) { | |
Calendar calendar = new GregorianCalendar(tz, Locale.US); | |
calendar.setTime(date); | |
// estimate capacity of buffer as close as we can (yeah, that's pedantic ;) | |
int capacity = "yyyy-MM-ddThh:mm:ss".length(); | |
capacity += millis ? ".sss".length() : 0; | |
capacity += tz.getRawOffset() == 0 ? "Z".length() : "+hh:mm".length(); | |
StringBuilder formatted = new StringBuilder(capacity); | |
padInt(formatted, calendar.get(Calendar.YEAR), "yyyy".length()); | |
formatted.append('-'); | |
padInt(formatted, calendar.get(Calendar.MONTH) + 1, "MM".length()); | |
formatted.append('-'); | |
padInt(formatted, calendar.get(Calendar.DAY_OF_MONTH), "dd".length()); | |
formatted.append('T'); | |
padInt(formatted, calendar.get(Calendar.HOUR_OF_DAY), "hh".length()); | |
formatted.append(':'); | |
padInt(formatted, calendar.get(Calendar.MINUTE), "mm".length()); | |
formatted.append(':'); | |
padInt(formatted, calendar.get(Calendar.SECOND), "ss".length()); | |
if (millis) { | |
formatted.append('.'); | |
padInt(formatted, calendar.get(Calendar.MILLISECOND), "sss".length()); | |
} | |
int offset = tz.getOffset(calendar.getTimeInMillis()); | |
if (offset != 0) { | |
int hours = Math.abs((offset / (60 * 1000)) / 60); | |
int minutes = Math.abs((offset / (60 * 1000)) % 60); | |
formatted.append(offset < 0 ? '-' : '+'); | |
padInt(formatted, hours, "hh".length()); | |
formatted.append(':'); | |
padInt(formatted, minutes, "mm".length()); | |
} else { | |
formatted.append('Z'); | |
} | |
return formatted.toString(); | |
} | |
/* | |
/********************************************************** | |
/* Parsing | |
/********************************************************** | |
*/ | |
/** | |
* Parse a date from ISO-8601 formatted string. It expects a format yyyy-MM-ddThh:mm: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 | |
{ | |
Exception fail = null; | |
try { | |
int offset = pos.getIndex(); | |
// extract year | |
int year = parseInt(date, offset, offset += 4); | |
checkOffset(date, offset, '-'); | |
// extract month | |
int month = parseInt(date, offset += 1, offset += 2); | |
checkOffset(date, offset, '-'); | |
// 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 minutes = parseInt(date, offset += 1, offset += 2); | |
checkOffset(date, offset, ':'); | |
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); | |
} | |
// extract timezone | |
String timezoneId; | |
char timezoneIndicator = date.charAt(offset); | |
if (timezoneIndicator == '+' || timezoneIndicator == '-') { | |
String timezoneOffset = date.substring(offset); | |
timezoneId = GMT_ID + timezoneOffset; | |
offset += timezoneOffset.length(); | |
} else if (timezoneIndicator == 'Z') { | |
timezoneId = GMT_ID; | |
offset += 1; | |
} else { | |
throw new IndexOutOfBoundsException("Invalid time zone indicator " + timezoneIndicator); | |
} | |
TimeZone timezone = TimeZone.getTimeZone(timezoneId); | |
if (!timezone.getID().equals(timezoneId)) { | |
throw new IndexOutOfBoundsException(); | |
} | |
Calendar calendar = new GregorianCalendar(timezone); | |
calendar.setLenient(false); | |
calendar.set(Calendar.YEAR, year); | |
calendar.set(Calendar.MONTH, month - 1); | |
calendar.set(Calendar.DAY_OF_MONTH, day); | |
calendar.set(Calendar.HOUR_OF_DAY, hour); | |
calendar.set(Calendar.MINUTE, minutes); | |
calendar.set(Calendar.SECOND, seconds); | |
calendar.set(Calendar.MILLISECOND, milliseconds); | |
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. | |
} 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()); | |
} | |
/** | |
* 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 | |
* @param expected the expected character | |
* @throws IndexOutOfBoundsException if the expected character is not found | |
*/ | |
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); | |
} | |
} | |
/** | |
* Parse an integer located between 2 given offsets in a string | |
* | |
* @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 | |
* @return the int | |
* @throws NumberFormatException if the value is not a number | |
*/ | |
private static int parseInt(String value, int beginIndex, int endIndex) throws NumberFormatException { | |
if (beginIndex < 0 || endIndex > value.length() || beginIndex > endIndex) { | |
throw new NumberFormatException(value); | |
} | |
// use same logic as in Integer.parseInt() but less generic we're not supporting negative values | |
int i = beginIndex; | |
int result = 0; | |
int digit; | |
if (i < endIndex) { | |
digit = Character.digit(value.charAt(i++), 10); | |
if (digit < 0) { | |
throw new NumberFormatException("Invalid number: " + value); | |
} | |
result = -digit; | |
} | |
while (i < endIndex) { | |
digit = Character.digit(value.charAt(i++), 10); | |
if (digit < 0) { | |
throw new NumberFormatException("Invalid number: " + value); | |
} | |
result *= 10; | |
result -= digit; | |
} | |
return -result; | |
} | |
/** | |
* 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 length the length of the string we should zero pad | |
*/ | |
private static void padInt(StringBuilder buffer, int value, int length) { | |
String strValue = Integer.toString(value); | |
for (int i = length - strValue.length(); i > 0; i--) { | |
buffer.append('0'); | |
} | |
buffer.append(strValue); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment