/** * Add a duration to a dateTime * * @param duration the duration to be added (may be negative) * @return the new date * @throws net.sf.saxon.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; } }
/** * Make a copy of this date, time, or dateTime value, but with a new type label * * @param typeLabel the type label to be attached to the new copy. It is the caller's * responsibility to ensure that the value actually conforms to the rules for this type. */ public AtomicValue copyAsSubType(AtomicType typeLabel) { DateTimeValue v = new DateTimeValue( year, month, day, hour, minute, second, microsecond, getTimezoneInMinutes()); v.typeLabel = typeLabel; return v; }
/** * 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; } DayTimeDurationValue tz1 = (DayTimeDurationValue) date.getComponent(Component.TIMEZONE); DayTimeDurationValue tz2 = (DayTimeDurationValue) time.getComponent(Component.TIMEZONE); boolean zoneSpecified = (tz1 != null || tz2 != null); if (tz1 != null && tz2 != null && !tz1.equals(tz2)) { XPathException err = new XPathException("Supplied date and time are in different timezones"); err.setErrorCode("FORG0008"); throw err; } DateTimeValue v = new DateTimeValue(); v.year = (int) ((Int64Value) date.getComponent(Component.YEAR_ALLOWING_ZERO)).longValue(); v.month = (byte) ((Int64Value) date.getComponent(Component.MONTH)).longValue(); v.day = (byte) ((Int64Value) date.getComponent(Component.DAY)).longValue(); v.hour = (byte) ((Int64Value) time.getComponent(Component.HOURS)).longValue(); v.minute = (byte) ((Int64Value) time.getComponent(Component.MINUTES)).longValue(); final BigDecimal secs = ((DecimalValue) time.getComponent(Component.SECONDS)).getDecimalValue(); v.second = (byte) secs.intValue(); v.microsecond = secs.multiply(BigDecimal.valueOf(1000000)).intValue() % 1000000; if (zoneSpecified) { if (tz1 == null) { tz1 = tz2; } v.setTimezoneInMinutes((int) (tz1.getLengthInMicroseconds() / 60000000)); } v.typeLabel = BuiltInAtomicType.DATE_TIME; return v; }
/** * 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); }
/** * 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) copyAsSubType(null); dt.setTimezoneInMinutes(cc.getImplicitTimezone()); return (DateTimeValue) dt.adjustTimezone(0); } }
/** * 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) copyAsSubType(typeLabel); 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; }
/** * Factory method: create a dateTime value from a supplied string, in ISO 8601 format * * @param s a string in the lexical space of xs:dateTime * @return either a DateTimeValue representing the xs:dateTime supplied, or a ValidationFailure if * the lexical value was invalid */ public static ConversionResult makeDateTimeValue(CharSequence s) { // input must have format [-]yyyy-mm-ddThh:mm:ss[.fff*][([+|-]hh:mm | Z)] DateTimeValue dt = new DateTimeValue(); StringTokenizer tok = new StringTokenizer(Whitespace.trimWhitespace(s).toString(), "-:.+TZ", true); if (!tok.hasMoreElements()) { return badDate("too short", s); } String part = (String) tok.nextElement(); int era = +1; if ("+".equals(part)) { return badDate("Date must not start with '+' sign", s); } else if ("-".equals(part)) { era = -1; if (!tok.hasMoreElements()) { return badDate("No year after '-'", s); } part = (String) tok.nextElement(); } int value = DurationValue.simpleInteger(part); if (value < 0) { return badDate("Non-numeric year component", s); } dt.year = value * era; if (part.length() < 4) { return badDate("Year is less than four digits", s); } if (part.length() > 4 && part.charAt(0) == '0') { return badDate("When year exceeds 4 digits, leading zeroes are not allowed", s); } if (dt.year == 0) { return badDate("Year zero is not allowed", s); } if (era < 0) { dt.year++; // internal representation allows a year zero. } if (!tok.hasMoreElements()) { return badDate("Too short", s); } if (!"-".equals(tok.nextElement())) { return badDate("Wrong delimiter after year", s); } if (!tok.hasMoreElements()) { return badDate("Too short", s); } part = (String) tok.nextElement(); if (part.length() != 2) { return badDate("Month must be two digits", s); } value = DurationValue.simpleInteger(part); if (value < 0) { return badDate("Non-numeric month component", s); } dt.month = (byte) value; if (dt.month < 1 || dt.month > 12) { return badDate("Month is out of range", s); } if (!tok.hasMoreElements()) { return badDate("Too short", s); } if (!"-".equals(tok.nextElement())) { return badDate("Wrong delimiter after month", s); } if (!tok.hasMoreElements()) { return badDate("Too short", s); } part = (String) tok.nextElement(); if (part.length() != 2) { return badDate("Day must be two digits", s); } value = DurationValue.simpleInteger(part); if (value < 0) { return badDate("Non-numeric day component", s); } dt.day = (byte) value; if (dt.day < 1 || dt.day > 31) { return badDate("Day is out of range", s); } if (!tok.hasMoreElements()) { return badDate("Too short", s); } if (!"T".equals(tok.nextElement())) { return badDate("Wrong delimiter after day", s); } if (!tok.hasMoreElements()) { return badDate("Too short", s); } part = (String) tok.nextElement(); if (part.length() != 2) { return badDate("Hour must be two digits", s); } value = DurationValue.simpleInteger(part); if (value < 0) { return badDate("Non-numeric hour component", s); } dt.hour = (byte) value; if (dt.hour > 24) { return badDate("Hour is out of range", s); } if (!tok.hasMoreElements()) { return badDate("Too short", s); } if (!":".equals(tok.nextElement())) { return badDate("Wrong delimiter after hour", s); } if (!tok.hasMoreElements()) { return badDate("Too short", s); } part = (String) tok.nextElement(); if (part.length() != 2) { return badDate("Minute must be two digits", s); } value = DurationValue.simpleInteger(part); if (value < 0) { return badDate("Non-numeric minute component", s); } dt.minute = (byte) value; if (dt.minute > 59) { return badDate("Minute is out of range", s); } if (dt.hour == 24 && dt.minute != 0) { return badDate("If hour is 24, minute must be 00", s); } if (!tok.hasMoreElements()) { return badDate("Too short", s); } if (!":".equals(tok.nextElement())) { return badDate("Wrong delimiter after minute", s); } if (!tok.hasMoreElements()) { return badDate("Too short", s); } part = (String) tok.nextElement(); if (part.length() != 2) { return badDate("Second must be two digits", s); } value = DurationValue.simpleInteger(part); if (value < 0) { return badDate("Non-numeric second component", s); } dt.second = (byte) value; if (dt.second > 59) { return badDate("Second is out of range", s); } if (dt.hour == 24 && dt.second != 0) { return badDate("If hour is 24, second must be 00", s); } int tz = 0; int state = 0; while (tok.hasMoreElements()) { if (state == 9) { return badDate("Characters after the end", s); } String delim = (String) tok.nextElement(); if (".".equals(delim)) { if (state != 0) { return badDate("Decimal separator occurs twice", s); } if (!tok.hasMoreElements()) { return badDate("Decimal point must be followed by digits", s); } part = (String) tok.nextElement(); value = DurationValue.simpleInteger(part); if (value < 0) { return badDate("Non-numeric fractional seconds component", s); } double fractionalSeconds = Double.parseDouble('.' + part); dt.microsecond = (int) (Math.round(fractionalSeconds * 1000000)); if (dt.hour == 24 && dt.microsecond != 0) { return badDate("If hour is 24, fractional seconds must be 0", s); } state = 1; } else if ("Z".equals(delim)) { if (state > 1) { return badDate("Z cannot occur here", s); } tz = 0; state = 9; // we've finished dt.setTimezoneInMinutes(0); } else if ("+".equals(delim) || "-".equals(delim)) { if (state > 1) { return badDate(delim + " cannot occur here", s); } state = 2; if (!tok.hasMoreElements()) { return badDate("Missing timezone", s); } part = (String) tok.nextElement(); if (part.length() != 2) { return badDate("Timezone hour must be two digits", s); } value = DurationValue.simpleInteger(part); if (value < 0) { return badDate("Non-numeric timezone hour component", s); } tz = value; if (tz > 14) { return badDate("Timezone is out of range (-14:00 to +14:00)", s); } tz *= 60; if ("-".equals(delim)) { tz = -tz; } } else if (":".equals(delim)) { if (state != 2) { return badDate("Misplaced ':'", s); } state = 9; part = (String) tok.nextElement(); value = DurationValue.simpleInteger(part); if (value < 0) { return badDate("Non-numeric timezone minute component", s); } int tzminute = value; if (part.length() != 2) { return badDate("Timezone minute must be two digits", s); } if (tzminute > 59) { return badDate("Timezone minute is out of range", s); } if (tz < 0) { tzminute = -tzminute; } if (Math.abs(tz) == 14 * 60 && tzminute != 0) { return badDate("Timezone is out of range (-14:00 to +14:00)", s); } tz += tzminute; dt.setTimezoneInMinutes(tz); } else { return badDate("Timezone format is incorrect", s); } } if (state == 2 || state == 3) { return badDate("Timezone incomplete", s); } boolean midnight = false; if (dt.hour == 24) { dt.hour = 0; midnight = true; } // 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:00 on the next day if (midnight) { DateValue t = DateValue.tomorrow(dt.year, dt.month, dt.day); dt.year = t.getYear(); dt.month = t.getMonth(); dt.day = t.getDay(); } dt.typeLabel = BuiltInAtomicType.DATE_TIME; return dt; }
// Rules from XML Schema Part 2 public int compareTo(Object o) { if (o instanceof DateTimeComparable) { DateTimeValue dt0 = DateTimeValue.this; DateTimeValue dt1 = ((DateTimeComparable) o).asDateTimeValue(); if (dt0.hasTimezone()) { if (dt1.hasTimezone()) { dt0 = (DateTimeValue) dt0.adjustTimezone(0); dt1 = (DateTimeValue) dt1.adjustTimezone(0); return dt0.compareTo(dt1); } else { DateTimeValue dt1max = (DateTimeValue) dt1.adjustTimezone(14 * 60); if (dt0.compareTo(dt1max) < 0) { return -1; } DateTimeValue dt1min = (DateTimeValue) dt1.adjustTimezone(-14 * 60); if (dt0.compareTo(dt1min) > 0) { return +1; } return INDETERMINATE_ORDERING; } } else { if (dt1.hasTimezone()) { DateTimeValue dt0min = (DateTimeValue) dt0.adjustTimezone(-14 * 60); if (dt0min.compareTo(dt1) < 0) { return -1; } DateTimeValue dt0max = (DateTimeValue) dt0.adjustTimezone(14 * 60); if (dt0max.compareTo(dt1) > 0) { return +1; } return INDETERMINATE_ORDERING; } else { dt0 = (DateTimeValue) dt0.adjustTimezone(0); dt1 = (DateTimeValue) dt1.adjustTimezone(0); return dt0.compareTo(dt1); } } } else { return INDETERMINATE_ORDERING; } }