package client.net.sf.saxon.ce.value;
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.type.ConversionResult;
import client.net.sf.saxon.ce.type.ValidationFailure;
import com.google.gwt.regexp.shared.MatchResult;
import com.google.gwt.regexp.shared.RegExp;
/**
* Abstract superclass for the primitive types containing date components: xs:date, xs:gYear,
* xs:gYearMonth, xs:gMonth, xs:gMonthDay, xs:gDay
*/
public abstract class GDateValue extends CalendarValue {
protected int year; // unlike the lexical representation, includes a year zero
protected int month;
protected int day;
/**
* Test whether a candidate date is actually a valid date in the proleptic Gregorian calendar
*/
protected static byte[] daysPerMonth = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
protected static final short[] monthData = {306, 337, 0, 31, 61, 92, 122, 153, 184, 214, 245, 275};
/**
* Get the year component of the date (in local form)
* @return the year component, as represented internally (allowing a year zero)
*/
public int getYear() {
return year;
}
/**
* Get the month component of the date (in local form)
* @return the month component (1-12)
*/
public int getMonth() {
return month;
}
/**
* Get the day component of the date (in local form)
* @return the day component (1-31)
*/
public int getDay() {
return day;
}
private static RegExp datePattern =
RegExp.compile("\\-?([0-9]+)-([0-9][0-9])-([0-9][0-9])([-+Z].*)?");
protected static ConversionResult setLexicalValue(GDateValue dt, CharSequence s) {
String str = s.toString();
MatchResult match = datePattern.exec(str);
if (match == null) {
return badDate("wrong format", str);
}
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));
String tz = match.getGroup(4);
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);
}
return dt;
}
private static ValidationFailure badDate(String msg, CharSequence value) {
ValidationFailure err = new ValidationFailure(
"Invalid date " + Err.wrap(value, Err.VALUE) + " (" + msg + ")");
err.setErrorCode("FORG0001");
return err;
}
/**
* Determine whether a given date is valid
* @param year the year (permitting year zero)
* @param month the month (1-12)
* @param day the day (1-31)
* @return true if this is a valid date
*/
public static boolean isValidDate(int year, int month, int day) {
return month > 0 && month <= 12 && day > 0 && day <= daysPerMonth[month - 1]
|| month == 2 && day == 29 && isLeapYear(year);
}
/**
* Test whether a year is a leap year
* @param year the year (permitting year zero)
* @return true if the supplied year is a leap year
*/
public static boolean isLeapYear(int year) {
return (year % 4 == 0) && !(year % 100 == 0 && !(year % 400 == 0));
}
/**
* The equals() methods on atomic values is defined to follow the semantics of eq when applied
* to two atomic values. When the other operand is not an atomic value, the result is undefined
* (may be false, may be an exception). When the other operand is an atomic value that cannot be
* compared with this one, the method returns false.
* <p/>
* <p>The hashCode() method is consistent with equals().</p>
*
* <p>This implementation performs a context-free comparison: it fails with ClassCastException
* if one value has a timezone and the other does not.</p>
*
* @param o the other value
* @return true if the other operand is an atomic value and the two values are equal as defined
* by the XPath eq operator
* @throws ClassCastException if the values are not comparable
*/
public boolean equals(Object o) {
if (o instanceof GDateValue) {
GDateValue gdv = (GDateValue)o;
return getPrimitiveType() == gdv.getPrimitiveType() && toDateTime().equals(gdv.toDateTime());
} else {
return false;
}
}
public int hashCode() {
return DateTimeValue.hashCode(year, month, day, 12, 0, 0, 0, getTimezoneInMinutes());
}
/**
* Compare this value to another value of the same type, using the supplied context object
* to get the implicit timezone if required. This method implements the XPath comparison semantics.
* @param other the value to be compared
* @param context the XPath dynamic evaluation context (needed only to get the implicit timezone)
* @return -1 if this value is less, 0 if equal, +1 if greater
*/
public int compareTo(CalendarValue other, XPathContext context) throws NoDynamicContextException {
if (getPrimitiveType() != other.getPrimitiveType()) {
throw new ClassCastException("Cannot compare dates of different types");
// covers, for example, comparing a gYear to a gYearMonth
}
GDateValue v2 = (GDateValue)other;
if (getTimezoneInMinutes() == other.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);
}
return 0;
}
return toDateTime().compareTo(other.toDateTime(), context);
}
/**
* Convert to DateTime.
* @return the starting instant of the GDateValue (with the same timezone)
*/
public DateTimeValue toDateTime() {
return new DateTimeValue(year, month, day, (byte)0, (byte)0, (byte)0, 0, getTimezoneInMinutes());
}
/**
* 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.TIMEZONE:
if (hasTimezone()) {
return DayTimeDurationValue.fromMilliseconds(60000L * getTimezoneInMinutes());
} else {
return null;
}
default:
throw new IllegalArgumentException("Unknown component for date: " + component);
}
}
/**
* 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 evaluation context
* @throws NoDynamicContextException if the implicit timezone is required and is not available
* (because the method is being called at compile time)
*/
public ComparisonKey getComparisonKey(XPathContext context) throws NoDynamicContextException {
return new ComparisonKey(StandardNames.XS_DATE, toDateTime().normalize(context));
}
}
// 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.