/* * Copyright Aduna (http://www.aduna-software.com/) (c) 1997-2007. * * Licensed under the Aduna BSD-style license. */ package org.openrdf.model.datatypes; import java.util.Calendar; import java.util.GregorianCalendar; import java.util.NoSuchElementException; import java.util.StringTokenizer; /** * This class provides utility functions for comparisons operating on * <code>xml:dateTime</code> datatypes as specified in <a * href="http://www.w3.org/TR/xmlschema-2/#dateTime">W3C, XML Schema Part 2: * Datatypes Second Edition</a> Known deviations from the standard: - the range * of years in this implementation is limited to Integer.MIN_VALUE to * Integer.MAX_VALUE for practical reasons - this implementation accepts some * dates that have impossible month, day-of-month combinations (such as * 2005-02-29, which was not a leap year) * * @author Arjohn Kampman */ public class XMLDateTime implements Cloneable, Comparable<XMLDateTime> { /*-----------* * Variables * *-----------*/ /** The raw dateTime string that was used to initialize this object. */ private String dateTimeString; /** Flag indicating whether the year is positive or negative. */ private boolean isNegativeYear; /** year part of the dateTime object as String */ private String year; /** month part of the dateTime object as String */ private String months; /** day part of the dateTime object as String */ private String days; /** hour part of the dateTime object as String */ private String hours; /** minutes part of the dateTime object as String */ private String minutes; /** seconds part of the dateTime object as String */ private String seconds; /** fractional seconds part of the dateTime object as String */ private String fractionalSeconds; /** Flag indicating whether the timezone, if any, is positive or negative. */ private boolean isNegativeTimezone; /** hours part of the optional timezone as String */ private String hoursTimezone; /** minutes part of the optional timezone as String */ private String minutesTimezone; /** year part of the dateTime object as int */ private int iYear; /** month part of the dateTime object as int */ private int iMonths; /** day part of the dateTime object as int */ private int iDays; /** hour part of the dateTime object as int */ private int iHours; /** minute part of the dateTime object as int */ private int iMinutes; /** second part of the dateTime object as int */ private int iSeconds; /** fractional seconds part of the dateTime object as int */ private double iFractionalSeconds; /** hours part of the optional timezone as int */ private int iHoursTimezone; /** minutes part of the optional timezone as int */ private int iMinutesTimezone; /** Flag indicating whether the values have been normalized. */ private boolean isNormalized = false; /*--------------* * Constructors * *--------------*/ /** * Creates a new DateTime object for the supplied xsd:dateTime string value. * * @param dateTimeString * An xsd:dateTime value, for example * <tt>1999-05-31T13:20:00-05:00</tt>. */ public XMLDateTime(String dateTimeString) { this.dateTimeString = XMLDatatypeUtil.collapseWhiteSpace(dateTimeString); parseDateTimeString(); setNumericFields(); validateFieldValues(); } /*---------* * Methods * *---------*/ private void parseDateTimeString() { if (dateTimeString.length() < 19) { throw new IllegalArgumentException("String value too short to be a valid xsd:dateTime value: " + dateTimeString); } String errMsg = "Invalid xsd:dateTime value: " + dateTimeString; StringTokenizer st = new StringTokenizer(dateTimeString, "+-:.TZ", true); try { year = st.nextToken(); isNegativeYear = year.equals("-"); if (isNegativeYear) { year = st.nextToken(); } verifyTokenValue(st.nextToken(), "-", errMsg); months = st.nextToken(); verifyTokenValue(st.nextToken(), "-", errMsg); days = st.nextToken(); verifyTokenValue(st.nextToken(), "T", errMsg); hours = st.nextToken(); verifyTokenValue(st.nextToken(), ":", errMsg); minutes = st.nextToken(); verifyTokenValue(st.nextToken(), ":", errMsg); seconds = st.nextToken(); String token = st.hasMoreTokens() ? st.nextToken() : null; if (".".equals(token)) { fractionalSeconds = st.nextToken(); token = st.hasMoreTokens() ? st.nextToken() : null; } if ("+".equals(token) || "-".equals(token)) { isNegativeTimezone = "-".equals(token); hoursTimezone = st.nextToken(); verifyTokenValue(st.nextToken(), ":", errMsg); minutesTimezone = st.nextToken(); } else if ("Z".equals(token)) { isNegativeTimezone = false; hoursTimezone = minutesTimezone = "00"; } if (st.hasMoreTokens()) { throw new IllegalArgumentException(errMsg); } } catch (NoSuchElementException e) { throw new IllegalArgumentException(errMsg); } } private void verifyTokenValue(String token, String expected, String errMsg) { if (!token.equals(expected)) { throw new IllegalArgumentException(errMsg); } } private void setNumericFields() { try { // FIXME: the following statement fails when the year is // outside the range of integers (comment by Arjohn) iYear = Integer.parseInt(year); iMonths = Integer.parseInt(months); iDays = Integer.parseInt(days); iHours = Integer.parseInt(hours); iMinutes = Integer.parseInt(minutes); iSeconds = Integer.parseInt(seconds); if (fractionalSeconds != null) { // FIXME: the following statement fails when the fractional // seconds are outside the range of doubles (comment by Arjohn) iFractionalSeconds = Double.parseDouble("0." + fractionalSeconds); } if (hoursTimezone != null) { iHoursTimezone = Integer.parseInt(hoursTimezone); } if (minutesTimezone != null) { iMinutesTimezone = Integer.parseInt(minutesTimezone); } } catch (NumberFormatException e) { throw new IllegalArgumentException("All fields must be numbers: " + dateTimeString); } } private void validateFieldValues() { if (year.length() < 4) { throw new IllegalArgumentException("Year field requires at least 4 digits: " + dateTimeString); } if (months.length() != 2) { throw new IllegalArgumentException("Month field must be two digits: " + dateTimeString); } if (days.length() != 2) { throw new IllegalArgumentException("Days field must be two digits: " + dateTimeString); } if (hours.length() != 2) { throw new IllegalArgumentException("Hours field must be two digits: " + dateTimeString); } if (minutes.length() != 2) { throw new IllegalArgumentException("Minutes field must be two digits: " + dateTimeString); } if (seconds.length() != 2) { throw new IllegalArgumentException("Seconds field must be two digits: " + dateTimeString); } if (hoursTimezone != null) { if (hoursTimezone.length() != 2) { throw new IllegalArgumentException("Timezone-hours field must be two digits: " + dateTimeString); } if (minutesTimezone.length() != 2) { throw new IllegalArgumentException("Timezone-minutes field must be two digits: " + dateTimeString); } } if (year.length() > 4 && year.charAt(0) == '0') { throw new IllegalArgumentException("Leading zeros in years with more than 4 digits are prohibited: " + dateTimeString); } if (iYear == 0) { throw new IllegalArgumentException("0000 is not a valid year: " + dateTimeString); } if (iHours > 24) { throw new IllegalArgumentException("Invalid hour value: " + dateTimeString); } if (iMinutes > 59) { throw new IllegalArgumentException("Invalid minute value: " + dateTimeString); } if (iSeconds > 59) { throw new IllegalArgumentException("Invalid second value: " + dateTimeString); } if (iHours == 24 && (iMinutes != 0 || iSeconds != 0)) { throw new IllegalArgumentException("Invalid time: " + dateTimeString); } if (iHoursTimezone > 14 || iMinutesTimezone > 59 || iHoursTimezone == 14 && iMinutesTimezone != 0) { throw new IllegalArgumentException("Invalid timezone: " + dateTimeString); } } /** * Checks whether this object has already been normalized. */ public boolean isNormalized() { return isNormalized; } /** * Normalizes this dateTime object. */ public void normalize() { if (isNormalized) { // Values already normalized return; } if (iHours == 24 || hoursTimezone != null && (iHoursTimezone != 0 || iMinutesTimezone != 0)) { // Normalize the timezone to Coordinated Universal Time (UTC) // Insert values into a GregorianCalendar object. // Note: GregorianCalendar uses 0-based months Calendar cal = new GregorianCalendar(iYear, iMonths - 1, iDays, iHours, iMinutes, iSeconds); if (isNegativeYear) { cal.set(Calendar.ERA, GregorianCalendar.BC); } // Add/subtract the timezone if (isNegativeTimezone) { cal.add(Calendar.HOUR_OF_DAY, iHoursTimezone); cal.add(Calendar.MINUTE, iMinutesTimezone); } else { cal.add(Calendar.HOUR_OF_DAY, -iHoursTimezone); cal.add(Calendar.MINUTE, -iMinutesTimezone); } // Get the updated fields if (cal.get(Calendar.ERA) == GregorianCalendar.BC) { isNegativeYear = true; } iYear = cal.get(Calendar.YEAR); iMonths = cal.get(Calendar.MONTH) + 1; iDays = cal.get(Calendar.DAY_OF_MONTH); iHours = cal.get(Calendar.HOUR_OF_DAY); iMinutes = cal.get(Calendar.MINUTE); iSeconds = cal.get(Calendar.SECOND); year = int2string(iYear, 4); months = int2string(iMonths, 2); days = int2string(iDays, 2); hours = int2string(iHours, 2); minutes = int2string(iMinutes, 2); seconds = int2string(iSeconds, 2); if (hoursTimezone != null) { iHoursTimezone = iMinutesTimezone = 0; hoursTimezone = minutesTimezone = "00"; isNegativeTimezone = false; } } if (fractionalSeconds != null) { // Remove any trailing zeros int zeroCount = 0; for (int i = fractionalSeconds.length() - 1; i >= 0; i--) { if (fractionalSeconds.charAt(i) == '0') { zeroCount++; } else { break; } } if (zeroCount == fractionalSeconds.length()) { fractionalSeconds = null; } else if (zeroCount > 0) { fractionalSeconds = fractionalSeconds.substring(0, fractionalSeconds.length() - zeroCount); } } isNormalized = true; } /** * Converts an integer to a string, enforcing the resulting string to have at * least <tt>minDigits</tt> digits by prepending zeros if it has less than * that amount of digits. */ private String int2string(int iValue, int minDigits) { String result = String.valueOf(iValue); int zeroCount = minDigits - result.length(); if (zeroCount > 0) { StringBuilder sb = new StringBuilder(minDigits); for (int i = 0; i < zeroCount; i++) { sb.append('0'); } sb.append(result); result = sb.toString(); } return result; } /** * Returns the xsd:dateTime string-representation of this object. * * @return An xsd:dateTime value, e.g. <tt>1999-05-31T13:20:00-05:00</tt>. */ @Override public String toString() { StringBuilder sb = new StringBuilder(32); if (isNegativeYear) { sb.append('-'); } sb.append(year); sb.append('-'); sb.append(months); sb.append('-'); sb.append(days); sb.append('T'); sb.append(hours); sb.append(':'); sb.append(minutes); sb.append(':'); sb.append(seconds); if (fractionalSeconds != null) { sb.append('.'); sb.append(fractionalSeconds); } if (hoursTimezone != null) { if (iHoursTimezone == 0 && iMinutesTimezone == 0) { sb.append("Z"); } else { if (isNegativeTimezone) { sb.append('-'); } else { sb.append('+'); } sb.append(hoursTimezone); sb.append(':'); sb.append(minutesTimezone); } } return sb.toString(); } /** * Compares this DateTime object to another DateTime object. * * @throws ClassCastException * If <tt>other</tt> is not a DateTime object. */ public int compareTo(XMLDateTime otherDT) { XMLDateTime thisDT = this; if (thisDT.hoursTimezone != null && (thisDT.iHoursTimezone != 0 || thisDT.iMinutesTimezone != 0)) { // Create a normalized copy of this DateTime object thisDT = (XMLDateTime)thisDT.clone(); thisDT.normalize(); } if (otherDT.hoursTimezone != null && (otherDT.iHoursTimezone != 0 || otherDT.iMinutesTimezone != 0)) { // Create a normalized copy of this DateTime object otherDT = (XMLDateTime)otherDT.clone(); otherDT.normalize(); } if (thisDT.isNegativeYear && !otherDT.isNegativeYear) { return -1; } else if (!thisDT.isNegativeYear && otherDT.isNegativeYear) { return 1; } int result = 0; if (thisDT.iYear != otherDT.iYear) { result = thisDT.iYear - otherDT.iYear; } else if (thisDT.iMonths != otherDT.iMonths) { result = thisDT.iMonths - otherDT.iMonths; } else if (thisDT.iDays != otherDT.iDays) { result = thisDT.iDays - otherDT.iDays; } else if (thisDT.iHours != otherDT.iHours) { result = thisDT.iHours - otherDT.iHours; } else if (thisDT.iMinutes != otherDT.iMinutes) { result = thisDT.iMinutes - otherDT.iMinutes; } else if (thisDT.iSeconds != otherDT.iSeconds) { result = thisDT.iSeconds - otherDT.iSeconds; } else if (thisDT.iFractionalSeconds != otherDT.iFractionalSeconds) { result = (thisDT.iFractionalSeconds < otherDT.iFractionalSeconds) ? -1 : 1; } if (thisDT.isNegativeYear) { // Invert result for negative years result = -result; } return result; } // Overrides Object.clone(); @Override public Object clone() { try { return super.clone(); } catch (CloneNotSupportedException e) { throw new RuntimeException(e); } } }