Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
219 changes: 99 additions & 120 deletions exist-core/src/main/java/org/exist/xquery/value/DurationValue.java
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,17 @@ public class DurationValue extends ComputableValue {
// (xdt:yearMonthDuration) at the long range; values outside raise FODT0002.
protected static final BigDecimal MAX_DAY_TIME_SECONDS = new BigDecimal(Long.MAX_VALUE);
protected static final BigInteger MAX_YEAR_MONTH_MONTHS = BigInteger.valueOf(Long.MAX_VALUE);

// Cap on the day-time magnitude of a duration when used in xs:date /
// xs:dateTime / xs:time arithmetic. XMLGregorianCalendar.add() iterates
// proportionally to the day count, so very large day-time durations make
// a single add() call take seconds to hours (issue #5045). The threshold
// below caps worst-case latency at well under one second on modern hardware
// while permitting +/- 1 million Gregorian years of arithmetic, which
// exceeds any realistic use of date/time arithmetic.
private static final long DATE_ARITH_DAYS_LIMIT = 365_242_500L; // 1,000,000 Gregorian years
protected static final BigDecimal MAX_DATE_ARITH_SECONDS =
BigDecimal.valueOf(DATE_ARITH_DAYS_LIMIT).multiply(BigDecimal.valueOf(86_400L));
protected static final Duration CANONICAL_ZERO_DURATION =
TimeUtils.getInstance().newDuration(true, null, null, null, null, null, ZERO_DECIMAL);
protected final Duration duration;
Expand Down Expand Up @@ -124,32 +135,20 @@ public static DurationValue wrap(Duration duration) {
}
}

