Index: ISO8601.java
===================================================================
--- ISO8601.java (revision 620098)
+++ ISO8601.java (working copy)
@@ -19,195 +19,248 @@
import java.text.DecimalFormat;
import java.util.Calendar;
import java.util.GregorianCalendar;
+import java.util.NoSuchElementException;
+import java.util.StringTokenizer;
import java.util.TimeZone;
/**
- * The ISO8601 utility class provides helper methods
- * to deal with date/time formatting using a specific ISO8601-compliant
- * format (see ISO 8601).
- *
ISO8601 utility class provides helper methods to deal with date/time formatting using a specific
+ * ISO8601-compliant format (see ISO 8601). The currently
+ * supported format is:
+ *
* - * ±YYYY-MM-DDThh:mm:ss.SSSTZD + * ±YYYY-MM-DDThh:mm:ss.SSSTZD *+ * * where: + * *
- * ±YYYY = four-digit year with optional sign where values <= 0 are - * denoting years BCE and values > 0 are denoting years CE, - * e.g. -0001 denotes the year 2 BCE, 0000 denotes the year 1 BCE, - * 0001 denotes the year 1 CE, and so on... - * MM = two-digit month (01=January, etc.) - * DD = two-digit day of month (01 through 31) - * hh = two digits of hour (00 through 23) (am/pm NOT allowed) - * mm = two digits of minute (00 through 59) - * ss = two digits of second (00 through 59) - * SSS = three digits of milliseconds (000 through 999) - * TZD = time zone designator, Z for Zulu (i.e. UTC) or an offset from UTC - * in the form of +hh:mm or -hh:mm + * ±YYYY = four-digit year with optional sign where values <= 0 are + * denoting years BCE and values > 0 are denoting years CE, + * e.g. -0001 denotes the year 2 BCE, 0000 denotes the year 1 BCE, + * 0001 denotes the year 1 CE, and so on... + * MM = two-digit month (01=January, etc.) + * DD = two-digit day of month (01 through 31) + * hh = two digits of hour (00 through 23) (am/pm NOT allowed) + * mm = two digits of minute (00 through 59) + * ss = two digits of second (00 through 59) + * SSS = three digits of milliseconds (000 through 999) + * TZD = time zone designator, Z for Zulu (i.e. UTC) or an offset from UTC + * in the form of +hh:mm or -hh:mm **/ public final class ISO8601 { - /** - * misc. numeric formats used in formatting - */ + + /** 2-digit numeric format. */ private static final DecimalFormat XX_FORMAT = new DecimalFormat("00"); + + /** 3-digit numeric format. */ private static final DecimalFormat XXX_FORMAT = new DecimalFormat("000"); + + /** 4-digit numeric format. */ private static final DecimalFormat XXXX_FORMAT = new DecimalFormat("0000"); + /** Constant MILISECONDS_IN_SECOND. */ + private static final int MILISECONDS_IN_SECOND = 1000; + + /** Constant SECONDS_IN_MINUTE. */ + private static final int SECONDS_IN_MINUTE = 60; + + /** Constant MINUTES_IN_HOUR. */ + private static final int MINUTES_IN_HOUR = 60; + /** * Parses an ISO8601-compliant date/time string. - * + * * @param text the date/time string to be parsed - * @return a
Calendar, or null if the input could
- * not be parsed
+ * @return a Calendar, or null if the input could not be parsed
* @throws IllegalArgumentException if a null argument is passed
*/
public static Calendar parse(String text) {
+
if (text == null) {
throw new IllegalArgumentException("argument can not be null");
}
- // check optional leading sign
- char sign;
- int start;
- if (text.startsWith("-")) {
- sign = '-';
- start = 1;
- } else if (text.startsWith("+")) {
- sign = '+';
- start = 1;
- } else {
- sign = '+'; // no sign specified, implied '+'
- start = 0;
- }
+ StringTokenizer st = new StringTokenizer(text, "-T:.+Z", true);
- /**
- * the expected format of the remainder of the string is:
- * YYYY-MM-DDThh:mm:ss.SSSTZD
- *
- * note that we cannot use java.text.SimpleDateFormat for
- * parsing because it can't handle years <= 0 and TZD's
- */
+ Calendar calendar = new GregorianCalendar(TimeZone.getTimeZone("UTC"));
+ calendar.clear();
+ calendar.setLenient(false);
- int year, month, day, hour, min, sec, ms;
- String tzID;
- try {
- // year (YYYY)
- year = Integer.parseInt(text.substring(start, start + 4));
- start += 4;
- // delimiter '-'
- if (text.charAt(start) != '-') {
+ if (st.hasMoreElements()) {
+
+ try {
+
+ // year
+ String token = st.nextToken();
+
+ int year;
+ char sign = '+';
+ if (token.matches("[+-]")) {
+ if (st.hasMoreTokens()) {
+ if (token.equals("-")) {
+ sign = '-';
+ }
+ year = Integer.parseInt(st.nextToken());
+ } else {
+ return null;
+ }
+ } else {
+ year = Integer.parseInt(token);
+ }
+
+ if (sign == '-' || year == 0) {
+ calendar.set(Calendar.YEAR, year + 1);
+ calendar.set(Calendar.ERA, GregorianCalendar.BC);
+ } else {
+ calendar.set(Calendar.YEAR, year);
+ calendar.set(Calendar.ERA, GregorianCalendar.AD);
+ }
+
+ // month
+ if (checkToken(st, "-") && (st.hasMoreTokens())) {
+ int month = Integer.parseInt(st.nextToken()) - 1;
+ calendar.set(Calendar.MONTH, month);
+ } else {
+ return null;
+ }
+
+ // day
+ if (checkToken(st, "-") && (st.hasMoreTokens())) {
+ int day = Integer.parseInt(st.nextToken());
+ calendar.set(Calendar.DAY_OF_MONTH, day);
+ } else {
+ return null;
+ }
+
+ // optional time part
+ if (st.hasMoreTokens()) {
+
+ // hour
+ if (checkToken(st, "T") && (st.hasMoreTokens())) {
+ int hour = Integer.parseInt(st.nextToken());
+ calendar.set(Calendar.HOUR_OF_DAY, hour);
+ } else {
+ return null;
+ }
+ // minutes
+ if (checkToken(st, ":") && (st.hasMoreTokens())) {
+ int minutes = Integer.parseInt(st.nextToken());
+ calendar.set(Calendar.MINUTE, minutes);
+ } else {
+ return null;
+ }
+
+ // seconds
+ if (checkToken(st, ":") && (st.hasMoreTokens())) {
+ int seconds = Integer.parseInt(st.nextToken());
+ calendar.set(Calendar.SECOND, seconds);
+ calendar.set(Calendar.MILLISECOND, 0);
+ } else {
+ return null;
+ }
+
+ // optional parts (miliseconds, time zone)
+ if (st.hasMoreTokens()) {
+
+ token = st.nextToken();
+
+ // miliseconds
+ if (token.equals(".")) {
+ if (st.hasMoreTokens()) {
+ int milis = Math.round(Float.parseFloat("0." + st.nextToken()) * MILISECONDS_IN_SECOND);
+ calendar.set(Calendar.MILLISECOND, milis);
+ if (st.hasMoreTokens()) {
+ token = st.nextToken();
+ } else {
+ token = null;
+ }
+ } else {
+ return null;
+ }
+ }
+
+ // GMT time zone
+ if ((token != null) && token.equals("Z")) {
+ calendar.setTimeZone(TimeZone.getTimeZone("GMT"));
+ token = null;
+ }
+
+ // custom time zone
+ if ((token != null) && token.matches("[+-]")) {
+ if (st.hasMoreTokens()) {
+ String hourToken = st.nextToken();
+ if (checkToken(st, ":") && (st.hasMoreTokens())) {
+ String minuteToken = st.nextToken();
+ String tzID = "GMT" + token + hourToken + ":" + minuteToken;
+ TimeZone tz = TimeZone.getTimeZone(tzID);
+ // verify id of returned time zone (getTimeZone defaults to "GMT")
+ if (tz.getID().equals(tzID)) {
+ calendar.setTimeZone(tz);
+ } else {
+ // invalid time zone
+ return null;
+ }
+ token = null;
+ } else {
+ return null;
+ }
+ } else {
+ return null;
+ }
+ }
+
+ if ((token != null) || st.hasMoreTokens()) {
+ return null;
+ }
+
+ }
+
+ }
+
+ } catch (IndexOutOfBoundsException e) {
return null;
- }
- start++;
- // month (MM)
- month = Integer.parseInt(text.substring(start, start + 2));
- start += 2;
- // delimiter '-'
- if (text.charAt(start) != '-') {
+ } catch (NumberFormatException e) {
return null;
- }
- start++;
- // day (DD)
- day = Integer.parseInt(text.substring(start, start + 2));
- start += 2;
- // delimiter 'T'
- if (text.charAt(start) != 'T') {
+ } catch (NoSuchElementException ex) {
return null;
}
- start++;
- // hour (hh)
- hour = Integer.parseInt(text.substring(start, start + 2));
- start += 2;
- // delimiter ':'
- if (text.charAt(start) != ':') {
- return null;
- }
- start++;
- // minute (mm)
- min = Integer.parseInt(text.substring(start, start + 2));
- start += 2;
- // delimiter ':'
- if (text.charAt(start) != ':') {
- return null;
- }
- start++;
- // second (ss)
- sec = Integer.parseInt(text.substring(start, start + 2));
- start += 2;
- // delimiter '.'
- if (text.charAt(start) != '.') {
- return null;
- }
- start++;
- // millisecond (SSS)
- ms = Integer.parseInt(text.substring(start, start + 3));
- start += 3;
- // time zone designator (Z or +00:00 or -00:00)
- if (text.charAt(start) == '+' || text.charAt(start) == '-') {
- // offset to UTC specified in the format +00:00/-00:00
- tzID = "GMT" + text.substring(start);
- } else if (text.substring(start).equals("Z")) {
- tzID = "GMT";
- } else {
- // invalid time zone designator
- return null;
- }
- } catch (IndexOutOfBoundsException e) {
- return null;
- } catch (NumberFormatException e) {
- return null;
- }
- TimeZone tz = TimeZone.getTimeZone(tzID);
- // verify id of returned time zone (getTimeZone defaults to "GMT")
- if (!tz.getID().equals(tzID)) {
- // invalid time zone
+ } else {
return null;
}
- // initialize Calendar object
- Calendar cal = Calendar.getInstance(tz);
- cal.setLenient(false);
- // year and era
- if (sign == '-' || year == 0) {
- // not CE, need to set era (BCE) and adjust year
- cal.set(Calendar.YEAR, year + 1);
- cal.set(Calendar.ERA, GregorianCalendar.BC);
- } else {
- cal.set(Calendar.YEAR, year);
- cal.set(Calendar.ERA, GregorianCalendar.AD);
- }
- // month (0-based!)
- cal.set(Calendar.MONTH, month - 1);
- // day of month
- cal.set(Calendar.DAY_OF_MONTH, day);
- // hour
- cal.set(Calendar.HOUR_OF_DAY, hour);
- // minute
- cal.set(Calendar.MINUTE, min);
- // second
- cal.set(Calendar.SECOND, sec);
- // millisecond
- cal.set(Calendar.MILLISECOND, ms);
-
try {
- /**
- * the following call will trigger an IllegalArgumentException
- * if any of the set values are illegal or out of range
- */
- cal.getTime();
+ // the following call will trigger an IllegalArgumentException if any of
+ // the set values are illegal or out of range
+ calendar.getTime();
} catch (IllegalArgumentException e) {
return null;
}
- return cal;
+ return calendar;
+
}
/**
- * Formats a Calendar value into an ISO8601-compliant
- * date/time string.
- *
+ * Checks if given token is present in tokenizer.
+ *
+ * @param st tokenizer object.
+ * @param token token to check.
+ * @return true if given token is available, false otherwise.
+ */
+ private static boolean checkToken(StringTokenizer st, String token) {
+ try {
+ return st.nextToken().equals(token);
+ } catch (NoSuchElementException ex) {
+ return false;
+ }
+ }
+
+ /**
+ * Formats a Calendar value into an ISO8601-compliant date/time string.
+ *
* @param cal the time value to be formatted into a date/time string.
* @return the formatted date/time string.
* @throws IllegalArgumentException if a null argument is passed
@@ -219,21 +272,16 @@
// determine era and adjust year if necessary
int year = cal.get(Calendar.YEAR);
- if (cal.isSet(Calendar.ERA)
- && cal.get(Calendar.ERA) == GregorianCalendar.BC) {
+ if (cal.isSet(Calendar.ERA) && cal.get(Calendar.ERA) == GregorianCalendar.BC) {
/**
- * calculate year using astronomical system:
- * year n BCE => astronomical year -n + 1
+ * calculate year using astronomical system: year n BCE => astronomical year -n + 1
*/
year = 0 - year + 1;
}
/**
- * the format of the date/time string is:
- * YYYY-MM-DDThh:mm:ss.SSSTZD
- *
- * note that we cannot use java.text.SimpleDateFormat for
- * formatting because it can't handle years <= 0 and TZD's
+ * the format of the date/time string is: YYYY-MM-DDThh:mm:ss.SSSTZD note that we cannot use
+ * java.text.SimpleDateFormat for formatting because it can't handle years <= 0 and TZD's
*/
StringBuffer buf = new StringBuffer();
// year ([-]YYYY)
@@ -261,8 +309,8 @@
// determine offset of timezone from UTC (incl. daylight saving)
int offset = tz.getOffset(cal.getTimeInMillis());
if (offset != 0) {
- int hours = Math.abs((offset / (60 * 1000)) / 60);
- int minutes = Math.abs((offset / (60 * 1000)) % 60);
+ int hours = Math.abs((offset / (SECONDS_IN_MINUTE * MILISECONDS_IN_SECOND)) / MINUTES_IN_HOUR);
+ int minutes = Math.abs((offset / (SECONDS_IN_MINUTE * MILISECONDS_IN_SECOND)) % MINUTES_IN_HOUR);
buf.append(offset < 0 ? '-' : '+');
buf.append(XX_FORMAT.format(hours));
buf.append(':');
@@ -272,4 +320,12 @@
}
return buf.toString();
}
-}
+
+ /**
+ * Singleton constructor. This class should be nevere instantiated.
+ */
+ private ISO8601() {
+ super();
+ }
+
+}
\ No newline at end of file