/*
* eXist Open Source Native XML Database
* Copyright (C) 2001-06 Wolfgang M. Meier
* wolfgang@exist-db.org
* http://exist.sourceforge.net
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*
* $Id$
*/
package org.exist.xquery.value;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.text.Collator;
import javax.xml.datatype.DatatypeConstants;
import javax.xml.datatype.Duration;
import org.exist.xquery.Constants;
import org.exist.xquery.XPathException;
/**
* @author <a href="mailto:piotr@ideanest.com">Piotr Kaminski</a>
*/
public class DurationValue extends ComputableValue {
public final static int YEAR = 0;
public final static int MONTH = 1;
public final static int DAY = 2;
public final static int HOUR = 3;
public final static int MINUTE = 4;
protected final Duration duration;
private Duration canonicalDuration;
protected static final BigInteger
TWELVE = BigInteger.valueOf(12),
TWENTY_FOUR = BigInteger.valueOf(24),
SIXTY = BigInteger.valueOf(60);
protected static final BigDecimal
SIXTY_DECIMAL = BigDecimal.valueOf(60),
ZERO_DECIMAL = BigDecimal.valueOf(0); // TODO: replace with BigDecimal.ZERO in JDK 1.5
protected static final Duration CANONICAL_ZERO_DURATION =
TimeUtils.getInstance().newDuration(true, null, null, null, null, null, ZERO_DECIMAL);
/**
* Create a new duration value of the most specific type allowed by the fields set in the given
* duration object. If no fields are set, return a xs:dayTimeDuration.
*
* @param duration the duration to wrap
* @return a new instance of the most specific subclass of <code>DurationValue</code>
*/
public static DurationValue wrap(Duration duration) {
try {
return new DayTimeDurationValue(duration);
} catch (XPathException e) {
try {
return new YearMonthDurationValue(duration);
} catch (XPathException e2) {
return new DurationValue(duration);
}
}
}
public DurationValue(Duration duration) {
this.duration = duration;
}
public DurationValue(String str) throws XPathException {
try {
this.duration = TimeUtils.getInstance().newDuration(StringValue.trimWhitespace(str));
} catch (IllegalArgumentException e) {
throw new XPathException("FORG0001: cannot construct " + Type.getTypeName(this.getItemType()) +
" from \"" + str + "\"");
}
}
public Duration getCanonicalDuration() {
canonicalize();
return canonicalDuration;
}
public int getType() {
return Type.DURATION;
}
protected DurationValue createSameKind(Duration d) throws XPathException {
return new DurationValue(d);
}
public DurationValue negate() throws XPathException {
return createSameKind(duration.negate());
}
public String getStringValue() {
canonicalize();
return canonicalDuration.toString();
}
private static BigInteger nullIfZero(BigInteger x) {
if (BigInteger.ZERO.compareTo(x) == Constants.EQUAL) x = null;
return x;
}
private static BigInteger zeroIfNull(BigInteger x) {
if (x == null) x = BigInteger.ZERO;
return x;
}
private static BigDecimal nullIfZero(BigDecimal x) {
if (ZERO_DECIMAL.compareTo(x) == Constants.EQUAL) x = null;
return x;
}
private static BigDecimal zeroIfNull(BigDecimal x) {
if (x == null) x = ZERO_DECIMAL;
return x;
}
private void canonicalize() {
if (canonicalDuration != null)
return;
BigInteger years, months, days, hours, minutes;
BigDecimal seconds;
BigInteger[] r;
r = monthsValue().divideAndRemainder(TWELVE);
years = nullIfZero(r[0]);
months = nullIfZero(r[1]);
// TODO: replace following segment with this for JDK 1.5
// BigDecimal[] rd = secondsValue().divideAndRemainder(SIXTY_DECIMAL);
// seconds = nullIfZero(rd[1]);
// r = rd[0].toBigInteger().divideAndRemainder(SIXTY);
// segment to be replaced:
BigDecimal secondsValue = secondsValue();
BigDecimal m = secondsValue.divide(SIXTY_DECIMAL, 0, BigDecimal.ROUND_DOWN);
seconds = nullIfZero(secondsValue.subtract(SIXTY_DECIMAL.multiply(m)));
r = m.toBigInteger().divideAndRemainder(SIXTY);
minutes = nullIfZero(r[1]);
r = r[0].divideAndRemainder(TWENTY_FOUR);
hours = nullIfZero(r[1]);
days = nullIfZero(r[0]);
if (years == null && months == null && days == null && hours == null && minutes == null && seconds == null) {
canonicalDuration = canonicalZeroDuration();
} else {
canonicalDuration = TimeUtils.getInstance().newDuration(
duration.getSign() >= 0,
years, months, days, hours, minutes, seconds);
}
}
protected BigDecimal secondsValue() {
return
new BigDecimal(
zeroIfNull((BigInteger) duration.getField(DatatypeConstants.DAYS))
.multiply(TWENTY_FOUR)
.add(zeroIfNull((BigInteger) duration.getField(DatatypeConstants.HOURS)))
.multiply(SIXTY)
.add(zeroIfNull((BigInteger) duration.getField(DatatypeConstants.MINUTES)))
.multiply(SIXTY)
).add(zeroIfNull((BigDecimal) duration.getField(DatatypeConstants.SECONDS)));
}
protected BigDecimal secondsValueSigned() {
BigDecimal x = secondsValue();
if (duration.getSign() < 0) x = x.negate();
return x;
}
protected BigInteger monthsValue() {
return
zeroIfNull((BigInteger) duration.getField(DatatypeConstants.YEARS))
.multiply(TWELVE)
.add(zeroIfNull((BigInteger) duration.getField(DatatypeConstants.MONTHS)));
}
protected BigInteger monthsValueSigned() {
BigInteger x = monthsValue();
if (duration.getSign() < 0) x = x.negate();
return x;
}
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;
default:
throw new IllegalArgumentException("Invalid argument to method getPart");
}
return r * duration.getSign();
}
public double getSeconds() {
Number n = duration.getField(DatatypeConstants.SECONDS);
return n == null ? 0 : n.doubleValue() * duration.getSign();
}
public AtomicValue convertTo(int requiredType) throws XPathException {
canonicalize();
switch(requiredType) {
case Type.ITEM:
case Type.ATOMIC:
case Type.DURATION:
return new DurationValue(canonicalDuration);
case Type.YEAR_MONTH_DURATION:
if (canonicalDuration.getField(DatatypeConstants.YEARS) != null ||
canonicalDuration.getField(DatatypeConstants.MONTHS) != null)
return new YearMonthDurationValue(TimeUtils.getInstance().newDurationYearMonth(
canonicalDuration.getSign() >= 0,
(BigInteger) canonicalDuration.getField(DatatypeConstants.YEARS),
(BigInteger) canonicalDuration.getField(DatatypeConstants.MONTHS)));
else
return new YearMonthDurationValue(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(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(DayTimeDurationValue.CANONICAL_ZERO_DURATION);
case Type.STRING:
canonicalize();
return new StringValue(getStringValue());
case Type.UNTYPED_ATOMIC :
canonicalize();
return new UntypedAtomicValue(getStringValue());
default:
throw new XPathException(
"Type error: cannot cast ' + Type.getTypeName(getType()) 'to "
+ Type.getTypeName(requiredType));
}
}
public boolean compareTo(Collator collator, int operator, AtomicValue other) throws XPathException {
switch (operator) {
case Constants.EQ :
{
if (!(DurationValue.class.isAssignableFrom(other.getClass())))
throw new XPathException("FORG0006: 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 Constants.NEQ :
{
if (!(DurationValue.class.isAssignableFrom(other.getClass())))
throw new XPathException("FORG0006: 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 Constants.LT :
case Constants.LTEQ :
case Constants.GT :
case Constants.GTEQ :
throw new XPathException("XPTY0004: " + Type.getTypeName(other.getType()) + " type can not be ordered");
default :
throw new IllegalArgumentException("Unknown comparison operator");
}
}
public int compareTo(Collator collator, AtomicValue other) throws XPathException {
if (!(DurationValue.class.isAssignableFrom(other.getClass())))
throw new XPathException("FORG0006: invalid operand type: " + Type.getTypeName(other.getType()));
//TODO : what to do with the collator ?
return duration.compare(((DurationValue)other).duration);
}
public AtomicValue max(Collator collator, AtomicValue other) throws XPathException {
throw new XPathException("FORG0001: invalid operation on " + Type.getTypeName(this.getType()));
}
public AtomicValue min(Collator collator, AtomicValue other) throws XPathException {
throw new XPathException("FORG0001: invalid operation on " + Type.getTypeName(this.getType()));
}
public ComputableValue plus(ComputableValue other) throws XPathException {
throw new XPathException("FORG0001: invalid operation on " + Type.getTypeName(this.getType()));
}
public ComputableValue minus(ComputableValue other) throws XPathException {
throw new XPathException("FORG0001: invalid operation on " + Type.getTypeName(this.getType()));
}
public ComputableValue mult(ComputableValue other) throws XPathException {
throw new XPathException("FORG0001: invalid operation on " + Type.getTypeName(this.getType()));
}
public ComputableValue div(ComputableValue other) throws XPathException {
throw new XPathException("FORG0001: invalid operation on " + Type.getTypeName(this.getType()));
}
public int conversionPreference(Class target) {
if (target.isAssignableFrom(getClass())) return 0;
if (target.isAssignableFrom(Duration.class)) return 1;
return Integer.MAX_VALUE;
}
public Object toJavaObject(Class target) throws XPathException {
if (target.isAssignableFrom(getClass())) return this;
if (target.isAssignableFrom(Duration.class)) return duration;
throw new XPathException("cannot convert value of type " + Type.getTypeName(getType()) + " to Java object of type " + target.getName());
}
public boolean effectiveBooleanValue() throws XPathException {
throw new XPathException("FORG0006: value of type " + Type.getTypeName(getType()) +
" has no boolean value.");
}
public static boolean areReallyEqual(Duration duration1, Duration duration2) {
boolean secondsEqual = zeroIfNull((BigDecimal)duration1.getField(DatatypeConstants.SECONDS)).compareTo(
zeroIfNull((BigDecimal)duration2.getField(DatatypeConstants.SECONDS))) == Constants.EQUAL;
return secondsEqual &&
duration1.getMinutes() == duration2.getMinutes() &&
duration1.getHours() == duration2.getHours() &&
duration1.getDays() == duration2.getDays() &&
duration1.getMonths() == duration2.getMonths() &&
duration1.getYears() == duration2.getYears();
}
}