package client.net.sf.saxon.ce.value;
import client.net.sf.saxon.ce.Controller;
import client.net.sf.saxon.ce.expr.XPathContext;
import client.net.sf.saxon.ce.expr.sort.ComparisonKey;
import client.net.sf.saxon.ce.functions.Component;
import client.net.sf.saxon.ce.om.StandardNames;
import client.net.sf.saxon.ce.trans.Err;
import client.net.sf.saxon.ce.trans.NoDynamicContextException;
import client.net.sf.saxon.ce.trans.XPathException;
import client.net.sf.saxon.ce.tree.util.FastStringBuffer;
import client.net.sf.saxon.ce.type.BuiltInAtomicType;
import client.net.sf.saxon.ce.type.ConversionResult;
import client.net.sf.saxon.ce.type.ValidationFailure;
import com.google.gwt.regexp.shared.MatchResult;
import com.google.gwt.regexp.shared.RegExp;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.Date;
/**
* A value of type DateTime
*/
public final class DateTimeValue extends CalendarValue implements Comparable {
private int year; // the year as written, +1 for BC years
private int month; // the month as written, range 1-12
private int day; // the day as written, range 1-31
private int hour; // the hour as written (except for midnight), range 0-23
private int minute; // the minutes as written, range 0-59
private int second; // the seconds as written, range 0-59 (no leap seconds)
private int microsecond;
/**
* Private default constructor
*/
private DateTimeValue() {
}
/**
* Get the dateTime value representing the nominal
* date/time of this transformation run. Two calls within the same
* query or transformation will always return the same answer.
*
* @param context the XPath dynamic context. May be null, in which case
* the current date and time are taken directly from the system clock
* @return the current xs:dateTime
*/
public static DateTimeValue getCurrentDateTime(XPathContext context) {
Controller c;
if (context == null || (c = context.getController()) == null) {
// non-XSLT/XQuery environment
// We also take this path when evaluating compile-time expressions that require an implicit timezone.
try {
return DateTimeValue.fromJavaDate(new Date());
} catch (XPathException e) {
throw new IllegalStateException();
}
} else {
return c.getCurrentDateTime();
}
}
/**
* Factory method: create a dateTime value given a Java Date object. The returned dateTime
* value will always have a timezone, which will always be UTC.
*
* @param suppliedDate holds the date and time
* @return the corresponding xs:dateTime value
*/
public static DateTimeValue fromJavaDate(Date suppliedDate) throws XPathException {
long millis = suppliedDate.getTime();
return (DateTimeValue)EPOCH.add(DayTimeDurationValue.fromMilliseconds(millis));
}
/**
* Fixed date/time used by Java (and Unix) as the origin of the universe: 1970-01-01
*/
public static final DateTimeValue EPOCH =
new DateTimeValue(1970, 1, 1, 0, 0, 0, 0, 0);
/**
* Factory method: create a dateTime value given a date and a time.
*
* @param date the date
* @param time the time
* @return the dateTime with the given components. If either component is null, returns null
* @throws XPathException if the timezones are both present and inconsistent
*/
public static DateTimeValue makeDateTimeValue(DateValue date, TimeValue time) throws XPathException {
if (date == null || time == null) {
return null;
}
int tz1 = date.getTimezoneInMinutes();
int tz2 = time.getTimezoneInMinutes();
if (tz1 != NO_TIMEZONE && tz2 != NO_TIMEZONE && tz1 != tz2) {
XPathException err = new XPathException("Supplied date and time are in different timezones");
err.setErrorCode("FORG0008");
throw err;
}
DateTimeValue v = date.toDateTime();
v.hour = time.getHour();
v.minute = time.getMinute();
v.second = time.getSecond();
v.microsecond = time.getMicrosecond();
v.setTimezoneInMinutes(Math.max(tz1, tz2));
v.typeLabel = BuiltInAtomicType.DATE_TIME;
return v;
}
private static RegExp dateTimePattern =
RegExp.compile("\\-?([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])T([0-9][0-9]):([0-9][0-9]):([0-9][0-9])(\\.[0-9]*)?([-+Z].*)?");
public static ConversionResult makeDateTimeValue(CharSequence s) {
String str = s.toString();
MatchResult match = dateTimePattern.exec(str);
if (match == null) {
return badDate("wrong format", str);
}
DateTimeValue dt = new DateTimeValue();
dt.year = DurationValue.simpleInteger(match.getGroup(1));
if (str.startsWith("-")) {
dt.year = dt.year - 1; // no year zero in lexical space for XSD 1.0 - so -1 becomes 0 and -2 becomes -1 etc.
dt.year = -dt.year;
}
dt.month = DurationValue.simpleInteger(match.getGroup(2));
dt.day = DurationValue.simpleInteger(match.getGroup(3));
dt.hour = DurationValue.simpleInteger(match.getGroup(4));
dt.minute = DurationValue.simpleInteger(match.getGroup(5));
dt.second = DurationValue.simpleInteger(match.getGroup(6));
String frac = match.getGroup(7);
if (frac != null && frac.length() > 0) {
double fractionalSeconds = Double.parseDouble(frac);
dt.microsecond = (int)(Math.round(fractionalSeconds * 1000000));
}
String tz = match.getGroup(8);
int tzmin = parseTimezone(tz);
if (tzmin == BAD_TIMEZONE) {
return badDate("Invalid timezone", str);
}
dt.setTimezoneInMinutes(tzmin);
if (dt.year == 0) {
return badDate("year zero", str);
}
// Check that this is a valid calendar date
if (!DateValue.isValidDate(dt.year, dt.month, dt.day)) {
return badDate("Non-existent date", s);
}
// Adjust midnight to 00:00 on the following day
if (dt.hour == 24) {
if (dt.minute != 0 || dt.second != 0 || dt.microsecond != 0) {
return badDate("after midnight", str);
} else {
dt.hour = 0;
DateValue tomorrow = DateValue.tomorrow(dt.year, dt.month, dt.day);
dt.year = tomorrow.getYear();
dt.month = tomorrow.getMonth();
dt.day = tomorrow.getDay();
}
}
dt.typeLabel = BuiltInAtomicType.DATE_TIME;
return dt;
}
private static ValidationFailure badDate(String msg, CharSequence value) {
ValidationFailure err = new ValidationFailure(
"Invalid dateTime value " + Err.wrap(value, Err.VALUE) + " (" + msg + ")");
err.setErrorCode("FORG0001");
return err;
}
/**
* Constructor: construct a DateTimeValue from its components.
* This constructor performs no validation.
*
* @param year The year as held internally (note that the year before 1AD is 0)
* @param month The month, 1-12
* @param day The day 1-31
* @param hour the hour value, 0-23
* @param minute the minutes value, 0-59
* @param second the seconds value, 0-59
* @param microsecond the number of microseconds, 0-999999
* @param tz the timezone displacement in minutes from UTC. Supply the value
* {@link CalendarValue#NO_TIMEZONE} if there is no timezone component.
*/
public DateTimeValue(int year, int month, int day,
int hour, int minute, int second, int microsecond, int tz) {
this.year = year;
this.month = month;
this.day = day;
this.hour = hour;
this.minute = minute;
this.second = second;
this.microsecond = microsecond;
setTimezoneInMinutes(tz);
typeLabel = BuiltInAtomicType.DATE_TIME;
}
/**
* Determine the primitive type of the value. This delivers the same answer as
* getItemType().getPrimitiveItemType(). The primitive types are
* the 19 primitive types of XML Schema, plus xs:integer, xs:dayTimeDuration and xs:yearMonthDuration,
* and xs:untypedAtomic. For external objects, the result is AnyAtomicType.
*/
public BuiltInAtomicType getPrimitiveType() {
return BuiltInAtomicType.DATE_TIME;
}
/**
* Get the year component, in its internal form (which allows a year zero)
*
* @return the year component
*/
public int getYear() {
return year;
}
/**
* Get the month component, 1-12
*
* @return the month component
*/
public int getMonth() {
return month;
}
/**
* Get the day component, 1-31
*
* @return the day component
*/
public int getDay() {
return day;
}
/**
* Get the hour component, 0-23
*
* @return the hour component (never 24, even if the input was specified as 24:00:00)
*/
public int getHour() {
return hour;
}
/**
* Get the minute component, 0-59
*
* @return the minute component
*/
public int getMinute() {
return minute;
}
/**
* Get the second component, 0-59
*
* @return the second component
*/
public int getSecond() {
return second;
}
/**
* Get the microsecond component, 0-999999
*
* @return the microsecond component
*/
public int getMicrosecond() {
return microsecond;
}
/**
* Convert the value to a DateTime, retaining all the components that are actually present, and
* substituting conventional values for components that are missing. (This method does nothing in
* the case of xs:dateTime, but is there to implement a method in the {@link CalendarValue} interface).
*
* @return the value as an xs:dateTime
*/
public DateTimeValue toDateTime() {
return this;
}
/**
* Normalize the date and time to be in timezone Z.
*
* @param cc used to supply the implicit timezone, used when the value has
* no explicit timezone
* @return in general, a new DateTimeValue in timezone Z, representing the same instant in time.
* Returns the original DateTimeValue if this is already in timezone Z.
* @throws NoDynamicContextException if the implicit timezone is needed and is not available
*/
public DateTimeValue normalize(XPathContext cc) throws NoDynamicContextException {
if (hasTimezone()) {
return (DateTimeValue)adjustTimezone(0);
} else {
DateTimeValue dt = (DateTimeValue) copy();
dt.setTimezoneInMinutes(cc.getImplicitTimezone());
return (DateTimeValue)dt.adjustTimezone(0);
}
}
/**
* Get a comparison key for this value. Two values are equal if and only if they their comparison
* keys are equal
* @param context XPath dynamic context
* @throws NoDynamicContextException if the implicit timezone is needed and is not available
*/
public ComparisonKey getComparisonKey(XPathContext context) throws NoDynamicContextException {
return new ComparisonKey(StandardNames.XS_DATE_TIME, normalize(context));
}
/**
* Get the Julian instant: a decimal value whose integer part is the Julian day number
* multiplied by the number of seconds per day,
* and whose fractional part is the fraction of the second.
* This method operates on the local time, ignoring the timezone. The caller should call normalize()
* before calling this method to get a normalized time.
*
* @return the Julian instant corresponding to this xs:dateTime value
*/
public BigDecimal toJulianInstant() {
int julianDay = DateValue.getJulianDayNumber(year, month, day);
long julianSecond = julianDay * (24L * 60L * 60L);
julianSecond += (((hour * 60L + minute) * 60L) + second);
BigDecimal j = BigDecimal.valueOf(julianSecond);
if (microsecond == 0) {
return j;
} else {
return j.add(BigDecimal.valueOf(microsecond).divide(DecimalValue.BIG_DECIMAL_ONE_MILLION, 6, BigDecimal.ROUND_HALF_EVEN));
}
}
/**
* Get the DateTimeValue corresponding to a given Julian instant
*
* @param instant the Julian instant: a decimal value whose integer part is the Julian day number
* multiplied by the number of seconds per day, and whose fractional part is the fraction of the second.
* @return the xs:dateTime value corresponding to the Julian instant. This will always be in timezone Z.
*/
public static DateTimeValue fromJulianInstant(BigDecimal instant) {
BigInteger julianSecond = instant.toBigInteger();
BigDecimal microseconds = instant.subtract(new BigDecimal(julianSecond)).multiply(DecimalValue.BIG_DECIMAL_ONE_MILLION);
long js = julianSecond.longValue();
long jd = js / (24L * 60L * 60L);
DateValue date = DateValue.dateFromJulianDayNumber((int)jd);
js = js % (24L * 60L * 60L);
byte hour = (byte)(js / (60L * 60L));
js = js % (60L * 60L);
byte minute = (byte)(js / (60L));
js = js % (60L);
return new DateTimeValue(date.getYear(), date.getMonth(), date.getDay(),
hour, minute, (byte)js, microseconds.intValue(),0);
}
/**
* Convert to target data type
*
* @param requiredType an integer identifying the required atomic type
* @return an AtomicValue, a value of the required type; or an ErrorValue
*/
public ConversionResult convertPrimitive(BuiltInAtomicType requiredType, boolean validate) {
switch (requiredType.getPrimitiveType()) {
case StandardNames.XS_DATE_TIME:
case StandardNames.XS_ANY_ATOMIC_TYPE:
return this;
case StandardNames.XS_DATE:
return new DateValue(year, month, day, getTimezoneInMinutes());
case StandardNames.XS_TIME:
return new TimeValue(hour, minute, second, microsecond, getTimezoneInMinutes());
case StandardNames.XS_G_YEAR:
return new GYearValue(year, getTimezoneInMinutes());
case StandardNames.XS_G_YEAR_MONTH:
return new GYearMonthValue(year, month, getTimezoneInMinutes());
case StandardNames.XS_G_MONTH:
return new GMonthValue(month, getTimezoneInMinutes());
case StandardNames.XS_G_MONTH_DAY:
return new GMonthDayValue(month, day, getTimezoneInMinutes());
case StandardNames.XS_G_DAY:
return new GDayValue(day, getTimezoneInMinutes());
case StandardNames.XS_STRING:
return new StringValue(getStringValueCS());
case StandardNames.XS_UNTYPED_ATOMIC:
return new UntypedAtomicValue(getStringValueCS());
default:
ValidationFailure err = new ValidationFailure("Cannot convert dateTime to " +
requiredType.getDisplayName());
err.setErrorCode("XPTY0004");
return err;
}
}
/**
* Convert to string
*
* @return ISO 8601 representation. The value returned is the localized representation,
* that is it uses the timezone contained within the value itself.
*/
public CharSequence getPrimitiveStringValue() {
FastStringBuffer sb = new FastStringBuffer(30);
int yr = year;
if (year <= 0) {
yr = -yr + 1; // no year zero in lexical space for XSD 1.0, so zero becomes -1 in string representation
if(yr!=0){
sb.append('-');
}
}
appendString(sb, yr, (yr > 9999 ? (yr + "").length() : 4));
sb.append('-');
appendTwoDigits(sb, month);
sb.append('-');
appendTwoDigits(sb, day);
sb.append('T');
appendTwoDigits(sb, hour);
sb.append(':');
appendTwoDigits(sb, minute);
sb.append(':');
appendTwoDigits(sb, second);
if (microsecond != 0) {
sb.append('.');
int ms = microsecond;
int div = 100000;
while (ms > 0) {
int d = ms / div;
sb.append((char)(d + '0'));
ms = ms % div;
div /= 10;
}
}
if (hasTimezone()) {
appendTimezone(sb);
}
return sb;
}
/**
* Get the canonical lexical representation as defined in XML Schema. This is not always the same
* as the result of casting to a string according to the XPath rules. For an xs:dateTime it is the
* date/time adjusted to UTC.
*
* @return the canonical lexical representation as defined in XML Schema
*/
public CharSequence getCanonicalLexicalRepresentation() {
if (hasTimezone() && getTimezoneInMinutes() != 0) {
return adjustTimezone(0).getStringValueCS();
} else {
return getStringValueCS();
}
}
/**
* Make a copy of this date, time, or dateTime value, but with a new type label
*
*/
public AtomicValue copy() {
DateTimeValue v = new DateTimeValue(year, month, day,
hour, minute, second, microsecond, getTimezoneInMinutes());
v.typeLabel = typeLabel;
return v;
}
/**
* Return a new dateTime with the same normalized value, but
* in a different timezone.
*
* @param timezone the new timezone offset, in minutes
* @return the date/time in the new timezone. This will be a new DateTimeValue unless no change
* was required to the original value
*/
public CalendarValue adjustTimezone(int timezone) {
if (!hasTimezone()) {
CalendarValue in = (CalendarValue) copy();
in.setTimezoneInMinutes(timezone);
return in;
}
int oldtz = getTimezoneInMinutes();
if (oldtz == timezone) {
return this;
}
int tz = timezone - oldtz;
int h = hour;
int mi = minute;
mi += tz;
if (mi < 0 || mi > 59) {
h += Math.floor(mi / 60.0);
mi = (mi + 60 * 24) % 60;
}
if (h >= 0 && h < 24) {
return new DateTimeValue(year, month, day, (byte)h, (byte)mi, second, microsecond, timezone);
}
// Following code is designed to handle the corner case of adjusting from -14:00 to +14:00 or
// vice versa, which can cause a change of two days in the date
DateTimeValue dt = this;
while (h < 0) {
h += 24;
DateValue t = DateValue.yesterday(dt.getYear(), dt.getMonth(), dt.getDay());
dt = new DateTimeValue(t.getYear(), t.getMonth(), t.getDay(),
(byte)h, (byte)mi, second, microsecond, timezone);
}
if (h > 23) {
h -= 24;
DateValue t = DateValue.tomorrow(year, month, day);
return new DateTimeValue(t.getYear(), t.getMonth(), t.getDay(),
(byte)h, (byte)mi, second, microsecond, timezone);
}
return dt;
}
/**
* Add a duration to a dateTime
*
* @param duration the duration to be added (may be negative)
* @return the new date
* @throws client.net.sf.saxon.ce.trans.XPathException
* if the duration is an xs:duration, as distinct from
* a subclass thereof
*/
public CalendarValue add(DurationValue duration) throws XPathException {
if (duration instanceof DayTimeDurationValue) {
long microseconds = ((DayTimeDurationValue)duration).getLengthInMicroseconds();
BigDecimal seconds = BigDecimal.valueOf(microseconds).divide(
DecimalValue.BIG_DECIMAL_ONE_MILLION, 6, BigDecimal.ROUND_HALF_EVEN);
BigDecimal julian = toJulianInstant();
julian = julian.add(seconds);
DateTimeValue dt = fromJulianInstant(julian);
dt.setTimezoneInMinutes(getTimezoneInMinutes());
return dt;
} else if (duration instanceof YearMonthDurationValue) {
int months = ((YearMonthDurationValue)duration).getLengthInMonths();
int m = (month - 1) + months;
int y = year + m / 12;
m = m % 12;
if (m < 0) {
m += 12;
y -= 1;
}
m++;
int d = day;
while (!DateValue.isValidDate(y, m, d)) {
d -= 1;
}
return new DateTimeValue(y, (byte)m, (byte)d,
hour, minute, second, microsecond, getTimezoneInMinutes());
} else {
XPathException err = new XPathException("DateTime arithmetic is not supported on xs:duration, only on its subtypes");
err.setIsTypeError(true);
throw err;
}
}
/**
* Determine the difference between two points in time, as a duration
*
* @param other the other point in time
* @param context the XPath dynamic context
* @return the duration as an xs:dayTimeDuration
* @throws client.net.sf.saxon.ce.trans.XPathException
* for example if one value is a date and the other is a time
*/
public DayTimeDurationValue subtract(CalendarValue other, XPathContext context) throws XPathException {
if (!(other instanceof DateTimeValue)) {
XPathException err = new XPathException("First operand of '-' is a dateTime, but the second is not");
err.setIsTypeError(true);
throw err;
}
return super.subtract(other, context);
}
/**
* Get a component of the value. Returns null if the timezone component is
* requested and is not present.
*/
public AtomicValue getComponent(int component) throws XPathException {
switch (component) {
case Component.YEAR_ALLOWING_ZERO:
return IntegerValue.makeIntegerValue(year);
case Component.YEAR:
return IntegerValue.makeIntegerValue(year > 0 ? year : year - 1);
case Component.MONTH:
return IntegerValue.makeIntegerValue(month);
case Component.DAY:
return IntegerValue.makeIntegerValue(day);
case Component.HOURS:
return IntegerValue.makeIntegerValue(hour);
case Component.MINUTES:
return IntegerValue.makeIntegerValue(minute);
case Component.SECONDS:
BigDecimal d = BigDecimal.valueOf(microsecond);
d = d.divide(DecimalValue.BIG_DECIMAL_ONE_MILLION, 6, BigDecimal.ROUND_HALF_UP);
d = d.add(BigDecimal.valueOf(second));
return new DecimalValue(d);
case Component.WHOLE_SECONDS: //(internal use only)
return IntegerValue.makeIntegerValue(second);
case Component.MICROSECONDS:
// internal use only
return IntegerValue.makeIntegerValue(microsecond);
case Component.TIMEZONE:
if (hasTimezone()) {
return DayTimeDurationValue.fromMilliseconds(60000L * getTimezoneInMinutes());
} else {
return null;
}
default:
throw new IllegalArgumentException("Unknown component for dateTime: " + component);
}
}
/**
* Compare the value to another dateTime value, following the XPath comparison semantics
*
* @param other The other dateTime value
* @param context XPath dynamic evaluation context
* @return negative value if this one is the earler, 0 if they are chronologically equal,
* positive value if this one is the later. For this purpose, dateTime values with an unknown
* timezone are considered to be values in the implicit timezone (the Comparable interface requires
* a total ordering).
* @throws ClassCastException if the other value is not a DateTimeValue (the parameter
* is declared as CalendarValue to satisfy the interface)
* @throws NoDynamicContextException if the implicit timezone is needed and is not available
*/
public int compareTo(CalendarValue other, XPathContext context) throws NoDynamicContextException {
if (!(other instanceof DateTimeValue)) {
throw new ClassCastException("DateTime values are not comparable to " + other.getClass());
}
DateTimeValue v2 = (DateTimeValue)other;
if (getTimezoneInMinutes() == v2.getTimezoneInMinutes()) {
// both values are in the same timezone (explicitly or implicitly)
if (year != v2.year) {
return IntegerValue.signum(year - v2.year);
}
if (month != v2.month) {
return IntegerValue.signum(month - v2.month);
}
if (day != v2.day) {
return IntegerValue.signum(day - v2.day);
}
if (hour != v2.hour) {
return IntegerValue.signum(hour - v2.hour);
}
if (minute != v2.minute) {
return IntegerValue.signum(minute - v2.minute);
}
if (second != v2.second) {
return IntegerValue.signum(second - v2.second);
}
if (microsecond != v2.microsecond) {
return IntegerValue.signum(microsecond - v2.microsecond);
}
return 0;
}
return normalize(context).compareTo(v2.normalize(context), context);
}
/**
* Context-free comparison of two DateTimeValue values. For this to work,
* the two values must either both have a timezone or both have none.
* @param v2 the other value
* @return the result of the comparison: -1 if the first is earlier, 0 if they
* are equal, +1 if the first is later
* @throws ClassCastException if the values are not comparable (which might be because
* no timezone is available)
*/
public int compareTo(Object v2) {
try {
return compareTo((DateTimeValue)v2, null);
} catch (Exception err) {
throw new ClassCastException("DateTime comparison requires access to implicit timezone");
}
}
/**
* Context-free comparison of two dateTime values
* @param o the other date time value
* @return true if the two values represent the same instant in time
* @throws ClassCastException if one of the values has a timezone and the other does not
*/
public boolean equals(Object o) {
//noinspection RedundantCast
return compareTo((DateTimeValue)o) == 0;
}
/**
* Hash code for context-free comparison of date time values. Note that equality testing
* and therefore hashCode() works only for values with a timezone
* @return a hash code
*/
public int hashCode() {
return hashCode(year, month, day, hour, minute, second, microsecond, getTimezoneInMinutes());
}
static int hashCode(int year, int month, int day, int hour, int minute, int second, int microsecond, int tzMinutes) {
int tz = -tzMinutes;
int h = hour;
int mi = minute;
mi += tz;
if (mi < 0 || mi > 59) {
h += Math.floor(mi / 60.0);
mi = (mi + 60 * 24) % 60;
}
while (h < 0) {
h += 24;
DateValue t = DateValue.yesterday(year, month, day);
year = t.getYear();
month = t.getMonth();
day = t.getDay();
}
while (h > 23) {
h -= 24;
DateValue t = DateValue.tomorrow(year, month, day);
year = t.getYear();
month = t.getMonth();
day = t.getDay();
}
return (year<<4) ^ (month<<28) ^ (day<<23) ^ (h<<18) ^ (mi<<13) ^ second ^ microsecond;
}
}
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// This Source Code Form is “Incompatible With Secondary Licenses”, as defined by the Mozilla Public License, v. 2.0.