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). - *

- * The currently supported format is: + * The 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
+ *  &plusmnYYYY-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
+ * &plusmnYYYY = 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