private static BigInteger nullIfZero(BigInteger x) {
if (BigInteger.ZERO.compareTo(x) == Constants.EQUAL) {
x = null;
}
return x;
private static BigInteger nullIfZero(final BigInteger x) {
return BigInteger.ZERO.compareTo(x) == Constants.EQUAL ? null : x;
}

private static BigInteger zeroIfNull(BigInteger x) {
if (x == null) {
x = BigInteger.ZERO;
}
return x;
private static BigInteger zeroIfNull(final BigInteger x) {
return x == null ? BigInteger.ZERO : x;
}

private static BigDecimal nullIfZero(BigDecimal x) {
if (ZERO_DECIMAL.compareTo(x) == Constants.EQUAL) {
x = null;
}
return x;
private static BigDecimal nullIfZero(final BigDecimal x) {
return ZERO_DECIMAL.compareTo(x) == Constants.EQUAL ? null : x;
}

private static BigDecimal zeroIfNull(BigDecimal x) {
if (x == null) {
x = ZERO_DECIMAL;
}
return x;
private static BigDecimal zeroIfNull(final BigDecimal x) {
return x == null ? ZERO_DECIMAL : x;
}

public static boolean areReallyEqual(Duration duration1, Duration duration2) {
Expand Down Expand Up @@ -194,7 +193,11 @@ private void canonicalize() {
return;
}

BigInteger years, months, days, hours, minutes;
BigInteger years;
BigInteger months;
BigInteger days;
BigInteger hours;
BigInteger minutes;
BigDecimal seconds;
BigInteger[] r;

Expand Down Expand Up @@ -276,122 +279,98 @@ protected void checkYearMonthOverflow(final BigInteger months) throws XPathExcep
}
}

protected void checkDateArithMagnitude(final BigDecimal seconds) throws XPathException {
if (seconds.abs().compareTo(MAX_DATE_ARITH_SECONDS) > 0) {
throw new XPathException(getExpression(), ErrorCodes.FODT0001,
"Overflow/underflow in date/time operation: duration "
+ this + " is too large for date/time arithmetic");
}
}

protected Duration canonicalZeroDuration() {
return CANONICAL_ZERO_DURATION;
}

public int getPart(int part) {
int r;
switch (part) {
case YEAR:
r = duration.getYears();
break;
case MONTH:
r = duration.getMonths();
break;
case DAY:
r = duration.getDays();
break;
case HOUR:
r = duration.getHours();
break;
case MINUTE:
r = duration.getMinutes();
break;
case SIGN:
return duration.getSign();
default:
throw new IllegalArgumentException("Invalid argument to method getPart");
}
return r * duration.getSign();
public int getPart(final int part) {
return switch (part) {
case YEAR -> duration.getYears() * duration.getSign();
case MONTH -> duration.getMonths() * duration.getSign();
case DAY -> duration.getDays() * duration.getSign();
case HOUR -> duration.getHours() * duration.getSign();
case MINUTE -> duration.getMinutes() * duration.getSign();
case SIGN -> duration.getSign();
default -> throw new IllegalArgumentException("Invalid argument to method getPart");
};
}

public double getSeconds() {
final Number n = duration.getField(DatatypeConstants.SECONDS);
return n == null ? 0 : n.doubleValue() * duration.getSign();
}

public AtomicValue convertTo(int requiredType) throws XPathException {
public AtomicValue convertTo(final int requiredType) throws XPathException {
canonicalize();
switch (requiredType) {
case Type.ITEM:
case Type.ANY_ATOMIC_TYPE:
case Type.DURATION:
return new DurationValue(getExpression(), canonicalDuration);
case Type.YEAR_MONTH_DURATION:
if (canonicalDuration.getField(DatatypeConstants.YEARS) != null ||
canonicalDuration.getField(DatatypeConstants.MONTHS) != null) {
return new YearMonthDurationValue(getExpression(), TimeUtils.getInstance().newDurationYearMonth(
canonicalDuration.getSign() >= 0,
(BigInteger) canonicalDuration.getField(DatatypeConstants.YEARS),
(BigInteger) canonicalDuration.getField(DatatypeConstants.MONTHS)));
} else {
return new YearMonthDurationValue(getExpression(), YearMonthDurationValue.CANONICAL_ZERO_DURATION);
}
case Type.DAY_TIME_DURATION:
if (canonicalDuration.isSet(DatatypeConstants.DAYS) ||
canonicalDuration.isSet(DatatypeConstants.HOURS) ||
canonicalDuration.isSet(DatatypeConstants.MINUTES) ||
canonicalDuration.isSet(DatatypeConstants.SECONDS)) {
return new DayTimeDurationValue(getExpression(), TimeUtils.getInstance().newDuration(
canonicalDuration.getSign() >= 0,
null,
null,
(BigInteger) canonicalDuration.getField(DatatypeConstants.DAYS),
(BigInteger) canonicalDuration.getField(DatatypeConstants.HOURS),
(BigInteger) canonicalDuration.getField(DatatypeConstants.MINUTES),
(BigDecimal) canonicalDuration.getField(DatatypeConstants.SECONDS)));
} else {
return new DayTimeDurationValue(getExpression(), DayTimeDurationValue.CANONICAL_ZERO_DURATION);
}
case Type.STRING:
canonicalize();
return new StringValue(getExpression(), getStringValue());
case Type.UNTYPED_ATOMIC:
canonicalize();
return new UntypedAtomicValue(getExpression(), getStringValue());
default:
throw new XPathException(getExpression(), ErrorCodes.FORG0001,
"Type error: cannot cast ' + Type.getTypeName(getType()) 'to "
+ Type.getTypeName(requiredType));
return switch (requiredType) {
case Type.ITEM, Type.ANY_ATOMIC_TYPE, Type.DURATION ->
new DurationValue(getExpression(), canonicalDuration);
case Type.YEAR_MONTH_DURATION -> toYearMonthDurationValue();
case Type.DAY_TIME_DURATION -> toDayTimeDurationValue();
case Type.STRING -> new StringValue(getExpression(), getStringValue());
case Type.UNTYPED_ATOMIC -> new UntypedAtomicValue(getExpression(), getStringValue());
default -> throw new XPathException(getExpression(), ErrorCodes.FORG0001,
"Type error: cannot cast '" + Type.getTypeName(getType()) + "' to "
+ Type.getTypeName(requiredType));
};
}

private YearMonthDurationValue toYearMonthDurationValue() throws XPathException {
if (canonicalDuration.getField(DatatypeConstants.YEARS) != null
|| canonicalDuration.getField(DatatypeConstants.MONTHS) != null) {
return new YearMonthDurationValue(getExpression(), TimeUtils.getInstance().newDurationYearMonth(
canonicalDuration.getSign() >= 0,
(BigInteger) canonicalDuration.getField(DatatypeConstants.YEARS),
(BigInteger) canonicalDuration.getField(DatatypeConstants.MONTHS)));
}
return new YearMonthDurationValue(getExpression(), YearMonthDurationValue.CANONICAL_ZERO_DURATION);
}

private DayTimeDurationValue toDayTimeDurationValue() throws XPathException {
if (canonicalDuration.isSet(DatatypeConstants.DAYS)
|| canonicalDuration.isSet(DatatypeConstants.HOURS)
|| canonicalDuration.isSet(DatatypeConstants.MINUTES)
|| canonicalDuration.isSet(DatatypeConstants.SECONDS)) {
return new DayTimeDurationValue(getExpression(), TimeUtils.getInstance().newDuration(
canonicalDuration.getSign() >= 0,
null,
null,
(BigInteger) canonicalDuration.getField(DatatypeConstants.DAYS),
(BigInteger) canonicalDuration.getField(DatatypeConstants.HOURS),
(BigInteger) canonicalDuration.getField(DatatypeConstants.MINUTES),
(BigDecimal) canonicalDuration.getField(DatatypeConstants.SECONDS)));
}
return new DayTimeDurationValue(getExpression(), DayTimeDurationValue.CANONICAL_ZERO_DURATION);
}

@Override
public boolean compareTo(Collator collator, Comparison operator, AtomicValue other) throws XPathException {
switch (operator) {
case EQ: {
if (!(DurationValue.class.isAssignableFrom(other.getClass()))) {
throw new XPathException(getExpression(), ErrorCodes.XPTY0004, "invalid operand type: " + Type.getTypeName(other.getType()));
}
//TODO : upgrade so that P365D is *not* equal to P1Y
boolean r = duration.equals(((DurationValue) other).duration);
//confirm strict equality to work around the JDK standard behaviour
if (r) {
r = r & areReallyEqual(getCanonicalDuration(), ((DurationValue) other).getCanonicalDuration());
}
return r;
}
case NEQ: {
if (!(DurationValue.class.isAssignableFrom(other.getClass()))) {
throw new XPathException(getExpression(), ErrorCodes.XPTY0004, "invalid operand type: " + Type.getTypeName(other.getType()));
}
//TODO : upgrade so that P365D is *not* equal to P1Y
boolean r = duration.equals(((DurationValue) other).duration);
//confirm strict equality to work around the JDK standard behaviour
if (r) {
r = r & areReallyEqual(getCanonicalDuration(), ((DurationValue) other).getCanonicalDuration());
}
return !r;
}
case LT:
case LTEQ:
case GT:
case GTEQ:
throw new XPathException(getExpression(), ErrorCodes.XPTY0004, Type.getTypeName(other.getType()) + " type can not be ordered");
default:
throw new IllegalArgumentException("Unknown comparison operator");
public boolean compareTo(final Collator collator, final Comparison operator, final AtomicValue other) throws XPathException {
return switch (operator) {
case EQ -> durationsAreEqual(other);
case NEQ -> !durationsAreEqual(other);
case LT, LTEQ, GT, GTEQ -> throw new XPathException(getExpression(), ErrorCodes.XPTY0004,
Type.getTypeName(other.getType()) + " type can not be ordered");
default -> throw new IllegalArgumentException("Unknown comparison operator");
};
}

private boolean durationsAreEqual(final AtomicValue other) throws XPathException {
if (!DurationValue.class.isAssignableFrom(other.getClass())) {
throw new XPathException(getExpression(), ErrorCodes.XPTY0004,
"invalid operand type: " + Type.getTypeName(other.getType()));
}
//TODO : upgrade so that P365D is *not* equal to P1Y
//confirm strict equality to work around the JDK standard behaviour
return duration.equals(((DurationValue) other).duration)
&& areReallyEqual(getCanonicalDuration(), ((DurationValue) other).getCanonicalDuration());
}

public int compareTo(Collator collator, AtomicValue other) throws XPathException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,44 +47,60 @@ abstract class OrderedDurationValue extends DurationValue {
}

@Override
public boolean compareTo(Collator collator, Comparison operator, AtomicValue other) throws XPathException {
public boolean compareTo(final Collator collator, final Comparison operator, final AtomicValue other)
throws XPathException {
if (other.isEmpty()) {
return false;
}
final Boolean mixedSubtypeResult = compareMixedSubtypes(operator, other);
if (mixedSubtypeResult != null) {
return mixedSubtypeResult;
}
validateOrderingComparable(operator, other);
return resultMatchesOperator(compareTo(collator, other), operator);
}

// Mixed duration subtypes (e.g., xs:yearMonthDuration vs xs:dayTimeDuration):
// equality comparisons return false (they can never be equal);
// ordering operators (lt/gt/le/ge) are not defined and raise XPTY0004.
if (Type.subTypeOf(other.getType(), Type.DURATION)
&& getType() != other.getType()
&& getType() != Type.DURATION
&& other.getType() != Type.DURATION) {
if (operator == Comparison.EQ) {
return false;
}
if (operator == Comparison.NEQ) {
return true;
}
throw new XPathException(getExpression(), ErrorCodes.XPTY0004,
/**
* Mixed duration subtypes (e.g., xs:yearMonthDuration vs xs:dayTimeDuration):
* equality comparisons return false (they can never be equal);
* ordering operators (lt/gt/le/ge) are not defined and raise XPTY0004.
* Returns null when the subtypes are not mixed and the caller should
* continue with normal comparison.
*/
private Boolean compareMixedSubtypes(final Comparison operator, final AtomicValue other) throws XPathException {
if (!Type.subTypeOf(other.getType(), Type.DURATION)
|| getType() == other.getType()
|| getType() == Type.DURATION
|| other.getType() == Type.DURATION) {
return null;
}
return switch (operator) {
case EQ -> Boolean.FALSE;
case NEQ -> Boolean.TRUE;
default -> throw new XPathException(getExpression(), ErrorCodes.XPTY0004,
"cannot compare " + Type.getTypeName(getType()) + " to "
+ Type.getTypeName(other.getType()));
}
};
}

// Ordering operators (LT/GT/LTEQ/GTEQ) are not defined for unordered xs:duration
if (operator != Comparison.EQ && operator != Comparison.NEQ) {
if (getType() == Type.DURATION) {
throw new XPathException(getExpression(), ErrorCodes.XPTY0004,
"cannot compare unordered " + Type.getTypeName(getType()) + " to "
+ Type.getTypeName(other.getType()));
}
if (other.getType() == Type.DURATION) {
throw new XPathException(getExpression(), ErrorCodes.XPTY0004,
"cannot compare " + Type.getTypeName(getType()) + " to unordered "
+ Type.getTypeName(other.getType()));
}
/** Ordering operators (LT/GT/LTEQ/GTEQ) are not defined for unordered xs:duration. */
private void validateOrderingComparable(final Comparison operator, final AtomicValue other) throws XPathException {
if (operator == Comparison.EQ || operator == Comparison.NEQ) {
return;
}
if (getType() == Type.DURATION) {
throw new XPathException(getExpression(), ErrorCodes.XPTY0004,
"cannot compare unordered " + Type.getTypeName(getType()) + " to "
+ Type.getTypeName(other.getType()));
}
if (other.getType() == Type.DURATION) {
throw new XPathException(getExpression(), ErrorCodes.XPTY0004,
"cannot compare " + Type.getTypeName(getType()) + " to unordered "
+ Type.getTypeName(other.getType()));
}
}

final int r = compareTo(collator, other);
private boolean resultMatchesOperator(final int r, final Comparison operator) throws XPathException {
return switch (operator) {
case EQ -> r == DatatypeConstants.EQUAL;
case NEQ -> r != DatatypeConstants.EQUAL;
Expand Down Expand Up @@ -116,7 +132,8 @@ public int compareTo(Collator collator, AtomicValue other) throws XPathException
Constants.INFERIOR : Constants.SUPERIOR;
}
if (r == DatatypeConstants.INDETERMINATE) {
throw new RuntimeException("indeterminate order between totally ordered duration values " + this + " and " + other);
throw new IllegalStateException(
"indeterminate order between totally ordered duration values " + this + " and " + other);
}
return r;
}
Expand Down Expand Up @@ -173,6 +190,7 @@ public ComputableValue plus(ComputableValue other) throws XPathException {

private ComputableValue addDurationToDate(AbstractDateTimeValue date) throws XPathException {
date.checkYearOverflow(date.calendar);
checkDateArithMagnitude(secondsValueSigned());
final XMLGregorianCalendar gc = (XMLGregorianCalendar) date.calendar.clone();
gc.add(duration);
// For xs:time the year/month/day are FIELD_UNDEFINED; the legacy
Expand Down
Loading
Loading