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.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;
/**
* A value of type xs:time
*/
public final class TimeValue extends CalendarValue implements Comparable {
private int hour;
private int minute;
private int second;
private int microsecond;
private TimeValue() {
typeLabel = BuiltInAtomicType.TIME;
}
/**
* Construct a time value given the hour, minute, second, and microsecond components.
* This constructor performs no validation.
*
* @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 TimeValue(int hour, int minute, int second, int microsecond, int tz) {
this.hour = hour;
this.minute = minute;
this.second = second;
this.microsecond = microsecond;
setTimezoneInMinutes(tz);
typeLabel = BuiltInAtomicType.TIME;
}
/**
* Static factory method: create a time value from a supplied string, in
* ISO 8601 format
*
* @param s the time in the lexical format hh:mm:ss[.ffffff] followed optionally by
* timezone in the form [+-]hh:mm or Z
* @return either a TimeValue corresponding to the xs:time, or a ValidationFailure
* if the supplied value was invalid
*/
private static RegExp timePattern =
RegExp.compile("([0-9][0-9]):([0-9][0-9]):([0-9][0-9])(\\.[0-9]*)?([-+Z].*)?");
public static ConversionResult makeTimeValue(CharSequence s) {
String str = s.toString();
MatchResult match = timePattern.exec(str);
if (match == null) {
return badTime("wrong format", str);
}
TimeValue dt = new TimeValue();
dt.hour = DurationValue.simpleInteger(match.getGroup(1));
dt.minute = DurationValue.simpleInteger(match.getGroup(2));
dt.second = DurationValue.simpleInteger(match.getGroup(3));
String frac = match.getGroup(4);
if (frac != null && frac.length() > 0) {
double fractionalSeconds = Double.parseDouble(frac);
dt.microsecond = (int)(Math.round(fractionalSeconds * 1000000));
}
String tz = match.getGroup(5);
int tzmin = parseTimezone(tz);
if (tzmin == BAD_TIMEZONE) {
return badTime("Invalid timezone", str);
}
dt.setTimezoneInMinutes(tzmin);
// Adjust midnight to 00:00 on the following day
if (dt.hour == 24) {
if (dt.minute != 0 || dt.second != 0 || dt.microsecond != 0) {
return badTime("after midnight", str);
} else {
dt.hour = 0;
}
}
return dt;
}
private static ValidationFailure badTime(String msg, CharSequence value) {
ValidationFailure err = new ValidationFailure(
"Invalid time " + Err.wrap(value, Err.VALUE) + " (" + msg + ")");
err.setErrorCode("FORG0001");
return err;
}
/**
* 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.TIME;
}
/**
* Get the hour component, 0-23
*
* @return the hour
*/
public int getHour() {
return hour;
}
/**
* Get the minute component, 0-59
*
* @return the minute
*/
public int getMinute() {
return minute;
}
/**
* Get the second component, 0-59
*
* @return the second
*/
public int getSecond() {
return second;
}
/**
* Get the microsecond component, 0-999999
*
* @return the microseconds
*/
public int getMicrosecond() {
return microsecond;
}
/**
* 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_TIME:
case StandardNames.XS_ANY_ATOMIC_TYPE:
return this;
case StandardNames.XS_STRING:
return new StringValue(getStringValueCS());
case StandardNames.XS_UNTYPED_ATOMIC:
return new UntypedAtomicValue(getStringValueCS());
default:
ValidationFailure err = new ValidationFailure("Cannot convert time to " +
requiredType.getDisplayName());
err.setErrorCode("XPTY0004");
return err;
}
}
/**
* Convert to string
*
* @return ISO 8601 representation, in the localized timezone
* (the timezone held within the value).
*/
public CharSequence getPrimitiveStringValue() {
FastStringBuffer sb = new FastStringBuffer(FastStringBuffer.TINY);
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:time it is the
* time adjusted to UTC
*
* @return the canonical lexical representation if defined in XML Schema
*/
public CharSequence getCanonicalLexicalRepresentation() {
if (hasTimezone() && getTimezoneInMinutes() != 0) {
return adjustTimezone(0).getStringValueCS();
} else {
return getStringValueCS();
}
}
/**
* Convert to a DateTime value. The date components represent a reference date, as defined
* in the spec for comparing times.
*/
public DateTimeValue toDateTime() {
return new DateTimeValue(1972, (byte)12, (byte)31, hour, minute, second, microsecond, getTimezoneInMinutes());
}
/**
* Make a copy of this time value,
* but with a different type label
*
*/
public AtomicValue copy() {
TimeValue v = new TimeValue(hour, minute, second, microsecond, getTimezoneInMinutes());
v.typeLabel = typeLabel;
return v;
}
/**
* Return a new time with the same normalized value, but
* in a different timezone. This is called only for a TimeValue that has an explicit timezone
*
* @param timezone the new timezone offset, in minutes
* @return the time in the new timezone. This will be a new TimeValue unless no change
* was required to the original value
*/
public CalendarValue adjustTimezone(int timezone) {
DateTimeValue dt = (DateTimeValue)toDateTime().adjustTimezone(timezone);
return new TimeValue(dt.getHour(), dt.getMinute(), dt.getSecond(),
dt.getMicrosecond(), dt.getTimezoneInMinutes());
}
/**
* Convert to Java object (for passing to external functions)
*/
// public Object convertAtomicToJava(Class target, XPathContext context) throws XPathException {
// if (target.isAssignableFrom(TimeValue.class)) {
// return this;
// } else if (target == String.class) {
// return getStringValue();
// } else if (target == Object.class) {
// return getStringValue();
// } else {
// Object o = super.convertSequenceToJava(target, context);
// if (o == null) {
// throw new XPathException("Conversion of time to " + target.getName() +
// " is not supported");
// }
// return o;
// }
// }
//
/**
* 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.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:
return IntegerValue.makeIntegerValue(microsecond);
case Component.TIMEZONE:
if (hasTimezone()) {
return DayTimeDurationValue.fromMilliseconds(60000L * getTimezoneInMinutes());
} else {
return null;
}
default:
throw new IllegalArgumentException("Unknown component for time: " + component);
}
}
/**
* Get a Comparable value that implements the XPath ordering comparison semantics for this value.
* Returns null if the value is not comparable according to XPath rules. The default implementation
* returns null. This is overridden for types that allow ordered comparisons in XPath: numeric, boolean,
* string, date, time, dateTime, yearMonthDuration, dayTimeDuration, and anyURI.
* @param ordered true if an ordered comparison is required
* @param collator collation to be used for strings
* @param context XPath dynamic evaluation context
*/
// public Object getXPathComparable(boolean ordered, StringCollator collator, XPathContext context) {
// return this;
// }
/**
* Compare the value to another dateTime value
*
* @param other The other dateTime value
* @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 UTC values (the Comparable interface requires
* a total ordering).
* @throws ClassCastException if the other value is not a TimeValue (the parameter
* is declared as Object to satisfy the Comparable interface)
*/
public int compareTo(Object other) {
TimeValue otherTime = (TimeValue)other;
if (getTimezoneInMinutes() == otherTime.getTimezoneInMinutes()) {
if (hour != otherTime.hour) {
return IntegerValue.signum(hour - otherTime.hour);
} else if (minute != otherTime.minute) {
return IntegerValue.signum(minute - otherTime.minute);
} else if (second != otherTime.second) {
return IntegerValue.signum(second - otherTime.second);
} else if (microsecond != otherTime.microsecond) {
return IntegerValue.signum(microsecond - otherTime.microsecond);
} else {
return 0;
}
} else {
return toDateTime().compareTo(otherTime.toDateTime());
}
}
/**
* Compare the value to another dateTime value
*
* @param other The other dateTime value
* @param context the 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 UTC values (the Comparable interface requires
* a total ordering).
* @throws ClassCastException if the other value is not a DateTimeValue (the parameter
* is declared as Object to satisfy the Comparable interface)
* @throws NoDynamicContextException if the implicit timezone is required and is not available
* (because the function is called at compile time)
*/
public int compareTo(CalendarValue other, XPathContext context) throws NoDynamicContextException {
if (!(other instanceof TimeValue)) {
throw new ClassCastException("Time values are not comparable to " + other.getClass());
}
TimeValue otherTime = (TimeValue)other;
if (getTimezoneInMinutes() == otherTime.getTimezoneInMinutes()) {
// The values have the same time zone, or neither has a timezone
return compareTo(other);
} else {
return toDateTime().compareTo(otherTime.toDateTime(), context);
}
}
/**
* 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 required and is not available
*/
public ComparisonKey getComparisonKey(XPathContext context) throws NoDynamicContextException {
return new ComparisonKey(StandardNames.XS_TIME, toDateTime().normalize(context));
}
public boolean equals(Object other) {
return other instanceof TimeValue && compareTo(other) == 0;
}
public int hashCode() {
return DateTimeValue.hashCode(
1951, (byte)10, (byte)11, hour, minute, second, microsecond, getTimezoneInMinutes());
}
/**
* 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) {
DateTimeValue dt = (DateTimeValue)toDateTime().add(duration);
return new TimeValue(dt.getHour(), dt.getMinute(), dt.getSecond(),
dt.getMicrosecond(), getTimezoneInMinutes());
} else {
XPathException err = new XPathException("Time+Duration arithmetic is supported only for xs:dayTimeDuration");
err.setErrorCode("XPTY0004");
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 XPath dynamic evaluation context
* @return the duration as an xs:dayTimeDuration
* @throws 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 TimeValue)) {
XPathException err = new XPathException("First operand of '-' is a time, but the second is not");
err.setIsTypeError(true);
throw err;
}
return super.subtract(other, 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.