001: package net.sf.saxon.value;
002:
003: import net.sf.saxon.expr.XPathContext;
004: import net.sf.saxon.functions.Component;
005: import net.sf.saxon.om.FastStringBuffer;
006: import net.sf.saxon.trans.DynamicError;
007: import net.sf.saxon.trans.XPathException;
008: import net.sf.saxon.type.*;
009:
010: import java.math.BigDecimal;
011: import java.util.*;
012:
013: /**
014: * A value of type xs:time
015: */
016:
017: public final class TimeValue extends CalendarValue {
018:
019: private byte hour;
020: private byte minute;
021: private byte second;
022: private int microsecond;
023:
024: TimeValue(byte hour, byte minute, byte second, int microsecond,
025: int tz) {
026: this .hour = hour;
027: this .minute = minute;
028: this .second = second;
029: this .microsecond = microsecond;
030: setTimezoneInMinutes(tz);
031: }
032:
033: /**
034: * Constructor: create a time value given a Java calendar object
035: * @param calendar holds the date and time
036: * @param tz the timezone offset in minutes, or NO_TIMEZONE indicating that there is no timezone
037: */
038:
039: public TimeValue(GregorianCalendar calendar, int tz) {
040: hour = (byte) (calendar.get(Calendar.HOUR_OF_DAY));
041: minute = (byte) (calendar.get(Calendar.MINUTE));
042: second = (byte) (calendar.get(Calendar.SECOND));
043: microsecond = calendar.get(Calendar.MILLISECOND) * 1000;
044: setTimezoneInMinutes(tz);
045: }
046:
047: /**
048: * Constructor: create a dateTime value from a supplied string, in
049: * ISO 8601 format
050: */
051:
052: public TimeValue(CharSequence s) throws XPathException {
053: // input must have format hh:mm:ss[.fff*][([+|-]hh:mm | Z)]
054:
055: StringTokenizer tok = new StringTokenizer(trimWhitespace(s)
056: .toString(), "-:.+Z", true);
057: try {
058: if (!tok.hasMoreElements())
059: badTime("too short");
060: String part = (String) tok.nextElement();
061:
062: if (part.length() != 2)
063: badTime("hour must be two digits");
064: hour = (byte) Integer.parseInt(part);
065: if (hour > 24)
066: badTime("hour is out of range");
067: if (!tok.hasMoreElements())
068: badTime("too short");
069: if (!":".equals(tok.nextElement()))
070: badTime("wrong delimiter after hour");
071:
072: if (!tok.hasMoreElements())
073: badTime("too short");
074: part = (String) tok.nextElement();
075: if (part.length() != 2)
076: badTime("minute must be two digits");
077: minute = (byte) Integer.parseInt(part);
078: if (minute > 59)
079: badTime("minute is out of range");
080: if (hour == 24 && minute != 0)
081: badTime("If hour is 24, minute must be 00");
082: if (!tok.hasMoreElements())
083: badTime("too short");
084: if (!":".equals(tok.nextElement()))
085: badTime("wrong delimiter after minute");
086:
087: if (!tok.hasMoreElements())
088: badTime("too short");
089: part = (String) tok.nextElement();
090: if (part.length() != 2)
091: badTime("second must be two digits");
092: second = (byte) Integer.parseInt(part);
093: if (second > 59)
094: badTime("second is out of range");
095: if (hour == 24 && second != 0)
096: badTime("If hour is 24, second must be 00");
097:
098: int tz = 0;
099:
100: int state = 0;
101: while (tok.hasMoreElements()) {
102: if (state == 9) {
103: badTime("characters after the end");
104: }
105: String delim = (String) tok.nextElement();
106: if (".".equals(delim)) {
107: if (state != 0) {
108: badTime("decimal separator occurs twice");
109: }
110: part = (String) tok.nextElement();
111: double fractionalSeconds = Double
112: .parseDouble('.' + part);
113: microsecond = (int) (Math
114: .round(fractionalSeconds * 1000000));
115: if (hour == 24 && microsecond != 0) {
116: badTime("If hour is 24, fractional seconds must be 0");
117: }
118: state = 1;
119: } else if ("Z".equals(delim)) {
120: if (state > 1) {
121: badTime("Z cannot occur here");
122: }
123: tz = 0;
124: state = 9; // we've finished
125: setTimezoneInMinutes(0);
126: } else if ("+".equals(delim) || "-".equals(delim)) {
127: if (state > 1) {
128: badTime(delim + " cannot occur here");
129: }
130: state = 2;
131: if (!tok.hasMoreElements())
132: badTime("missing timezone");
133: part = (String) tok.nextElement();
134: if (part.length() != 2)
135: badTime("timezone hour must be two digits");
136: tz = Integer.parseInt(part) * 60;
137: if (tz > 14 * 60)
138: badTime("timezone hour is out of range");
139: //if (tz > 12 * 60) badTime("Because of Java limitations, Saxon currently limits the timezone to +/- 12 hours");
140: if ("-".equals(delim))
141: tz = -tz;
142: } else if (":".equals(delim)) {
143: if (state != 2) {
144: badTime("colon cannot occur here");
145: }
146: state = 9;
147: part = (String) tok.nextElement();
148: int tzminute = Integer.parseInt(part);
149: if (part.length() != 2)
150: badTime("timezone minute must be two digits");
151: if (tzminute > 59)
152: badTime("timezone minute is out of range");
153: if (tz < 0)
154: tzminute = -tzminute;
155: tz += tzminute;
156: setTimezoneInMinutes(tz);
157: } else {
158: badTime("timezone format is incorrect");
159: }
160: }
161:
162: if (state == 2 || state == 3) {
163: badTime("timezone incomplete");
164: }
165:
166: if (hour == 24) {
167: hour = 0;
168: }
169:
170: } catch (NumberFormatException err) {
171: badTime("non-numeric component");
172: }
173: }
174:
175: private void badTime(String msg) throws XPathException {
176: throw new DynamicError("Invalid time value (" + msg + ')');
177: }
178:
179: /**
180: * Convert to target data type
181: * @param requiredType an integer identifying the required atomic type
182: * @param context
183: * @return an AtomicValue, a value of the required type; or an ErrorValue
184: */
185:
186: public AtomicValue convertPrimitive(BuiltInAtomicType requiredType,
187: boolean validate, XPathContext context) {
188: switch (requiredType.getPrimitiveType()) {
189: case Type.TIME:
190: case Type.ANY_ATOMIC:
191: case Type.ITEM:
192: return this ;
193: case Type.STRING:
194: return new StringValue(getStringValueCS());
195: case Type.UNTYPED_ATOMIC:
196: return new UntypedAtomicValue(getStringValueCS());
197: default:
198: ValidationException err = new ValidationException(
199: "Cannot convert time to "
200: + requiredType.getDisplayName());
201: err.setErrorCode("XPTY0004");
202: err.setIsTypeError(true);
203: return new ValidationErrorValue(err);
204: }
205: }
206:
207: /**
208: * Convert to string
209: * @return ISO 8601 representation, in the localized timezone
210: * (the timezone held within the value).
211: */
212:
213: public CharSequence getStringValueCS() {
214:
215: FastStringBuffer sb = new FastStringBuffer(16);
216:
217: appendTwoDigits(sb, hour);
218: sb.append(':');
219: appendTwoDigits(sb, minute);
220: sb.append(':');
221: appendTwoDigits(sb, second);
222: if (microsecond != 0) {
223: sb.append('.');
224: int ms = microsecond;
225: int div = 100000;
226: while (ms > 0) {
227: int d = ms / div;
228: sb.append((char) (d + '0'));
229: ms = ms % div;
230: div /= 10;
231: }
232: }
233:
234: if (hasTimezone()) {
235: appendTimezone(sb);
236: }
237:
238: return sb;
239:
240: }
241:
242: /**
243: * Convert to a DateTime value. The date components represent a reference date, as defined
244: * in the spec for comparing times.
245: */
246:
247: public DateTimeValue toDateTime() {
248: return new DateTimeValue(1972, (byte) 12, (byte) 31, hour,
249: minute, second, microsecond, getTimezoneInMinutes());
250: }
251:
252: /**
253: * Get a Java Calendar object corresponding to this time, on a reference date
254: */
255:
256: public GregorianCalendar getCalendar() {
257: // create a calendar using the specified timezone
258: int tz = (hasTimezone() ? getTimezoneInMinutes() : 0);
259: TimeZone zone = new SimpleTimeZone(tz * 60000, "LLL");
260: GregorianCalendar calendar = new GregorianCalendar(zone);
261: calendar.setLenient(false);
262:
263: // use a reference date of 1972-12-31
264: int year = 1972;
265: int month = 11;
266: int day = 31;
267:
268: calendar.set(year, month, day, hour, minute, second);
269: calendar.set(Calendar.MILLISECOND, microsecond / 1000);
270: calendar.set(Calendar.ZONE_OFFSET, tz * 60000);
271: calendar.set(Calendar.DST_OFFSET, 0);
272:
273: calendar.getTime();
274: return calendar;
275: }
276:
277: /**
278: * Determine the data type of the expression
279: * @return Type.TIME_TYPE,
280: * @param th
281: */
282:
283: public ItemType getItemType(TypeHierarchy th) {
284: return Type.TIME_TYPE;
285: }
286:
287: /**
288: * Make a copy of this date, time, or dateTime value
289: */
290:
291: public CalendarValue copy() {
292: return new TimeValue(hour, minute, second, microsecond,
293: getTimezoneInMinutes());
294: }
295:
296: /**
297: * Return a new time with the same normalized value, but
298: * in a different timezone. This is called only for a TimeValue that has an explicit timezone
299: * @param timezone the new timezone offset, in minutes
300: * @return the time in the new timezone. This will be a new TimeValue unless no change
301: * was required to the original value
302: */
303:
304: public CalendarValue adjustTimezone(int timezone) {
305: DateTimeValue dt = (DateTimeValue) toDateTime().adjustTimezone(
306: timezone);
307: return new TimeValue(dt.getHour(), dt.getMinute(), dt
308: .getSecond(), dt.getMicrosecond(), dt
309: .getTimezoneInMinutes());
310: }
311:
312: /**
313: * Convert to Java object (for passing to external functions)
314: */
315:
316: public Object convertToJava(Class target, XPathContext context)
317: throws XPathException {
318: if (target.isAssignableFrom(TimeValue.class)) {
319: return this ;
320: } else if (target == String.class) {
321: return getStringValue();
322: } else if (target == Object.class) {
323: return getStringValue();
324: } else {
325: Object o = super .convertToJava(target, context);
326: if (o == null) {
327: throw new DynamicError("Conversion of time to "
328: + target.getName() + " is not supported");
329: }
330: return o;
331: }
332: }
333:
334: /**
335: * Get a component of the value. Returns null if the timezone component is
336: * requested and is not present.
337: */
338:
339: public AtomicValue getComponent(int component)
340: throws XPathException {
341: switch (component) {
342: case Component.HOURS:
343: return new IntegerValue(hour);
344: case Component.MINUTES:
345: return new IntegerValue(minute);
346: case Component.SECONDS:
347: BigDecimal d = new BigDecimal(microsecond);
348: d = d.divide(DecimalValue.ONE_MILLION, 6,
349: BigDecimal.ROUND_HALF_UP);
350: d = d.add(new BigDecimal(second));
351: return new DecimalValue(d);
352: case Component.TIMEZONE:
353: if (hasTimezone()) {
354: return SecondsDurationValue
355: .fromMilliseconds(getTimezoneInMinutes() * 60000);
356: } else {
357: return null;
358: }
359: default:
360: throw new IllegalArgumentException(
361: "Unknown component for time: " + component);
362: }
363: }
364:
365: /**
366: * Compare the value to another dateTime value
367: * @param other The other dateTime value
368: * @return negative value if this one is the earler, 0 if they are chronologically equal,
369: * positive value if this one is the later. For this purpose, dateTime values with an unknown
370: * timezone are considered to be UTC values (the Comparable interface requires
371: * a total ordering).
372: * @throws ClassCastException if the other value is not a DateTimeValue (the parameter
373: * is declared as Object to satisfy the Comparable interface)
374: */
375:
376: public int compareTo(Object other) {
377: if (!(other instanceof TimeValue)) {
378: throw new ClassCastException(
379: "Time values are not comparable to "
380: + other.getClass());
381: }
382:
383: TimeValue otherTime = (TimeValue) other;
384: if (getTimezoneInMinutes() == otherTime.getTimezoneInMinutes()) {
385: if (hour != otherTime.hour) {
386: return (hour - otherTime.hour);
387: } else if (minute != otherTime.minute) {
388: return (minute - otherTime.minute);
389: } else if (second != otherTime.second) {
390: return (second - otherTime.second);
391: } else if (microsecond != otherTime.microsecond) {
392: return (microsecond - otherTime.microsecond);
393: } else {
394: return 0;
395: }
396: } else {
397: return toDateTime().compareTo(otherTime.toDateTime());
398: }
399: }
400:
401: /**
402: * Compare the value to another dateTime value
403: * @param other The other dateTime value
404: * @return negative value if this one is the earler, 0 if they are chronologically equal,
405: * positive value if this one is the later. For this purpose, dateTime values with an unknown
406: * timezone are considered to be UTC values (the Comparable interface requires
407: * a total ordering).
408: * @throws ClassCastException if the other value is not a DateTimeValue (the parameter
409: * is declared as Object to satisfy the Comparable interface)
410: */
411:
412: public int compareTo(CalendarValue other, XPathContext context) {
413: if (!(other instanceof TimeValue)) {
414: throw new ClassCastException(
415: "Time values are not comparable to "
416: + other.getClass());
417: }
418: TimeValue otherTime = (TimeValue) other;
419: if (getTimezoneInMinutes() == otherTime.getTimezoneInMinutes()) {
420: // The values have the same time zone, or neither has a timezone
421: return compareTo(other);
422: } else {
423: return toDateTime().compareTo(otherTime.toDateTime(),
424: context);
425: }
426: }
427:
428: public boolean equals(Object other) {
429: return compareTo(other) == 0;
430: }
431:
432: public int hashCode() {
433: return toDateTime().hashCode();
434: }
435:
436: /**
437: * Add a duration to a dateTime
438: * @param duration the duration to be added (may be negative)
439: * @return the new date
440: * @throws net.sf.saxon.trans.XPathException if the duration is an xs:duration, as distinct from
441: * a subclass thereof
442: */
443:
444: public CalendarValue add(DurationValue duration)
445: throws XPathException {
446: if (duration instanceof SecondsDurationValue) {
447: DateTimeValue dt = (DateTimeValue) toDateTime().add(
448: duration);
449: return new TimeValue(dt.getHour(), dt.getMinute(), dt
450: .getSecond(), dt.getMicrosecond(),
451: getTimezoneInMinutes());
452: } else {
453: DynamicError err = new DynamicError(
454: "Time+Duration arithmetic is supported only for xdt:dayTimeDuration");
455: err.setIsTypeError(true);
456: throw err;
457: }
458: }
459:
460: /**
461: * Determine the difference between two points in time, as a duration
462: * @param other the other point in time
463: * @param context
464: * @return the duration as an xdt:dayTimeDuration
465: * @throws net.sf.saxon.trans.XPathException for example if one value is a date and the other is a time
466: */
467:
468: public SecondsDurationValue subtract(CalendarValue other,
469: XPathContext context) throws XPathException {
470: if (!(other instanceof TimeValue)) {
471: DynamicError err = new DynamicError(
472: "First operand of '-' is a time, but the second is not");
473: err.setIsTypeError(true);
474: throw err;
475: }
476: return super .subtract(other, context);
477: }
478:
479: }
480:
481: //
482: // The contents of this file are subject to the Mozilla Public License Version 1.0 (the "License");
483: // you may not use this file except in compliance with the License. You may obtain a copy of the
484: // License at http://www.mozilla.org/MPL/
485: //
486: // Software distributed under the License is distributed on an "AS IS" basis,
487: // WITHOUT WARRANTY OF ANY KIND, either express or implied.
488: // See the License for the specific language governing rights and limitations under the License.
489: //
490: // The Original Code is: all this file.
491: //
492: // The Initial Developer of the Original Code is Michael H. Kay
493: //
494: // Portions created by (your name) are Copyright (C) (your legal entity). All Rights Reserved.
495: //
496: // Contributor(s): none.
497: //
|