/* This file is part of RouteConverter. RouteConverter is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. RouteConverter is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with RouteConverter; if not, write to the Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA Copyright (C) 2007 Christian Pesch. All Rights Reserved. */ package slash.common.type; import java.text.DecimalFormat; import java.util.Calendar; import java.util.TimeZone; import static java.lang.Character.isDigit; import static java.lang.Integer.parseInt; import static java.util.Calendar.*; import static java.util.GregorianCalendar.AD; import static java.util.GregorianCalendar.BC; import static java.util.TimeZone.getTimeZone; import static slash.common.type.CompactCalendar.UTC; /** * The <code>ISO8601</code> utility class provides helper methods * to deal with date/time formatting using a specific ISO8601-compliant * format (see <a href="http://www.w3.org/TR/NOTE-datetime">ISO 8601</a>). * * The currently supported format is: * <pre> * +-YYY-MM-DDThh:mm:ss[.SSS]TZD * </pre> * where: * <pre> * +-YYYY = four-digit year with optional sign where values ;lteq; 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 = optionally: three digits of milliseconds (000 through 999) * TZD = time zone designator (Z or +hh:mm or -hh:mm) * </pre> * * @author Unknown */ public final class ISO8601 { private static final DecimalFormat XX_FORMAT = new DecimalFormat("00"); private static final DecimalFormat XXX_FORMAT = new DecimalFormat("000"); private static final DecimalFormat XXXX_FORMAT = new DecimalFormat("0000"); /** * Parses an ISO8601-compliant date/time string. * * @param text the date/time string to be parsed * @return a <code>Calendar</code>, or <code>null</code> if the input could * not be parsed * @throws IllegalArgumentException if a <code>null</code> argument is passed */ public static Calendar parseDate(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; } /* * the expected format of the remainder of the string is: * YYYY-MM-DDThh:mm:ss * * note that we cannot use java.text.SimpleDateFormat for * parsing because it can't handle years <= 0 and TZD's */ TimeZone timeZone; int year, month, day, hour, minutes, seconds, milliseconds = 0; try { // year (YYYY) year = parseInt(text.substring(start, start + 4)); start += 4; // delimiter '-' if (text.charAt(start) != '-') { return null; } start++; // month (MM) month = parseInt(text.substring(start, start + 2)); start += 2; // delimiter '-' if (text.charAt(start) != '-') { return null; } start++; // day (DD) day = parseInt(text.substring(start, start + 2)); start += 2; // delimiter 'T' if (text.charAt(start) != 'T') { return null; } start++; // hour (hh) hour = parseInt(text.substring(start, start + 2)); start += 2; // delimiter ':' if (text.charAt(start) != ':') { return null; } start++; // minute (mm) minutes = parseInt(text.substring(start, start + 2)); start += 2; // delimiter ':' if (text.charAt(start) != ':') { return null; } start++; // second (ss) seconds = parseInt(text.substring(start, start + 2)); start += 2; // delimiter '.' 'Z' '+' (or 'T') char delimiter = text.charAt(start++); if (delimiter == '.') { // milliseconds (S), (SS), (SSS) StringBuilder buffer = new StringBuilder(); while (true) { delimiter = text.charAt(start++); if (isDigit(delimiter)) buffer.append(delimiter); else break; } while (buffer.length() < 3) { buffer.append('0'); } milliseconds = parseInt(buffer.toString()); } if (delimiter == 'T') delimiter = '+'; if (delimiter == 'Z') { timeZone = UTC; } else if (delimiter == '+' || delimiter == '-') { // delimiter hour (hh) ':' minute (mm) String tzString = text.substring(start, start + 5); timeZone = getTimeZone("GMT" + delimiter + tzString); } else return null; } catch (IndexOutOfBoundsException | NumberFormatException e) { return null; } // initialize Calendar object Calendar calendar = Calendar.getInstance(timeZone); calendar.setLenient(false); // year and era if (sign == '-' || year == 0) { // not CE, need to set era (BCE) and adjust year calendar.set(YEAR, year + 1); calendar.set(ERA, BC); } else { calendar.set(YEAR, year); calendar.set(ERA, AD); } // month (0-based!) calendar.set(MONTH, month - 1); // day of month calendar.set(DAY_OF_MONTH, day); // hour calendar.set(HOUR_OF_DAY, hour); // minute calendar.set(MINUTE, minutes); // second calendar.set(SECOND, seconds); // millisecond calendar.set(MILLISECOND, milliseconds); try { /* * 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 calendar; } /** * Formats a {@link CompactCalendar} value into an ISO8601-compliant date/time string. * * @param calendar the time value to be formatted into a date/time string * @return the formatted date/time string * @throws IllegalArgumentException if a <code>null</code> argument is passed */ public static String formatDate(CompactCalendar calendar) { if (calendar == null) { throw new IllegalArgumentException("argument can not be null"); } return formatDate(calendar.getCalendar(), false); } /** * Formats a {@link Calendar} value into an ISO8601-compliant date/time string. * * @param calendar the time value to be formatted into a date/time string * @param includeMilliseconds if milli seconds should be included although the spec does not include them * @return the formatted date/time string * @throws IllegalArgumentException if a <code>null</code> argument is passed */ public static String formatDate(Calendar calendar, boolean includeMilliseconds) { if (calendar == null) { throw new IllegalArgumentException("argument can not be null"); } // determine era and adjust year if necessary int year = calendar.get(YEAR); if (calendar.isSet(ERA) && calendar.get(ERA) == BC) { /* * 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 * * note that we cannot use java.text.SimpleDateFormat for * formatting because it can't handle years <= 0 and TZD's */ StringBuilder buffer = new StringBuilder(); // year ([-]YYYY) buffer.append(XXXX_FORMAT.format(year)); buffer.append('-'); // month (MM) buffer.append(XX_FORMAT.format(calendar.get(MONTH) + 1)); buffer.append('-'); // day (DD) buffer.append(XX_FORMAT.format(calendar.get(DAY_OF_MONTH))); buffer.append('T'); // hour (hh) buffer.append(XX_FORMAT.format(calendar.get(HOUR_OF_DAY))); buffer.append(':'); // minute (mm) buffer.append(XX_FORMAT.format(calendar.get(MINUTE))); buffer.append(':'); // second (ss) buffer.append(XX_FORMAT.format(calendar.get(SECOND))); if (includeMilliseconds) { // millisecond (SSS) buffer.append('.'); buffer.append(XXX_FORMAT.format(calendar.get(MILLISECOND))); } if (calendar.getTimeZone().equals(UTC)) buffer.append('Z'); else { buffer.append('+'); int offsetHours = calendar.getTimeZone().getRawOffset() / 1000 / 3600; int offsetMinutes = calendar.getTimeZone().getRawOffset() / 1000 / 60 - offsetHours * 60; buffer.append(XX_FORMAT.format(offsetHours)); buffer.append(':'); buffer.append(XX_FORMAT.format(offsetMinutes)); } return buffer.toString(); } }