/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package freemarker.template.utility;

import java.text.ParseException;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

Date and time related utilities.
/** * Date and time related utilities. */
public class DateUtil {
Show hours (24h); always 2 digits, like 00, 05, etc.
/** * Show hours (24h); always 2 digits, like {@code 00}, {@code 05}, etc. */
public static final int ACCURACY_HOURS = 4;
Show hours and minutes (even if minutes is 00).
/** * Show hours and minutes (even if minutes is 00). */
public static final int ACCURACY_MINUTES = 5;
Show hours, minutes and seconds (even if seconds is 00).
/** * Show hours, minutes and seconds (even if seconds is 00). */
public static final int ACCURACY_SECONDS = 6;
Show hours, minutes and seconds and up to 3 fraction second digits, without trailing 0-s in the fraction part.
/** * Show hours, minutes and seconds and up to 3 fraction second digits, without trailing 0-s in the fraction part. */
public static final int ACCURACY_MILLISECONDS = 7;
Show hours, minutes and seconds and exactly 3 fraction second digits (even if it's 000)
/** * Show hours, minutes and seconds and exactly 3 fraction second digits (even if it's 000) */
public static final int ACCURACY_MILLISECONDS_FORCED = 8; public static final TimeZone UTC = TimeZone.getTimeZone("UTC"); private static final String REGEX_XS_TIME_ZONE = "Z|(?:[-+][0-9]{2}:[0-9]{2})"; private static final String REGEX_ISO8601_BASIC_TIME_ZONE = "Z|(?:[-+][0-9]{2}(?:[0-9]{2})?)"; private static final String REGEX_ISO8601_EXTENDED_TIME_ZONE = "Z|(?:[-+][0-9]{2}(?::[0-9]{2})?)"; private static final String REGEX_XS_OPTIONAL_TIME_ZONE = "(" + REGEX_XS_TIME_ZONE + ")?"; private static final String REGEX_ISO8601_BASIC_OPTIONAL_TIME_ZONE = "(" + REGEX_ISO8601_BASIC_TIME_ZONE + ")?"; private static final String REGEX_ISO8601_EXTENDED_OPTIONAL_TIME_ZONE = "(" + REGEX_ISO8601_EXTENDED_TIME_ZONE + ")?"; private static final String REGEX_XS_DATE_BASE = "(-?[0-9]+)-([0-9]{2})-([0-9]{2})"; private static final String REGEX_ISO8601_BASIC_DATE_BASE = "(-?[0-9]{4,}?)([0-9]{2})([0-9]{2})"; private static final String REGEX_ISO8601_EXTENDED_DATE_BASE = "(-?[0-9]{4,})-([0-9]{2})-([0-9]{2})"; private static final String REGEX_XS_TIME_BASE = "([0-9]{2}):([0-9]{2}):([0-9]{2})(?:\\.([0-9]+))?"; private static final String REGEX_ISO8601_BASIC_TIME_BASE = "([0-9]{2})(?:([0-9]{2})(?:([0-9]{2})(?:[\\.,]([0-9]+))?)?)?"; private static final String REGEX_ISO8601_EXTENDED_TIME_BASE = "([0-9]{2})(?::([0-9]{2})(?::([0-9]{2})(?:[\\.,]([0-9]+))?)?)?"; private static final Pattern PATTERN_XS_DATE = Pattern.compile( REGEX_XS_DATE_BASE + REGEX_XS_OPTIONAL_TIME_ZONE); private static final Pattern PATTERN_ISO8601_BASIC_DATE = Pattern.compile( REGEX_ISO8601_BASIC_DATE_BASE); // No time zone allowed here private static final Pattern PATTERN_ISO8601_EXTENDED_DATE = Pattern.compile( REGEX_ISO8601_EXTENDED_DATE_BASE); // No time zone allowed here private static final Pattern PATTERN_XS_TIME = Pattern.compile( REGEX_XS_TIME_BASE + REGEX_XS_OPTIONAL_TIME_ZONE); private static final Pattern PATTERN_ISO8601_BASIC_TIME = Pattern.compile( REGEX_ISO8601_BASIC_TIME_BASE + REGEX_ISO8601_BASIC_OPTIONAL_TIME_ZONE); private static final Pattern PATTERN_ISO8601_EXTENDED_TIME = Pattern.compile( REGEX_ISO8601_EXTENDED_TIME_BASE + REGEX_ISO8601_EXTENDED_OPTIONAL_TIME_ZONE); private static final Pattern PATTERN_XS_DATE_TIME = Pattern.compile( REGEX_XS_DATE_BASE + "T" + REGEX_XS_TIME_BASE + REGEX_XS_OPTIONAL_TIME_ZONE); private static final Pattern PATTERN_ISO8601_BASIC_DATE_TIME = Pattern.compile( REGEX_ISO8601_BASIC_DATE_BASE + "T" + REGEX_ISO8601_BASIC_TIME_BASE + REGEX_ISO8601_BASIC_OPTIONAL_TIME_ZONE); private static final Pattern PATTERN_ISO8601_EXTENDED_DATE_TIME = Pattern.compile( REGEX_ISO8601_EXTENDED_DATE_BASE + "T" + REGEX_ISO8601_EXTENDED_TIME_BASE + REGEX_ISO8601_EXTENDED_OPTIONAL_TIME_ZONE); private static final Pattern PATTERN_XS_TIME_ZONE = Pattern.compile( REGEX_XS_TIME_ZONE); private static final String MSG_YEAR_0_NOT_ALLOWED = "Year 0 is not allowed in XML schema dates. BC 1 is -1, AD 1 is 1."; private DateUtil() { // can't be instantiated }
Returns the time zone object for the name (or ID). This differs from TimeZone.getTimeZone(String) in that the latest returns GMT if it doesn't recognize the name, while this throws an UnrecognizedTimeZoneException.
Throws:
/** * Returns the time zone object for the name (or ID). This differs from * {@link TimeZone#getTimeZone(String)} in that the latest returns GMT * if it doesn't recognize the name, while this throws an * {@link UnrecognizedTimeZoneException}. * * @throws UnrecognizedTimeZoneException If the time zone name wasn't understood */
public static TimeZone getTimeZone(String name) throws UnrecognizedTimeZoneException { if (isGMTish(name)) { if (name.equalsIgnoreCase("UTC")) { return UTC; } return TimeZone.getTimeZone(name); } TimeZone tz = TimeZone.getTimeZone(name); if (isGMTish(tz.getID())) { throw new UnrecognizedTimeZoneException(name); } return tz; }
Tells if a offset or time zone is GMT. GMT is a fuzzy term, it used to referred both to UTC and UT1.
/** * Tells if a offset or time zone is GMT. GMT is a fuzzy term, it used to * referred both to UTC and UT1. */
private static boolean isGMTish(String name) { if (name.length() < 3) { return false; } char c1 = name.charAt(0); char c2 = name.charAt(1); char c3 = name.charAt(2); if ( !( (c1 == 'G' || c1 == 'g') && (c2 == 'M' || c2 == 'm') && (c3 == 'T' || c3 == 't') ) && !( (c1 == 'U' || c1 == 'u') && (c2 == 'T' || c2 == 't') && (c3 == 'C' || c3 == 'c') ) && !( (c1 == 'U' || c1 == 'u') && (c2 == 'T' || c2 == 't') && (c3 == '1') ) ) { return false; } if (name.length() == 3) { return true; } String offset = name.substring(3); if (offset.startsWith("+")) { return offset.equals("+0") || offset.equals("+00") || offset.equals("+00:00"); } else { return offset.equals("-0") || offset.equals("-00") || offset.equals("-00:00"); } }
Format a date, time or dateTime with one of the ISO 8601 extended formats that is also compatible with the XML Schema format (as far as you don't have dates in the BC era). Examples of possible outputs: "2005-11-27T15:30:00+02:00", "2005-11-27", "15:30:00Z". Note the ":00" in the time zone offset; this is not required by ISO 8601, but included for compatibility with the XML Schema format. Regarding the B.C. issue, those dates will be one year off when read back according the XML Schema format, because of a mismatch between that format and ISO 8601:2000 Second Edition.

This method is thread-safe.

Params:
  • date – the date to convert to ISO 8601 string
  • datePart – whether the date part (year, month, day) will be included or not
  • timePart – whether the time part (hours, minutes, seconds, milliseconds) will be included or not
  • offsetPart – whether the time zone offset part will be included or not. This will be shown as an offset to UTC (examples: "+01", "-02", "+04:30") or as "Z" for UTC (and for UT1 and for GMT+00, since the Java platform doesn't really care about the difference). Note that this can't be true when timePart is false, because ISO 8601 (2004) doesn't mention such patterns.
  • accuracy – tells which parts of the date/time to drop. The datePart and timePart parameters are stronger than this. Note that when ACCURACY_MILLISECONDS is specified, the milliseconds part will be displayed as fraction seconds (like "15:30.00.25") with the minimum number of digits needed to show the milliseconds without precision lose. Thus, if the milliseconds happen to be exactly 0, no fraction seconds will be shown at all.
  • timeZone – the time zone in which the date/time will be shown. (You may find UTC handy here.) Note that although date-only formats has no time zone offset part, the result still depends on the time zone, as days start and end at different points on the time line in different zones.
  • calendarFactory – the factory that will create the calendar used internally for calculations. The point of this parameter is that creating a new calendar is relatively expensive, so it's desirable to reuse calendars and only set their time and zone. (This was tested on Sun JDK 1.6 x86 Win, where it gave 2x-3x speedup.)
/** * Format a date, time or dateTime with one of the ISO 8601 extended * formats that is also compatible with the XML Schema format (as far as you * don't have dates in the BC era). Examples of possible outputs: * {@code "2005-11-27T15:30:00+02:00"}, {@code "2005-11-27"}, * {@code "15:30:00Z"}. Note the {@code ":00"} in the time zone offset; * this is not required by ISO 8601, but included for compatibility with * the XML Schema format. Regarding the B.C. issue, those dates will be * one year off when read back according the XML Schema format, because of a * mismatch between that format and ISO 8601:2000 Second Edition. * * <p>This method is thread-safe. * * @param date the date to convert to ISO 8601 string * @param datePart whether the date part (year, month, day) will be included * or not * @param timePart whether the time part (hours, minutes, seconds, * milliseconds) will be included or not * @param offsetPart whether the time zone offset part will be included or * not. This will be shown as an offset to UTC (examples: * {@code "+01"}, {@code "-02"}, {@code "+04:30"}) or as {@code "Z"} * for UTC (and for UT1 and for GMT+00, since the Java platform * doesn't really care about the difference). * Note that this can't be {@code true} when {@code timePart} is * {@code false}, because ISO 8601 (2004) doesn't mention such * patterns. * @param accuracy tells which parts of the date/time to drop. The * {@code datePart} and {@code timePart} parameters are stronger than * this. Note that when {@link #ACCURACY_MILLISECONDS} is specified, * the milliseconds part will be displayed as fraction seconds * (like {@code "15:30.00.25"}) with the minimum number of * digits needed to show the milliseconds without precision lose. * Thus, if the milliseconds happen to be exactly 0, no fraction * seconds will be shown at all. * @param timeZone the time zone in which the date/time will be shown. (You * may find {@link DateUtil#UTC} handy here.) Note * that although date-only formats has no time zone offset part, * the result still depends on the time zone, as days start and end * at different points on the time line in different zones. * @param calendarFactory the factory that will create the calendar used * internally for calculations. The point of this parameter is that * creating a new calendar is relatively expensive, so it's desirable * to reuse calendars and only set their time and zone. (This was * tested on Sun JDK 1.6 x86 Win, where it gave 2x-3x speedup.) */
public static String dateToISO8601String( Date date, boolean datePart, boolean timePart, boolean offsetPart, int accuracy, TimeZone timeZone, DateToISO8601CalendarFactory calendarFactory) { return dateToString(date, datePart, timePart, offsetPart, accuracy, timeZone, false, calendarFactory); }
Same as dateToISO8601String, but gives XML Schema compliant format.
/** * Same as {@link #dateToISO8601String}, but gives XML Schema compliant format. */
public static String dateToXSString( Date date, boolean datePart, boolean timePart, boolean offsetPart, int accuracy, TimeZone timeZone, DateToISO8601CalendarFactory calendarFactory) { return dateToString(date, datePart, timePart, offsetPart, accuracy, timeZone, true, calendarFactory); } private static String dateToString( Date date, boolean datePart, boolean timePart, boolean offsetPart, int accuracy, TimeZone timeZone, boolean xsMode, DateToISO8601CalendarFactory calendarFactory) { if (!xsMode && !timePart && offsetPart) { throw new IllegalArgumentException( "ISO 8601:2004 doesn't specify any formats where the " + "offset is shown but the time isn't."); } if (timeZone == null) { timeZone = UTC; } GregorianCalendar cal = calendarFactory.get(timeZone, date); int maxLength; if (!timePart) { maxLength = 10 + (xsMode ? 6 : 0); // YYYY-MM-DD+00:00 } else { if (!datePart) { maxLength = 12 + 6; // HH:MM:SS.mmm+00:00 } else { maxLength = 10 + 1 + 12 + 6; } } char[] res = new char[maxLength]; int dstIdx = 0; if (datePart) { int x = cal.get(Calendar.YEAR); if (x > 0 && cal.get(Calendar.ERA) == GregorianCalendar.BC) { x = -x + (xsMode ? 0 : 1); } if (x >= 0 && x < 9999) { res[dstIdx++] = (char) ('0' + x / 1000); res[dstIdx++] = (char) ('0' + x % 1000 / 100); res[dstIdx++] = (char) ('0' + x % 100 / 10); res[dstIdx++] = (char) ('0' + x % 10); } else { String yearString = String.valueOf(x); // Re-allocate buffer: maxLength = maxLength - 4 + yearString.length(); res = new char[maxLength]; for (int i = 0; i < yearString.length(); i++) { res[dstIdx++] = yearString.charAt(i); } } res[dstIdx++] = '-'; x = cal.get(Calendar.MONTH) + 1; dstIdx = append00(res, dstIdx, x); res[dstIdx++] = '-'; x = cal.get(Calendar.DAY_OF_MONTH); dstIdx = append00(res, dstIdx, x); if (timePart) { res[dstIdx++] = 'T'; } } if (timePart) { int x = cal.get(Calendar.HOUR_OF_DAY); dstIdx = append00(res, dstIdx, x); if (accuracy >= ACCURACY_MINUTES) { res[dstIdx++] = ':'; x = cal.get(Calendar.MINUTE); dstIdx = append00(res, dstIdx, x); if (accuracy >= ACCURACY_SECONDS) { res[dstIdx++] = ':'; x = cal.get(Calendar.SECOND); dstIdx = append00(res, dstIdx, x); if (accuracy >= ACCURACY_MILLISECONDS) { x = cal.get(Calendar.MILLISECOND); int forcedDigits = accuracy == ACCURACY_MILLISECONDS_FORCED ? 3 : 0; if (x != 0 || forcedDigits != 0) { if (x > 999) { // Shouldn't ever happen... throw new RuntimeException( "Calendar.MILLISECOND > 999"); } res[dstIdx++] = '.'; do { res[dstIdx++] = (char) ('0' + (x / 100)); forcedDigits--; x = x % 100 * 10; } while (x != 0 || forcedDigits > 0); } } } } } if (offsetPart) { if (timeZone == UTC) { res[dstIdx++] = 'Z'; } else { int dt = timeZone.getOffset(date.getTime()); boolean positive; if (dt < 0) { positive = false; dt = -dt; } else { positive = true; } dt /= 1000; int offS = dt % 60; dt /= 60; int offM = dt % 60; dt /= 60; int offH = dt; if (offS == 0 && offM == 0 && offH == 0) { res[dstIdx++] = 'Z'; } else { res[dstIdx++] = positive ? '+' : '-'; dstIdx = append00(res, dstIdx, offH); res[dstIdx++] = ':'; dstIdx = append00(res, dstIdx, offM); if (offS != 0) { res[dstIdx++] = ':'; dstIdx = append00(res, dstIdx, offS); } } } } return new String(res, 0, dstIdx); }
Appends a number between 0 and 99 padded to 2 digits.
/** * Appends a number between 0 and 99 padded to 2 digits. */
private static int append00(char[] res, int dstIdx, int x) { res[dstIdx++] = (char) ('0' + x / 10); res[dstIdx++] = (char) ('0' + x % 10); return dstIdx; }
Parses an W3C XML Schema date string (not time or date-time). Unlike in ISO 8601:2000 Second Edition, year -1 means B.C 1, and year 0 is invalid.
Params:
  • dateStr – the string to parse.
  • defaultTimeZone – used if the date doesn't specify the time zone offset explicitly. Can't be null.
  • calToDateConverter – Used internally to calculate the result from the calendar field values. If you don't have a such object around, you can just use new TrivialCalendarFieldsToDateConverter().
Throws:
  • DateParseException – if the date is malformed, or if the time zone offset is unspecified and the defaultTimeZone is null.
/** * Parses an W3C XML Schema date string (not time or date-time). * Unlike in ISO 8601:2000 Second Edition, year -1 means B.C 1, and year 0 is invalid. * * @param dateStr the string to parse. * @param defaultTimeZone used if the date doesn't specify the * time zone offset explicitly. Can't be {@code null}. * @param calToDateConverter Used internally to calculate the result from the calendar field values. * If you don't have a such object around, you can just use * {@code new }{@link TrivialCalendarFieldsToDateConverter}{@code ()}. * * @throws DateParseException if the date is malformed, or if the time * zone offset is unspecified and the {@code defaultTimeZone} is * {@code null}. */
public static Date parseXSDate( String dateStr, TimeZone defaultTimeZone, CalendarFieldsToDateConverter calToDateConverter) throws DateParseException { Matcher m = PATTERN_XS_DATE.matcher(dateStr); if (!m.matches()) { throw new DateParseException("The value didn't match the expected pattern: " + PATTERN_XS_DATE); } return parseDate_parseMatcher( m, defaultTimeZone, true, calToDateConverter); } /** * Same as {@link #parseXSDate(String, TimeZone, CalendarFieldsToDateConverter)}, but for ISO 8601 dates. */ public static Date parseISO8601Date( String dateStr, TimeZone defaultTimeZone, CalendarFieldsToDateConverter calToDateConverter) throws DateParseException { Matcher m = PATTERN_ISO8601_EXTENDED_DATE.matcher(dateStr); if (!m.matches()) { m = PATTERN_ISO8601_BASIC_DATE.matcher(dateStr); if (!m.matches()) { throw new DateParseException("The value didn't match the expected pattern: " + PATTERN_ISO8601_EXTENDED_DATE + " or " + PATTERN_ISO8601_BASIC_DATE); } } return parseDate_parseMatcher( m, defaultTimeZone, false, calToDateConverter); } private static Date parseDate_parseMatcher( Matcher m, TimeZone defaultTZ, boolean xsMode, CalendarFieldsToDateConverter calToDateConverter) throws DateParseException { NullArgumentException.check("defaultTZ", defaultTZ); try { int year = groupToInt(m.group(1), "year", Integer.MIN_VALUE, Integer.MAX_VALUE); int era; // Starting from ISO 8601:2000 Second Edition, 0001 is AD 1, 0000 is BC 1, -0001 is BC 2. // However, according to http://www.w3.org/TR/2004/REC-xmlschema-2-20041028/, XML schemas are based // on the earlier version where 0000 didn't exist, and year -1 is BC 1. if (year <= 0) { era = GregorianCalendar.BC; year = -year + (xsMode ? 0 : 1); if (year == 0) { throw new DateParseException(MSG_YEAR_0_NOT_ALLOWED); } } else { era = GregorianCalendar.AD; } int month = groupToInt(m.group(2), "month", 1, 12) - 1; int day = groupToInt(m.group(3), "day-of-month", 1, 31); TimeZone tz = xsMode ? parseMatchingTimeZone(m.group(4), defaultTZ) : defaultTZ; return calToDateConverter.calculate(era, year, month, day, 0, 0, 0, 0, false, tz); } catch (IllegalArgumentException e) { // Calendar methods used to throw this for illegal dates. throw new DateParseException( "Date calculation faliure. " + "Probably the date is formally correct, but refers " + "to an unexistent date (like February 30)."); } }
Parses an W3C XML Schema time string (not date or date-time). If the time string doesn't specify the time zone offset explicitly, the value of the defaultTZ paramter will be used.
/** * Parses an W3C XML Schema time string (not date or date-time). * If the time string doesn't specify the time zone offset explicitly, * the value of the {@code defaultTZ} paramter will be used. */
public static Date parseXSTime( String timeStr, TimeZone defaultTZ, CalendarFieldsToDateConverter calToDateConverter) throws DateParseException { Matcher m = PATTERN_XS_TIME.matcher(timeStr); if (!m.matches()) { throw new DateParseException("The value didn't match the expected pattern: " + PATTERN_XS_TIME); } return parseTime_parseMatcher(m, defaultTZ, calToDateConverter); } /** * Same as {@link #parseXSTime(String, TimeZone, CalendarFieldsToDateConverter)} but for ISO 8601 times. */ public static Date parseISO8601Time( String timeStr, TimeZone defaultTZ, CalendarFieldsToDateConverter calToDateConverter) throws DateParseException { Matcher m = PATTERN_ISO8601_EXTENDED_TIME.matcher(timeStr); if (!m.matches()) { m = PATTERN_ISO8601_BASIC_TIME.matcher(timeStr); if (!m.matches()) { throw new DateParseException("The value didn't match the expected pattern: " + PATTERN_ISO8601_EXTENDED_TIME + " or " + PATTERN_ISO8601_BASIC_TIME); } } return parseTime_parseMatcher(m, defaultTZ, calToDateConverter); } private static Date parseTime_parseMatcher( Matcher m, TimeZone defaultTZ, CalendarFieldsToDateConverter calToDateConverter) throws DateParseException { NullArgumentException.check("defaultTZ", defaultTZ); try { // ISO 8601 allows both 00:00 and 24:00, // but Calendar.set(...) doesn't if the Calendar is not lenient. int hours = groupToInt(m.group(1), "hour-of-day", 0, 24); boolean hourWas24; if (hours == 24) { hours = 0; hourWas24 = true; // And a day will be added later... } else { hourWas24 = false; } final String minutesStr = m.group(2); int minutes = minutesStr != null ? groupToInt(minutesStr, "minute", 0, 59) : 0; final String secsStr = m.group(3); // Allow 60 because of leap seconds int secs = secsStr != null ? groupToInt(secsStr, "second", 0, 60) : 0; int millisecs = groupToMillisecond(m.group(4)); // As a time is just the distance from the beginning of the day, // the time-zone offest should be 0 usually. TimeZone tz = parseMatchingTimeZone(m.group(5), defaultTZ); // Continue handling the 24:00 special case int day; if (hourWas24) { if (minutes == 0 && secs == 0 && millisecs == 0) { day = 2; } else { throw new DateParseException( "Hour 24 is only allowed in the case of " + "midnight."); } } else { day = 1; } return calToDateConverter.calculate( GregorianCalendar.AD, 1970, 0, day, hours, minutes, secs, millisecs, false, tz); } catch (IllegalArgumentException e) { // Calendar methods used to throw this for illegal dates. throw new DateParseException( "Unexpected time calculation faliure."); } }
Parses an W3C XML Schema date-time string (not date or time). Unlike in ISO 8601:2000 Second Edition, year -1 means B.C 1, and year 0 is invalid.
Params:
  • dateTimeStr – the string to parse.
  • defaultTZ – used if the dateTime doesn't specify the time zone offset explicitly. Can't be null.
Throws:
/** * Parses an W3C XML Schema date-time string (not date or time). * Unlike in ISO 8601:2000 Second Edition, year -1 means B.C 1, and year 0 is invalid. * * @param dateTimeStr the string to parse. * @param defaultTZ used if the dateTime doesn't specify the * time zone offset explicitly. Can't be {@code null}. * * @throws DateParseException if the dateTime is malformed. */
public static Date parseXSDateTime( String dateTimeStr, TimeZone defaultTZ, CalendarFieldsToDateConverter calToDateConverter) throws DateParseException { Matcher m = PATTERN_XS_DATE_TIME.matcher(dateTimeStr); if (!m.matches()) { throw new DateParseException( "The value didn't match the expected pattern: " + PATTERN_XS_DATE_TIME); } return parseDateTime_parseMatcher( m, defaultTZ, true, calToDateConverter); } /** * Same as {@link #parseXSDateTime(String, TimeZone, CalendarFieldsToDateConverter)} but for ISO 8601 format. */ public static Date parseISO8601DateTime( String dateTimeStr, TimeZone defaultTZ, CalendarFieldsToDateConverter calToDateConverter) throws DateParseException { Matcher m = PATTERN_ISO8601_EXTENDED_DATE_TIME.matcher(dateTimeStr); if (!m.matches()) { m = PATTERN_ISO8601_BASIC_DATE_TIME.matcher(dateTimeStr); if (!m.matches()) { throw new DateParseException("The value (" + dateTimeStr + ") didn't match the expected pattern: " + PATTERN_ISO8601_EXTENDED_DATE_TIME + " or " + PATTERN_ISO8601_BASIC_DATE_TIME); } } return parseDateTime_parseMatcher( m, defaultTZ, false, calToDateConverter); } private static Date parseDateTime_parseMatcher( Matcher m, TimeZone defaultTZ, boolean xsMode, CalendarFieldsToDateConverter calToDateConverter) throws DateParseException { NullArgumentException.check("defaultTZ", defaultTZ); try { int year = groupToInt(m.group(1), "year", Integer.MIN_VALUE, Integer.MAX_VALUE); int era; // Starting from ISO 8601:2000 Second Edition, 0001 is AD 1, 0000 is BC 1, -0001 is BC 2. // However, according to http://www.w3.org/TR/2004/REC-xmlschema-2-20041028/, XML schemas are based // on the earlier version where 0000 didn't exist, and year -1 is BC 1. if (year <= 0) { era = GregorianCalendar.BC; year = -year + (xsMode ? 0 : 1); if (year == 0) { throw new DateParseException(MSG_YEAR_0_NOT_ALLOWED); } } else { era = GregorianCalendar.AD; } int month = groupToInt(m.group(2), "month", 1, 12) - 1; int day = groupToInt(m.group(3), "day-of-month", 1, 31); // ISO 8601 allows both 00:00 and 24:00, // but cal.set(...) doesn't if the Calendar is not lenient. int hours = groupToInt(m.group(4), "hour-of-day", 0, 24); boolean hourWas24; if (hours == 24) { hours = 0; hourWas24 = true; // And a day will be added later... } else { hourWas24 = false; } final String minutesStr = m.group(5); int minutes = minutesStr != null ? groupToInt(minutesStr, "minute", 0, 59) : 0; final String secsStr = m.group(6); // Allow 60 because of leap seconds int secs = secsStr != null ? groupToInt(secsStr, "second", 0, 60) : 0; int millisecs = groupToMillisecond(m.group(7)); // As a time is just the distance from the beginning of the day, // the time-zone offest should be 0 usually. TimeZone tz = parseMatchingTimeZone(m.group(8), defaultTZ); // Continue handling the 24:00 specail case if (hourWas24) { if (minutes != 0 || secs != 0 || millisecs != 0) { throw new DateParseException( "Hour 24 is only allowed in the case of " + "midnight."); } } return calToDateConverter.calculate( era, year, month, day, hours, minutes, secs, millisecs, hourWas24, tz); } catch (IllegalArgumentException e) { // Calendar methods used to throw this for illegal dates. throw new DateParseException( "Date-time calculation faliure. " + "Probably the date-time is formally correct, but " + "refers to an unexistent date-time " + "(like February 30)."); } }
Parses the time zone part from a W3C XML Schema date/time/dateTime.
Throws:
  • DateParseException – if the zone is malformed.
/** * Parses the time zone part from a W3C XML Schema date/time/dateTime. * @throws DateParseException if the zone is malformed. */
public static TimeZone parseXSTimeZone(String timeZoneStr) throws DateParseException { Matcher m = PATTERN_XS_TIME_ZONE.matcher(timeZoneStr); if (!m.matches()) { throw new DateParseException( "The time zone offset didn't match the expected pattern: " + PATTERN_XS_TIME_ZONE); } return parseMatchingTimeZone(timeZoneStr, null); } private static int groupToInt(String g, String gName, int min, int max) throws DateParseException { if (g == null) { throw new DateParseException("The " + gName + " part " + "is missing."); } int start; // Remove minus sign, so we can remove the 0-s later: boolean negative; if (g.startsWith("-")) { negative = true; start = 1; } else { negative = false; start = 0; } // Remove leading 0-s: while (start < g.length() - 1 && g.charAt(start) == '0') { start++; } if (start != 0) { g = g.substring(start); } try { int r = Integer.parseInt(g); if (negative) { r = -r; } if (r < min) { throw new DateParseException("The " + gName + " part " + "must be at least " + min + "."); } if (r > max) { throw new DateParseException("The " + gName + " part " + "can't be more than " + max + "."); } return r; } catch (NumberFormatException e) { throw new DateParseException("The " + gName + " part " + "is a malformed integer."); } } private static TimeZone parseMatchingTimeZone( String s, TimeZone defaultZone) throws DateParseException { if (s == null) { return defaultZone; } if (s.equals("Z")) { return DateUtil.UTC; } StringBuilder sb = new StringBuilder(9); sb.append("GMT"); sb.append(s.charAt(0)); String h = s.substring(1, 3); groupToInt(h, "offset-hours", 0, 23); sb.append(h); String m; int ln = s.length(); if (ln > 3) { int startIdx = s.charAt(3) == ':' ? 4 : 3; m = s.substring(startIdx, startIdx + 2); groupToInt(m, "offset-minutes", 0, 59); sb.append(':'); sb.append(m); } return TimeZone.getTimeZone(sb.toString()); } private static int groupToMillisecond(String g) throws DateParseException { if (g == null) { return 0; } if (g.length() > 3) { g = g.substring(0, 3); } int i = groupToInt(g, "partial-seconds", 0, Integer.MAX_VALUE); return g.length() == 1 ? i * 100 : (g.length() == 2 ? i * 10 : i); }
Used internally by DateUtil; don't use its implementations for anything else.
/** * Used internally by {@link DateUtil}; don't use its implementations for * anything else. */
public interface DateToISO8601CalendarFactory {
Returns a GregorianCalendar with the desired time zone and time and US locale. The returned calendar is used as read-only. It must be guaranteed that within a thread the instance returned last time is not in use anymore when this method is called again.
/** * Returns a {@link GregorianCalendar} with the desired time zone and * time and US locale. The returned calendar is used as read-only. * It must be guaranteed that within a thread the instance returned last time * is not in use anymore when this method is called again. */
GregorianCalendar get(TimeZone tz, Date date); }
Used internally by DateUtil; don't use its implementations for anything else.
/** * Used internally by {@link DateUtil}; don't use its implementations for anything else. */
public interface CalendarFieldsToDateConverter {
Calculates the Date from the specified calendar fields.
/** * Calculates the {@link Date} from the specified calendar fields. */
Date calculate(int era, int year, int month, int day, int hours, int minutes, int secs, int millisecs, boolean addOneDay, TimeZone tz); }
Non-thread-safe factory that hard-references a calendar internally.
/** * Non-thread-safe factory that hard-references a calendar internally. */
public static final class TrivialDateToISO8601CalendarFactory implements DateToISO8601CalendarFactory { private GregorianCalendar calendar; private TimeZone lastlySetTimeZone; public GregorianCalendar get(TimeZone tz, Date date) { if (calendar == null) { calendar = new GregorianCalendar(tz, Locale.US); calendar.setGregorianChange(new Date(Long.MIN_VALUE)); // never use Julian calendar } else { // At least on Java 6, calendar.getTimeZone is slow due to a bug, so we need lastlySetTimeZone. if (lastlySetTimeZone != tz) { // Deliberately `!=` instead of `!<...>.equals()` calendar.setTimeZone(tz); lastlySetTimeZone = tz; } } calendar.setTime(date); return calendar; } }
Non-thread-safe implementation that hard-references a calendar internally.
/** * Non-thread-safe implementation that hard-references a calendar internally. */
public static final class TrivialCalendarFieldsToDateConverter implements CalendarFieldsToDateConverter { private GregorianCalendar calendar; private TimeZone lastlySetTimeZone; public Date calculate(int era, int year, int month, int day, int hours, int minutes, int secs, int millisecs, boolean addOneDay, TimeZone tz) { if (calendar == null) { calendar = new GregorianCalendar(tz, Locale.US); calendar.setLenient(false); calendar.setGregorianChange(new Date(Long.MIN_VALUE)); // never use Julian calendar } else { // At least on Java 6, calendar.getTimeZone is slow due to a bug, so we need lastlySetTimeZone. if (lastlySetTimeZone != tz) { // Deliberately `!=` instead of `!<...>.equals()` calendar.setTimeZone(tz); lastlySetTimeZone = tz; } } calendar.set(Calendar.ERA, era); calendar.set(Calendar.YEAR, year); calendar.set(Calendar.MONTH, month); calendar.set(Calendar.DAY_OF_MONTH, day); calendar.set(Calendar.HOUR_OF_DAY, hours); calendar.set(Calendar.MINUTE, minutes); calendar.set(Calendar.SECOND, secs); calendar.set(Calendar.MILLISECOND, millisecs); if (addOneDay) { calendar.add(Calendar.DAY_OF_MONTH, 1); } return calendar.getTime(); } } public static final class DateParseException extends ParseException { public DateParseException(String message) { super(message, 0); } } }