/* Copyright (c) 2008 Google Inc.
*
* Licensed 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 com.google.gdata.data;
import com.google.gdata.util.ParseException;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
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;
/**
* Represents a date/time, or a date without a time. Optionally
* includes a time zone.
*/
public class DateTime implements Comparable<Object> {
public DateTime() {}
public DateTime(long value) {
this.value = value;
}
public DateTime(Date value) {
this.value = value.getTime();
}
public DateTime(long value, int tzShift) {
this.value = value;
this.tzShift = new Integer(tzShift);
}
public DateTime(Date value, TimeZone zone) {
this.value = value.getTime();
this.tzShift = zone.getOffset(value.getTime()) / 60000;
}
public static DateTime now() {
return new DateTime(new Date(), GMT);
}
/**
* Date/time value expressed as the number of ms since the Unix epoch.
*
* If the time zone is specified, this value is normalized to UTC, so
* to format this date/time value, the time zone shift has to be applied.
*/
protected long value = 0;
public long getValue() { return value; }
public void setValue(long v) { value = v; }
/** Specifies whether this is a date-only value. */
protected boolean dateOnly = false;
public boolean isDateOnly() { return dateOnly; }
public void setDateOnly(boolean v) { dateOnly = v; }
/**
* Time zone shift from UTC in minutes. If {@code null}, no time zone
* is set, and the time is always interpreted as local time.
*/
protected Integer tzShift = null;
public Integer getTzShift() { return tzShift; }
public void setTzShift(Integer v) { tzShift = v; }
@Override
public int hashCode() {
return Long.valueOf(value).hashCode();
}
/**
* Compares instance with DateTime or Date objects.
*
* Does not take the tzShift value into account.
* Therefore, two DateTime objects are equal independent to
* the time zone they refer to.
* Equals with a instance of java.util.Date is asymmetric.
*/
@Override
public boolean equals(Object o) {
if (o instanceof DateTime) {
return this.value == ((DateTime) o).value;
} else if (o instanceof Date) {
return this.value == ((Date) o).getTime();
} else {
return false;
}
}
public int compareTo(Object o) {
if (o instanceof DateTime) {
return new Long(value).compareTo(new Long(((DateTime) o).value));
} else if (o instanceof Date) {
return new Long(value).compareTo(new Long(((Date) o).getTime()));
} else {
throw new RuntimeException("Invalid type.");
}
}
/** XML date/time pattern. */
public static final Pattern dateTimePattern =
Pattern.compile("(\\d\\d\\d\\d)\\-(\\d\\d)\\-(\\d\\d)[Tt]" +
"(\\d\\d):(\\d\\d):(\\d\\d)(\\.(\\d+))?" +
"([Zz]|((\\+|\\-)(\\d\\d):(\\d\\d)))?");
/** XML date pattern. */
public static final Pattern datePattern =
Pattern.compile("(\\d\\d\\d\\d)\\-(\\d\\d)\\-(\\d\\d)" +
"([Zz]|((\\+|\\-)(\\d\\d):(\\d\\d)))?");
/** XML date/time or date pattern. */
public static final Pattern dateTimeChoicePattern =
Pattern.compile("(\\d\\d\\d\\d)\\-(\\d\\d)\\-(\\d\\d)" +
"([Tt](\\d\\d):(\\d\\d):(\\d\\d)(\\.(\\d+))?)?" +
"([Zz]|((\\+|\\-)(\\d\\d):(\\d\\d)))?");
/** RFC 822 date/time format. */
private static final SimpleDateFormat dateTimeFormat822 =
new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.ENGLISH);
private static final TimeZone GMT = TimeZone.getTimeZone("GMT");
static {
dateTimeFormat822.setTimeZone(GMT);
}
/** Formats the value as an xs:date or xs:dateTime string. */
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
Calendar dateTime = new GregorianCalendar(GMT);
long localTime = value;
if (tzShift != null) {
localTime += tzShift.longValue() * 60000;
}
dateTime.setTimeInMillis(localTime);
try {
appendInt(sb, dateTime.get(Calendar.YEAR), 4);
sb.append('-');
appendInt(sb, dateTime.get(Calendar.MONTH) + 1, 2);
sb.append('-');
appendInt(sb, dateTime.get(Calendar.DAY_OF_MONTH), 2);
if (!dateOnly) {
sb.append('T');
appendInt(sb, dateTime.get(Calendar.HOUR_OF_DAY), 2);
sb.append(':');
appendInt(sb, dateTime.get(Calendar.MINUTE), 2);
sb.append(':');
appendInt(sb, dateTime.get(Calendar.SECOND), 2);
if (dateTime.isSet(Calendar.MILLISECOND)) {
sb.append('.');
appendInt(sb, dateTime.get(Calendar.MILLISECOND), 3);
}
}
if (tzShift != null) {
if (tzShift.intValue() == 0) {
sb.append('Z');
} else {
int absTzShift = tzShift.intValue();
if (tzShift > 0) {
sb.append('+');
} else {
sb.append('-');
absTzShift = -absTzShift;
}
int tzHours = absTzShift / 60;
int tzMinutes = absTzShift % 60;
appendInt(sb, tzHours, 2);
sb.append(':');
appendInt(sb, tzMinutes, 2);
}
}
} catch (ArrayIndexOutOfBoundsException e) {
throw new RuntimeException(e);
}
return sb.toString();
}
/** Formats the value as an RFC 822 date/time. */
public String toStringRfc822() {
assert !dateOnly;
synchronized (dateTimeFormat822) {
return dateTimeFormat822.format(value);
}
}
/** Parses the value as an RFC 822 date/time. */
public static DateTime parseRfc822(String str) throws ParseException {
Date date;
synchronized (dateTimeFormat822) {
try {
date = dateTimeFormat822.parse(str);
} catch (java.text.ParseException e) {
throw new ParseException(e);
}
}
return new DateTime(date);
}
/** Formats the value as a human-readable string. */
public String toUiString() {
StringBuilder sb = new StringBuilder();
Calendar dateTime = new GregorianCalendar(GMT);
long localTime = value;
if (tzShift != null) {
localTime += tzShift.longValue() * 60000;
}
dateTime.setTimeInMillis(localTime);
try {
appendInt(sb, dateTime.get(Calendar.YEAR), 4);
sb.append('-');
appendInt(sb, dateTime.get(Calendar.MONTH) + 1, 2);
sb.append('-');
appendInt(sb, dateTime.get(Calendar.DAY_OF_MONTH), 2);
if (!dateOnly) {
sb.append(' ');
appendInt(sb, dateTime.get(Calendar.HOUR_OF_DAY), 2);
sb.append(':');
appendInt(sb, dateTime.get(Calendar.MINUTE), 2);
}
} catch (ArrayIndexOutOfBoundsException e) {
throw new RuntimeException(e);
}
return sb.toString();
}
/** Parses an xs:dateTime string. */
public static DateTime parseDateTime(String str)
throws NumberFormatException {
Matcher m = str == null ? null : dateTimePattern.matcher(str);
if (str == null || !m.matches()) {
throw new NumberFormatException("Invalid date/time format.");
}
/* Debugging help:
System.out.println("Year: " + m.group(1));
System.out.println("Month: " + m.group(2));
System.out.println("Day: " + m.group(3));
System.out.println("Hour: " + m.group(4));
System.out.println("Minute: " + m.group(5));
System.out.println("Second: " + m.group(6));
System.out.println("Second Fraction: " + m.group(8));
System.out.println("TZ: " + m.group(9));
System.out.println("TZ Shift: " + m.group(11));
System.out.println("TZ Hour: " + m.group(12));
System.out.println("TZ Minute: " + m.group(13));
*/
DateTime ret = new DateTime();
ret.dateOnly = false;
if (m.group(9) == null) {
// No time zone specified.
} else if (m.group(9).equalsIgnoreCase("Z")) {
ret.tzShift = new Integer(0);
} else {
ret.tzShift = new Integer((Integer.valueOf(m.group(12)) * 60 +
Integer.valueOf(m.group(13))));
if (m.group(11).equals("-")) {
ret.tzShift = new Integer(-ret.tzShift.intValue());
}
}
Calendar dateTime = new GregorianCalendar(GMT);
dateTime.clear();
dateTime.set(Integer.valueOf(m.group(1)),
Integer.valueOf(m.group(2)) - 1,
Integer.valueOf(m.group(3)),
Integer.valueOf(m.group(4)),
Integer.valueOf(m.group(5)),
Integer.valueOf(m.group(6)));
if (m.group(8) != null && m.group(8).length() > 0) {
final BigDecimal bd = new BigDecimal("0." + m.group(8));
// we care only for milliseconds, so movePointRight(3)
dateTime.set(Calendar.MILLISECOND, bd.movePointRight(3).intValue());
}
ret.value = dateTime.getTimeInMillis();
if (ret.tzShift != null) {
ret.value -= ret.tzShift.intValue() * 60000;
}
return ret;
}
/** Parses an xs:date string. */
public static DateTime parseDate(String str)
throws NumberFormatException {
Matcher m = str == null ? null : datePattern.matcher(str);
if (str == null || !m.matches()) {
throw new NumberFormatException("Invalid date format.");
}
/* Debugging help:
System.out.println("Year: " + m.group(1));
System.out.println("Month: " + m.group(2));
System.out.println("Day: " + m.group(3));
System.out.println("TZ: " + m.group(4));
System.out.println("TZ Shift: " + m.group(6));
System.out.println("TZ Hour: " + m.group(7));
System.out.println("TZ Minute: " + m.group(8));
*/
DateTime ret = new DateTime();
ret.dateOnly = true;
if (m.group(4) == null) {
// No time zone specified.
} else if (m.group(4).equalsIgnoreCase("Z")) {
ret.tzShift = new Integer(0);
} else {
ret.tzShift = new Integer((Integer.valueOf(m.group(7)) * 60 +
Integer.valueOf(m.group(8))));
if (m.group(6).equals("-")) {
ret.tzShift = new Integer(-ret.tzShift.intValue());
}
}
Calendar dateTime = new GregorianCalendar(GMT);
dateTime.clear();
dateTime.set(Integer.valueOf(m.group(1)),
Integer.valueOf(m.group(2)) - 1,
Integer.valueOf(m.group(3)));
ret.value = dateTime.getTimeInMillis();
if (ret.tzShift != null) {
ret.value -= ret.tzShift.intValue() * 60000;
}
return ret;
}
/**
* Parses an XML value that's either an xs:date or xs:dateTime string.
*
* @throws NumberFormatException
* Invalid RFC 3339 date or date/time string.
*/
public static DateTime parseDateTimeChoice(String value)
throws NumberFormatException {
NumberFormatException exception;
try {
return DateTime.parseDateTime(value);
} catch (NumberFormatException e) {
exception = e;
}
try {
return DateTime.parseDate(value);
} catch (NumberFormatException e) {
exception = e;
}
throw exception;
}
/** Appends a zero-padded number to a string builder. */
private static void appendInt(StringBuilder sb, int num, int numDigits) {
if (num < 0) {
sb.append('-');
num = -num;
}
char[] digits = new char[numDigits];
for (int digit = numDigits - 1; digit >= 0; --digit) {
digits[digit] = (char)('0' + num % 10);
num /= 10;
}
sb.append(digits);
}
}