blob: 9900fc952b1f23f6846d7254d4605c46425bab76 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2005, 2010 Andrea Bittau, University College London, and others
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Andrea Bittau - initial API and implementation from the PsychoPath XPath 2.0
* Mukul Gandhi - bug 273760 - wrong namespace for functions and data types
* Mukul Gandhi - improved string_value() implementation (motivated by bug, 281822)
* David Carver - bug 282223 - implementation of xs:duration.
* - bug 262765 - additional tweak to convert 24:00:00 to 00:00:00
* David Carver - bug 280547 - fix dates for comparison
* Mukul Gandhi - bug 280798 - PsychoPath support for JDK 1.4
*******************************************************************************/
package org.eclipse.wst.xml.xpath2.processor.internal.types;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.TimeZone;
import javax.xml.datatype.Duration;
import javax.xml.datatype.XMLGregorianCalendar;
import org.eclipse.wst.xml.xpath2.api.DynamicContext;
import org.eclipse.wst.xml.xpath2.api.Item;
import org.eclipse.wst.xml.xpath2.api.ResultBuffer;
import org.eclipse.wst.xml.xpath2.api.ResultSequence;
import org.eclipse.wst.xml.xpath2.api.typesystem.TypeDefinition;
import org.eclipse.wst.xml.xpath2.processor.DynamicError;
import org.eclipse.wst.xml.xpath2.processor.ResultSequenceFactory;
import org.eclipse.wst.xml.xpath2.processor.internal.function.CmpEq;
import org.eclipse.wst.xml.xpath2.processor.internal.function.CmpGt;
import org.eclipse.wst.xml.xpath2.processor.internal.function.CmpLt;
import org.eclipse.wst.xml.xpath2.processor.internal.function.MathMinus;
import org.eclipse.wst.xml.xpath2.processor.internal.function.MathPlus;
import org.eclipse.wst.xml.xpath2.processor.internal.types.builtin.BuiltinTypeLibrary;
/**
* A representation of a date and time (and optional timezone)
*/
public class XSDateTime extends CalendarType implements CmpEq, CmpLt, CmpGt,
MathMinus, MathPlus,
Cloneable {
private static final String XS_DATE_TIME = "xs:dateTime";
private Calendar _calendar;
private boolean _timezoned;
private XSDuration _tz;
/**
* Initiates a new representation of a supplied date and time
*
* @param cal
* The Calendar representation of the date and time to be stored
* @param tz
* The timezone of the date to be stored.
*/
public XSDateTime(Calendar cal, XSDuration tz) {
_calendar = cal;
_tz = tz;
if (tz == null)
_timezoned = false;
else
_timezoned = true;
}
/**
* Creates a copy of this date and time representation
*
* @return A copy of this date and time representation
*/
public Object clone() throws CloneNotSupportedException {
Calendar c = (Calendar) calendar().clone();
XSDuration t = tz();
if (t != null)
t = (XSDuration) t.clone();
return new XSDateTime(c, t);
}
/**
* Inititates a new representation of the current date and time
*/
public XSDateTime() {
this(new GregorianCalendar(), null);
}
/**
* Retrieves the datatype name
*
* @return "dateTime" which is the dataype name
*/
public String type_name() {
return "dateTime";
}
/**
* Check to see if a character is numeric
*
* @param x
* Character to be tested
* @return True if the character is numeric. False otherwise.
*/
public static boolean is_digit(char x) {
if ('0' <= x && x <= '9')
return true;
return false;
}
/**
* Parses a String representation of a date and time and retrieves the year,
* month and day from it
*
* @param str
* The String representation of the date (and optional timezone)
* @return Integer array of size 3. Element 1 is the year, element 2 is the
* month and element 3 is the day
*/
public static int[] parse_date(String str) {
int state = 0; // 0 expect year or minus
// 1 getting year
// 2 getting month
// 3 getting day
int[] ret = new int[3];
for (int i = 0; i < ret.length; i++)
ret[i] = 0;
String token = "";
for (int i = 0; i < str.length(); i++) {
char x = str.charAt(i);
switch (state) {
case 0:
if (is_digit(x)) {
token += x;
} else if (x == '-') {
token += x;
} else
return null;
state = 1;
break;
case 1:
// we got the year in theory...
if (x == '-') {
// check out the unsigned year
String uy = token;
if (uy.startsWith("-"))
uy = uy.substring(1, uy.length());
int uyl = uy.length();
if (uyl < 4)
return null;
if (uyl == 4) {
if (uy.compareTo("0000") == 0)
return null;
} else if (uy.charAt(0) == '0')
return null;
// semms good to me
ret[0] = Integer.parseInt(token);
token = "";
state = 2;
} else if (is_digit(x))
token += x;
else
return null;
break;
case 2:
// we got the month
if (x == '-') {
if (token.length() != 2)
return null;
ret[1] = Integer.parseInt(token);
token = "";
state = 3;
} else if (is_digit(x))
token += x;
else
return null;
break;
case 3:
// getting day
if (is_digit(x))
token += x;
else
return null;
break;
default:
assert false;
return ret;
}
}
if (state != 3)
return null;
// got the day
if (token.length() != 2)
return null;
ret[2] = Integer.parseInt(token);
return ret;
}
// return
// hour
// minute
// seconds
/**
* Parses a String representation of a date and time and retrieves the hour,
* minute and seconds from it
*
* @param str
* The String representation of the date (and optional timezone)
* @return Integer array of size 3. Element 1 is the hour, element 2 is the
* minute and element 3 is the seconds
*/
public static double[] parse_time(String str) {
int state = 0; // 0 getting minute
// 1 getting hour
// 2 getting seconds [the whole part]
// 3 getting fraction of seconds
double[] ret = new double[3];
String token = "";
for (int i = 0; i < str.length(); i++) {
char x = str.charAt(i);
switch (state) {
case 0:
case 1:
// got minute / hour
if (x == ':') {
if (token.length() != 2)
return null;
ret[state] = Integer.parseInt(token);
state++;
token = "";
} else if (is_digit(x))
token += x;
else
return null;
break;
case 2:
if (is_digit(x)) {
token += x;
if (token.length() > 2)
return null;
} else if (x == '.') {
token += x;
state = 3;
} else
return null;
break;
case 3:
if (is_digit(x))
token += x;
else
return null;
break;
default:
assert false;
return null;
}
}
if (!(state == 3 || state == 2))
return null;
// get seconds
// this is whole + dot + nothing else
if (token.length() == 3)
return null;
ret[2] = Double.parseDouble(token);
if (ret[0] == 24.0) {
ret[0] = 00.0;
}
// XXX sanity check args...
return ret;
}
// returns
// positive/negative
// hour
// minute
/**
* Parses a String representation of a date and time and retrieves the
* timezone from it
*
* @param str
* The String representation of the date (and optional timezone)
* @return Integer array of size 3. Element 1 represents whether the
* timezone is ahead or behind GMT, element 2 is the hour
* displacement and element 3 is the minute displacement.
*/
public static int[] parse_timezone(String str) {
int[] ret = new int[3];
for (int i = 0; i < ret.length; i++)
ret[i] = 0;
ret[0] = 1;
if (str.equals("Z")) {
return ret;
}
// get initial plus/minus
if (str.startsWith("+"))
ret[0] = 1;
else if (str.startsWith("-"))
ret[0] = -1;
else
return null;
str = str.substring(1, str.length());
if (str.length() != (2 + 1 + 2))
return null;
try {
ret[1] = Integer.parseInt(str.substring(0, 2));
ret[2] = Integer.parseInt(str.substring(3, 5));
// According to schema spec, timezone is limited to
// this... [well.. almost...]
if (ret[1] > 14)
return null;
if (ret[2] > 59)
return null;
return ret;
} catch (NumberFormatException err) {
return null;
}
}
/**
* Attempts to set a particular field in the Calendar
*
* @param cal
* The Calendar object to set the field in
* @param item
* The field to set
* @param val
* The value to set the field to
* @return True if successfully set. False otherwise (due to out of bounds
* for that field)
*/
private static boolean set_item(Calendar cal, int item, int val) {
int min = cal.getActualMinimum(item);
if (val < min)
return false;
int max = cal.getActualMaximum(item);
if (val > max)
return false;
cal.set(item, val);
return true;
}
/**
* Parses a String representation of a date and time and constructs a new
* XSDateTime object using that information
*
* @param str
* The String representation of the date (and optional timezone)
* @return The XSDateTime representation of the date and time (and optional
* timezone)
*/
public static XSDateTime parseDateTime(String str) {
// oh no... not again
// ok its three things:
// date T time timezone
int index = str.indexOf('T');
if (index == -1)
return null;
// split date and rest...
String date = str.substring(0, index);
String time = str.substring(index + 1, str.length());
String timezone = null;
// check for timezone
index = time.indexOf('+');
if (index == -1)
index = time.indexOf('-');
if (index == -1)
index = time.indexOf('Z');
if (index != -1) {
timezone = time.substring(index, time.length());
time = time.substring(0, index);
}
// get date
int d[] = parse_date(date);
if (d == null)
return null;
// SANITY CHEX
TimeZone UTC = TimeZone.getTimeZone("UTC");
GregorianCalendar cal = new GregorianCalendar(UTC);
// year
int year = d[0];
if (year < 0) {
year *= -1;
cal.set(Calendar.ERA, GregorianCalendar.BC);
} else {
cal.set(Calendar.ERA, GregorianCalendar.AD);
}
// this is a nice bug....
// if say the current day is 29...
// then if we set the month to feb for example, and 29 doesn't
// exist in that year, then the date screws up.
cal.set(Calendar.DAY_OF_MONTH, 2);
cal.set(Calendar.MONTH, 2);
if (!set_item(cal, Calendar.YEAR, year))
return null;
if (!set_item(cal, Calendar.MONTH, d[1] - 1))
return null;
if (!set_item(cal, Calendar.DAY_OF_MONTH, d[2]))
return null;
// get time
double t[] = parse_time(time);
if (t == null)
return null;
if (!set_item(cal, Calendar.HOUR_OF_DAY, (int) t[0]))
return null;
if (!set_item(cal, Calendar.MINUTE, (int) t[1]))
return null;
if (!set_item(cal, Calendar.SECOND, (int) t[2]))
return null;
double ms = t[2] - ((int) t[2]);
ms *= 1000;
if (!set_item(cal, Calendar.MILLISECOND, (int) ms))
return null;
// get timezone
int tz[] = null;
XSDuration tzd = null;
if (timezone != null) {
tz = parse_timezone(timezone);
if (tz == null)
return null;
tzd = new XSDayTimeDuration(0, tz[1], tz[2], 0.0, tz[0] < 0);
}
return new XSDateTime(cal, tzd);
}
/**
* Creates a new result sequence consisting of the retrievable date and time
* value in the supplied result sequence
*
* @param arg
* The result sequence from which to extract the date and time
* value.
* @throws DynamicError
* @return A new result sequence consisting of the date and time value
* supplied.
*/
public ResultSequence constructor(ResultSequence arg) throws DynamicError {
if (arg.empty())
return ResultBuffer.EMPTY;
AnyAtomicType aat = (AnyAtomicType) arg.first();
if (aat instanceof NumericType || aat instanceof XSDuration
|| aat instanceof XSTime || isGDataType(aat)
|| aat instanceof XSBoolean || aat instanceof XSBase64Binary
|| aat instanceof XSHexBinary || aat instanceof XSAnyURI) {
throw DynamicError.invalidType();
}
if (!isCastable(aat)) {
throw DynamicError.cant_cast(null);
}
CalendarType dt = castDateTime(aat);
if (dt == null)
throw DynamicError.cant_cast(null);
return dt;
}
private boolean isCastable(AnyAtomicType aat) {
if (aat instanceof XSString || aat instanceof XSUntypedAtomic) {
return true;
}
if (aat instanceof XSTime) {
return false;
}
if (aat instanceof XSDate || aat instanceof XSDateTime) {
return true;
}
return false;
}
private CalendarType castDateTime(AnyAtomicType aat) {
if (aat instanceof XSDate) {
XSDate date = (XSDate) aat;
return new XSDateTime(date.calendar(), date.tz());
}
if (aat instanceof XSDateTime) {
XSDateTime dateTime = (XSDateTime) aat;
return new XSDateTime(dateTime.calendar(), dateTime.tz());
}
return parseDateTime(aat.getStringValue());
}
/**
* Retrieve the year from the date stored
*
* @return the year value of the date stored
*/
public int year() {
int y = _calendar.get(Calendar.YEAR);
if (_calendar.get(Calendar.ERA) == GregorianCalendar.BC)
y *= -1;
return y;
}
/**
* Retrieve the month from the date stored
*
* @return the month value of the date stored
*/
public int month() {
return _calendar.get(Calendar.MONTH) + 1;
}
/**
* Retrieve the day from the date stored
*
* @return the day value of the date stored
*/
public int day() {
return _calendar.get(Calendar.DAY_OF_MONTH);
}
/**
* Retrieve the hour from the date stored
*
* @return the hour value of the date stored
*/
public int hour() {
return _calendar.get(Calendar.HOUR_OF_DAY);
}
/**
* Retrieve the minute from the date stored
*
* @return the minute value of the date stored
*/
public int minute() {
return _calendar.get(Calendar.MINUTE);
}
/**
* Retrieve the seconds from the date stored
*
* @return the seconds value of the date stored
*/
public double second() {
double s = _calendar.get(Calendar.SECOND);
double ms = _calendar.get(Calendar.MILLISECOND);
ms /= 1000;
s += ms;
return s;
}
public boolean timezoned() {
return _timezoned;
}
/**
* Pads the supplied number to the supplied number of digits by adding 0's
* in front of it
*
* @param num
* Number that si to be padded (if neccessay)
* @param len
* Desired length after padding
* @return String representation of the padded integer
*/
public static String pad_int(int num, int len) {
String ret = "";
String snum = "" + num;
int pad = len - snum.length();
// sort out the negative
if (num < 0) {
ret += "-";
snum = snum.substring(1, snum.length());
pad++;
}
StringBuffer buf = new StringBuffer(ret);
for (int i = 0; i < pad; i++) {
buf.append("0");
}
buf.append(snum);
ret = buf.toString();
return ret;
}
/**
* Retrieves a String representation of the date and time stored
*
* @return String representation of the date and time stored
*/
public String getStringValue() {
String ret = "";
Calendar adjustFortimezone = calendar();
if (adjustFortimezone.get(Calendar.ERA) == GregorianCalendar.BC) {
ret += "-";
}
ret += pad_int(adjustFortimezone.get(Calendar.YEAR), 4);
ret += "-";
ret += pad_int(month(), 2);
ret += "-";
ret += pad_int(adjustFortimezone.get(Calendar.DAY_OF_MONTH), 2);
// time
ret += "T";
ret += pad_int(adjustFortimezone.get(Calendar.HOUR_OF_DAY), 2);
ret += ":";
ret += pad_int(adjustFortimezone.get(Calendar.MINUTE), 2);
ret += ":";
int isecond = (int) second();
double sec = second();
if ((sec - (isecond)) == 0.0)
ret += pad_int(isecond, 2);
else {
if (sec < 10.0)
ret += "0" + sec;
else
ret += sec;
}
if (timezoned()) {
int hrs = _tz.hours();
int min = _tz.minutes();
double secs = _tz.seconds();
if (hrs == 0 && min == 0 && secs == 0) {
ret += "Z";
} else {
String tZoneStr = "";
if (_tz.negative()) {
tZoneStr += "-";
} else {
tZoneStr += "+";
}
tZoneStr += pad_int(hrs, 2);
tZoneStr += ":";
tZoneStr += pad_int(min, 2);
ret += tZoneStr;
}
}
return ret;
}
/**
* Retrive the datatype full pathname
*
* @return "xs:dateTime" which is the datatype full pathname
*/
public String string_type() {
return XS_DATE_TIME;
}
/**
* Retrieves the Calendar representation of the date stored
*
* @return Calendar representation of the date stored
*/
public Calendar calendar() {
return _calendar;
}
// comparisons
/**
* Equality comparison on this and the supplied dates and times (taking
* timezones into account)
*
* @param arg
* XSDateTime representation of the date to compare to
* @throws DynamicError
* @return True if the two dates and times are represent the same exact
* point in time. False otherwise.
*/
public boolean eq(AnyType arg, DynamicContext dynamicContext) throws DynamicError {
XSDateTime val = (XSDateTime) NumericType.get_single_type(arg,
XSDateTime.class);
Calendar thiscal = normalizeCalendar(calendar(), tz());
Calendar thatcal = normalizeCalendar(val.calendar(), val.tz());
return thiscal.equals(thatcal);
}
/**
* Comparison on this and the supplied dates and times (taking timezones
* into account)
*
* @param arg
* XSDateTime representation of the date to compare to
* @throws DynamicError
* @return True if in time, this date and time lies before the date and time
* supplied. False otherwise.
*/
public boolean lt(AnyType arg, DynamicContext context) throws DynamicError {
XSDateTime val = (XSDateTime) NumericType.get_single_type(arg,
XSDateTime.class);
Calendar thiscal = normalizeCalendar(calendar(), tz());
Calendar thatcal = normalizeCalendar(val.calendar(), val.tz());
return thiscal.before(thatcal);
}
/**
* Comparison on this and the supplied dates and times (taking timezones
* into account)
*
* @param arg
* XSDateTime representation of the date to compare to
* @throws DynamicError
* @return True if in time, this date and time lies after the date and time
* supplied. False otherwise.
*/
public boolean gt(AnyType arg, DynamicContext context) throws DynamicError {
XSDateTime val = (XSDateTime) NumericType.get_single_type(arg,
XSDateTime.class);
Calendar thiscal = normalizeCalendar(calendar(), tz());
Calendar thatcal = normalizeCalendar(val.calendar(), val.tz());
return thiscal.after(thatcal);
}
/**
* Retrieves the timezone associated with the date stored
*
* @return the timezone associated with the date stored
*/
public XSDuration tz() {
return _tz;
}
// XXX this is incorrect [epoch]
/**
* Currently unsupported method. Retrieves the date in milliseconds since
* the begining of epoch
*
* @return Number of milliseconds since the begining of the epoch
*/
public double value() {
return calendar().getTimeInMillis() / 1000.0;
}
// math
/**
* Mathematical minus operator between this XSDateTime and a supplied result
* sequence (XSDateTime, XDTYearMonthDuration and XDTDayTimeDuration are
* only valid ones).
*
* @param arg
* The supplied ResultSequence that is on the right of the minus
* operator. If this is an XSDateTime, the result will be a
* XDTDayTimeDuration of the duration of time between these two
* dates. If arg is an XDTYearMonthDuration or an
* XDTDayTimeDuration the result will be a XSDateTime of the
* result of the current date minus the duration of time
* supplied.
* @return New ResultSequence consisting of the result of the mathematical
* minus operation.
*/
public ResultSequence minus(ResultSequence arg) throws DynamicError {
if (arg.size() != 1)
DynamicError.throw_type_error();
Item at = arg.first();
if (!(at instanceof XSDateTime) && !(at instanceof XSYearMonthDuration)
&& !(at instanceof XSDayTimeDuration)) {
DynamicError.throw_type_error();
}
if (at instanceof XSDateTime) {
return minusXSDateTime(arg);
}
if (at instanceof XSYearMonthDuration) {
return minusXSYearMonthDuration(at);
}
if (at instanceof XSDayTimeDuration) {
return minusXSDayTimeDuration(at);
}
return null; // unreach
}
private ResultSequence minusXSDateTime(ResultSequence arg)
throws DynamicError {
XSDateTime val = (XSDateTime) NumericType.get_single_type(arg,
XSDateTime.class);
Calendar thisCal = normalizeCalendar(calendar(), tz());
Calendar thatCal = normalizeCalendar(val.calendar(), val.tz());
long duration = thisCal.getTimeInMillis()
- thatCal.getTimeInMillis();
Duration dtduration = _datatypeFactory.newDuration(duration);
return ResultSequenceFactory.create_new(XSDayTimeDuration
.parseDTDuration(dtduration.toString()));
}
private ResultSequence minusXSDayTimeDuration(Item at) {
XSDuration val = (XSDuration) at;
try {
XSDateTime res = (XSDateTime) clone();
XMLGregorianCalendar xmlCal = _datatypeFactory
.newXMLGregorianCalendar((GregorianCalendar) calendar());
Duration dtduration = _datatypeFactory
.newDuration(val.getStringValue());
xmlCal.add(dtduration.negate());
res = new XSDateTime(xmlCal.toGregorianCalendar(), res.tz());
return ResultSequenceFactory.create_new(res);
} catch (CloneNotSupportedException ex) {
}
return null;
}
private ResultSequence minusXSYearMonthDuration(Item at) {
XSYearMonthDuration val = (XSYearMonthDuration) at;
try {
XSDateTime res = (XSDateTime) clone();
res.calendar().add(Calendar.MONTH, val.monthValue() * -1);
return ResultSequenceFactory.create_new(res);
} catch (CloneNotSupportedException ex) {
}
return null;
}
/**
* Mathematical addition operator between this XSDateTime and a supplied
* result sequence (XDTYearMonthDuration and XDTDayTimeDuration are only
* valid ones).
*
* @param arg
* The supplied ResultSequence that is on the right of the minus
* operator. If arg is an XDTYearMonthDuration or an
* XDTDayTimeDuration the result will be a XSDateTime of the
* result of the current date minus the duration of time
* supplied.
* @return New ResultSequence consisting of the result of the mathematical
* minus operation.
*/
public ResultSequence plus(ResultSequence arg) throws DynamicError {
if (arg.size() != 1)
DynamicError.throw_type_error();
Item at = arg.first();
try {
if (at instanceof XSYearMonthDuration) {
XSYearMonthDuration val = (XSYearMonthDuration) at;
XSDateTime res = (XSDateTime) clone();
res.calendar().add(Calendar.MONTH, val.monthValue());
return ResultSequenceFactory.create_new(res);
} else if (at instanceof XSDayTimeDuration) {
XSDuration val = (XSDuration) at;
XSDateTime res = (XSDateTime) clone();
XMLGregorianCalendar xmlCal = _datatypeFactory
.newXMLGregorianCalendar(
(GregorianCalendar) calendar());
Duration dtduration = _datatypeFactory
.newDuration(val.getStringValue());
xmlCal.add(dtduration);
res = new XSDateTime(xmlCal.toGregorianCalendar(), res.tz());
return ResultSequenceFactory.create_new(res);
} else {
DynamicError.throw_type_error();
return null; // unreach
}
} catch (CloneNotSupportedException err) {
assert false;
return null;
}
}
public TypeDefinition getTypeDefinition() {
return BuiltinTypeLibrary.XS_DATETIME;
}
}