/* * Copyright 2009 Sun Microsystems, Inc. All Rights Reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. * * This code 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 * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Sun Microsystems, Inc., 4150 Network Circle, Santa Clara, * CA 95054 USA or visit www.sun.com if you need additional information or * have any questions. */ package org.visage.runtime.date; import static java.util.Calendar.*; import java.util.Locale; import java.util.TimeZone; public class DateTimeConverter { private static final String DELIMITERS = "--T::"; private static int FIELD_YEAR = 0; private static int FIELD_MONTH = 1; private static int FIELD_DAY_OF_MONTH = 2; private static int FIELD_HOUR = 3; private static int FIELD_MINUTE = 4; private static int FIELD_SECOND = 5; public static DateTimeEngine parseXMLDateTime(String input) { int index = 0; int[] value = { 0 }; int inputlen = input.length(); int len = DELIMITERS.length(); int[] fields = new int[len + 1]; // parse the yyyy-MM-ddTHH:mm:ss portion for (int i = 0; i <= len; i++) { int x = parseInt(input, index, value); int digits = x - index; int val = value[0]; if (i == FIELD_YEAR) { if (val == 0) { // "0000" is not allowed as a year value. syntaxError(index); } if (val < 0) { digits--; index++; } if (digits < 4 || (digits > 4 && input.charAt(index) == '0')) { // year must have 4 or more digits. No leading 0s // are allowed if year has more than 4 digits. syntaxError(index); } } else if (digits != 2 || val < 0) { syntaxError(index); } index = x; if (i != len && index < inputlen && input.charAt(index++) != DELIMITERS.charAt(i)) { syntaxError(index - 1); } fields[i] = val; } // Handle optional fractional seconds [millisecond] int ms = 0; if (index < inputlen) { char c = input.charAt(index); if (c == '.') { int start = index++; // Accept any trailing "0"s here for SimpleDateFormat // which doesn't support fractional seconds, but // milliseconds with ".SSS" which may have trailing // "0"s. while (index < inputlen && (c = input.charAt(index)) >= '0' && c <= '9') index++; try { float fraction = Float.parseFloat(input.substring(start, index)); ms = (int)(fraction * 1000); // round-down } catch (NumberFormatException e) { throw new IllegalArgumentException(e.toString()); } } } // Check the special time-of-day case "24:00:00" boolean handle24hour = false; if (fields[FIELD_HOUR] == 24) { if (fields[FIELD_MINUTE] != 0 || fields[FIELD_SECOND] != 0 || ms != 0) { throw new IllegalArgumentException("invalid hour of day value"); } // Need to handle 24:00:00 after validation. handle24hour = true; fields[FIELD_HOUR] = 0; } DateTimeEngine engine = DateTimeEngine.getInstance(); // parse the optional time zone part. if (index < inputlen) { char c = input.charAt(index++); if (c == 'Z') { engine.setZone(TimeZone.getTimeZone("UTC")); } else if (c == '-' || c == '+') { int start = index - 1; int x = parseInt(input, index, value); if (x - index != 2) { syntaxError(index); } int val = value[0] * 60; index = x; if (input.charAt(index++) != ':') { syntaxError(index - 1); } x = parseInt(input, index, value); if (x - index != 2 || value[0] > 59) { syntaxError(index); } index = x; val += value[0]; if (val > 14*60) { syntaxError(index); } if (val == 0) { engine.setZone(TimeZone.getTimeZone("UTC")); } else { TimeZone tz = TimeZone.getTimeZone("GMT" + input.substring(start, start + 6)); // Got "GMT"? if (tz.getRawOffset() == 0) { // TODO: subclass java.util.TimeZone in case // no custom time zone support is available in // the Java runtime throw new InternalError("No custom time zone support"); } engine.setZone(tz); } } } // Check if we've consumed the whole string. if (index != inputlen) { syntaxError(index); } // Create a calender calculation engine engine.setDate(fields[FIELD_YEAR], fields[FIELD_MONTH], fields[FIELD_DAY_OF_MONTH]); engine.setTimeOfDay(fields[FIELD_HOUR], fields[FIELD_MINUTE], fields[FIELD_SECOND], ms); if (!engine.validate()) { throw new IllegalArgumentException("invalid date-time"); } if (handle24hour) { engine.setHours(24); } engine.resetNormalized(); return engine; } static final String[] DAY_ABBRS = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; static final String[] MONTH_ABBRS = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" }; private static final int NAME_STD = 0; private static final int NAME_DST = 1; private static final String[][] RFC822ZONE_NAMES = { { "EST", "EDT" }, { "CST", "CDT" }, { "MST", "MDT" }, { "PST", "PDT" }, { "GMT", null }, { "UT", null }, }; private static final int OFFSET_STD = 0; private static final int OFFSET_DST = 1; private static final int[][] RFC822ZONE_OFFSETS = { { -5 * 60 * 60 * 1000, -4 * 60 * 60 * 1000 }, { -6 * 60 * 60 * 1000, -5 * 60 * 60 * 1000 }, { -7 * 60 * 60 * 1000, -6 * 60 * 60 * 1000 }, { -8 * 60 * 60 * 1000, -7 * 60 * 60 * 1000 }, { 0, 0 }, { 0, 0 }, }; private static final int ZONE_ID = 0; private static final int ZONE_ALTSTD = 1; private static final int ZONE_ALTDST = 2; private static final String[][] RFC822ZONE_IDS = { { "America/New_York", "GMT-05:00", "GMT-04:00" }, { "America/Chicago", "GMT-06:00", "GMT-05:00" }, { "America/Denver", "America/Phoenix", "GMT-06:00" }, { "America/Los_Angeles", "GMT-08:00", "GMT-07:00" }, { "GMT", null, null }, { "GMT", null, null }, }; public static String toRFC822String(DateTimeEngine engine) { engine.getInstant(); // normalize StringBuilder sb = new StringBuilder(); sb.append(DAY_ABBRS[engine.getDayOfWeek() - 1]).append(", "); CalendarUtils.sprintf0d(sb, engine.getDayOfMonth(), 2).append(' '); sb.append(MONTH_ABBRS[engine.getMonth() - 1]).append(' '); CalendarUtils.sprintf0d(sb, engine.getYear(), 4).append(' '); CalendarUtils.sprintf0d(sb, engine.getHours(), 2).append(':'); CalendarUtils.sprintf0d(sb, engine.getMinutes(), 2).append(':'); CalendarUtils.sprintf0d(sb, engine.getSeconds(), 2); TimeZone tz = engine.getZone(); if (tz != null) { sb.append(' '); try { // If both the standard abbreviation and the raw GMT // offset match an RFC822 zone, then use its RFC822 // zone name. Otherwise, use the numeric format (e.g., // +0900). If the TimeZone is "Europe/London", its // standard abbreviation matches "GMT", but its // daylight time needs to be converted to +0100. This // fallback is performed in appendRFC822ZoneName(). String tzabbr = tz.getDisplayName(false, TimeZone.SHORT, Locale.US); int rawOffset = tz.getRawOffset(); if (isRFC822ZoneName(tzabbr, rawOffset)) { appendRFC822ZoneName(sb, engine.getZoneOffset(), engine.getDaylightSaving() > 0); } else { appendRFC822ZoneNumeric(sb, engine.getZoneOffset()); } } catch (LinkageError e) { appendRFC822ZoneNumeric(sb, engine.getZoneOffset()); } } return sb.toString(); } public static DateTimeEngine parseRFC822DateTime(String input) { try { DateTimeEngine engine = DateTimeEngine.getInstance(); int index = 0; int dayOfWeek = 0; if (input.charAt(3) == ',') { for (int i = 0; i < DAY_ABBRS.length; i++) { if (input.startsWith(DAY_ABBRS[i])) { dayOfWeek = i + 1; break; } } if (dayOfWeek == 0) { parseError("invalid day name"); } checkDelimiter(input, 4, ' '); index = 5; } int[] value = { 0 }; // parse day of month int x = parseInt(input, index, value); if (x - index > 2) { syntaxError(index); } engine.setDayOfMonth(value[0]); checkDelimiter(input, x++, ' '); index = x; // parse month abbreviation for (int i = 0; i < MONTH_ABBRS.length; i++) { if (input.startsWith(MONTH_ABBRS[i], index)) { engine.setMonth(i + 1); index += 3; break; } } if (index == x) { parseError("invalid month name"); } checkDelimiter(input, index++, ' '); // parse year x = parseInt(input, index, value); int year = value[0]; if (year >= 0 && x - index == 2) { // handle 2-digit year DateTimeEngine e = DateTimeEngine.getInstance(System.currentTimeMillis(), TimeZone.getDefault()); int defaultCenturyStart = e.getYear() - 80; year += (defaultCenturyStart / 100) * 100; if (year < defaultCenturyStart) { year += 100; } } if (year == 0) { parseError("invalid year value"); } if (year < 0) { year++; } engine.setYear(year); index = x; checkDelimiter(input, index++, ' '); // parse time of day (HH:mm[:ss]) int hours = 0; timeofday: for (int i = 0; i < 3; i++) { x = parseInt(input, index, value); if (x - index != 2) { syntaxError(index); } int val = value[0]; boolean foundColon = input.charAt(x) == ':'; switch (i) { case 0: if (!foundColon) { syntaxError(x); } engine.setHours(val); // save hours to determine if DST amount needs to // be adjusted in case the given time zone is kind // of invalid. hours = val; x++; break; case 1: engine.setMinutes(val); if (!foundColon) { // No seconds field index = x; break timeofday; } x++; break; case 2: engine.setSeconds(val); break; } index = x; } checkDelimiter(input, index++, ' '); // parse time zone boolean[] isDST = { false }; String tzid = parseRFC822Zone(input, index, isDST); if (tzid == null) { throw new IllegalArgumentException("invalid time zone"); } TimeZone tz = TimeZone.getTimeZone(tzid); engine.setZone(tz); if (!engine.validate()) { parseError("invalid date-time"); } boolean expectDST = isDST[0]; engine.setDaylightTime(expectDST); long instant = engine.getInstant(); // normalize // In case the calculation result disagrees with the // specified time zone, it's due to either invalid local // time or corresponding non-DST time zone. (No support // for the historical differences in the U.S. time zones.) if ((engine.getDaylightSaving() > 0) != expectDST) { String altTzid = null; for (String[] ids : RFC822ZONE_IDS) { if (tzid.equals(ids[ZONE_ID])) { altTzid = ids[expectDST ? ZONE_ALTDST : ZONE_ALTSTD]; } } if (altTzid == null) { parseError("invalid local time"); } // Recalculate local time tz = TimeZone.getTimeZone(altTzid); if (hours == engine.getHours()) { instant += engine.getDaylightSaving(); } engine = DateTimeEngine.getInstance(instant, tz); } if (dayOfWeek != 0 && engine.getDayOfWeek() != dayOfWeek) { parseError("incorrect day of week"); } engine.resetNormalized(); return engine; } catch (IndexOutOfBoundsException ie) { } throw new IllegalArgumentException(); } /** * Determines if the given time zone abbreviation is "GMT" or one * of the U.S. time zones defined by RFC822 zones. * <code>abbr</code> and <code>offset</code> must agree. * * @param abbr time zone abbreviation such as "PST" * @param offset time zone offset in milliseconds */ private static boolean isRFC822ZoneName(String abbr, int offset) { for (int i = 0; i < RFC822ZONE_OFFSETS.length; i++) { if (offset == RFC822ZONE_OFFSETS[i][OFFSET_STD] && abbr.equals(RFC822ZONE_NAMES[i][NAME_STD])) { return true; } } return false; } private static void appendRFC822ZoneName(StringBuilder sb, int offset, boolean daylight) { int index = daylight ? OFFSET_DST : OFFSET_STD; for (int i = 0; i < RFC822ZONE_OFFSETS.length; i++) { if (offset == RFC822ZONE_OFFSETS[i][index]) { sb.append(RFC822ZONE_NAMES[i][index]); return; } } appendRFC822ZoneNumeric(sb, offset); } private static void appendRFC822ZoneNumeric(StringBuilder sb, int offset) { char sign = '+'; if (offset < 0) { offset = -offset; sign = '-'; } offset /= 60000; sb.append(sign); CalendarUtils.sprintf0d(sb, offset/60, 2); CalendarUtils.sprintf0d(sb, offset%60, 2); } private static String parseRFC822Zone(String input, int index, boolean[] isDST) { String tzid = null; char c = input.charAt(index); if (c == '+' || c == '-') { int[] val = { 0 }; int x = parseInt(input, ++index, val); int value = val[0]; if (x - index != 4 || x != input.length() || (value % 100) >= 60) { throw new IllegalArgumentException(); } if (value != 0) { StringBuilder sb = new StringBuilder(); sb.append("GMT").append(c); CalendarUtils.sprintf0d(sb, value/100, 2).append(':'); CalendarUtils.sprintf0d(sb, value%100, 2); tzid = sb.toString(); } else { tzid = "GMT"; } } else { String name = input.substring(index); if (name.length() == 1) { int offset = 0; char sign; if (c >= 'A' && c < 'J') { offset = c - 'A' + 1; sign = '-'; } else if (c >= 'K' && c <= 'M') { offset = c - 'A'; sign = '-'; } else if (c >= 'N' && c <= 'Y') { offset = c - 'N' + 1; sign = '+'; } else if (c == 'Z') { sign = '+'; } else { throw new IllegalArgumentException(); } StringBuilder sb = new StringBuilder(); sb.append("GMT").append(sign); CalendarUtils.sprintf0d(sb, offset, 2).append(":00"); tzid = sb.toString(); } else { int len = RFC822ZONE_NAMES.length; for (int i = 0; i < RFC822ZONE_NAMES.length; i++) { if (name.equals(RFC822ZONE_NAMES[i][NAME_STD])) { tzid = RFC822ZONE_IDS[i][ZONE_ID]; break; } if (name.equals(RFC822ZONE_NAMES[i][NAME_DST])) { tzid = RFC822ZONE_IDS[i][ZONE_ID]; isDST[0] = true; break; } } } } return tzid; } private static int parseInt(String input, int index, int[] value) { int length = input.length(); int sign = 1; if (index < length && input.charAt(index) == '-') { sign = -1; index++; } int val = 0; char c; while (index < length && (c = input.charAt(index)) >= '0' && c <= '9') { val = val * 10 + (c - '0'); index++; } value[0] = val * sign; return index; } private static void checkDelimiter(String input, int index, char delimiter) { if (input.charAt(index) != delimiter) { syntaxError(index); } } private static void syntaxError(int index) { throw new IllegalArgumentException("syntax error at " + index); } private static void parseError(String msg) { throw new IllegalArgumentException(msg); } }