001: package net.sf.saxon.value;
002:
003: import net.sf.saxon.Controller;
004: import net.sf.saxon.expr.XPathContext;
005: import net.sf.saxon.functions.Component;
006: import net.sf.saxon.om.FastStringBuffer;
007: import net.sf.saxon.trans.DynamicError;
008: import net.sf.saxon.trans.IndependentContext;
009: import net.sf.saxon.trans.XPathException;
010: import net.sf.saxon.type.*;
011:
012: import java.math.BigDecimal;
013: import java.math.BigInteger;
014: import java.util.*;
015:
016: /**
017: * A value of type DateTime
018: */
019:
020: public final class DateTimeValue extends CalendarValue {
021:
022: private int year; // the year as written, +1 for BC years
023: private byte month; // the month as written, range 1-12
024: private byte day; // the day as written, range 1-31
025: private byte hour; // the hour as written (except for midnight), range 0-23
026: private byte minute; // the minutes as written, range 0-59
027: private byte second; // the seconds as written, range 0-59 (no leap seconds)
028: private int microsecond;
029:
030: /**
031: * Get the dateTime value representing the nominal
032: * date/time of this transformation run. Two calls within the same
033: * query or transformation will always return the same answer.
034: */
035:
036: public static DateTimeValue getCurrentDateTime(XPathContext context) {
037: Controller c;
038: if (context == null || (c = context.getController()) == null) {
039: // non-XSLT/XQuery environment
040: // We also take this path when evaluating compile-time expressions that require an implicit timezone.
041: return new DateTimeValue(new GregorianCalendar(), true);
042: } else {
043: return c.getCurrentDateTime();
044: }
045: }
046:
047: /**
048: * Constructor: create a dateTime value given a Java calendar object
049: * @param calendar holds the date and time
050: * @param tzSpecified indicates whether the timezone is specified
051: */
052:
053: public DateTimeValue(Calendar calendar, boolean tzSpecified) {
054: int era = calendar.get(GregorianCalendar.ERA);
055: year = calendar.get(Calendar.YEAR);
056: if (era == GregorianCalendar.BC) {
057: year = -year;
058: }
059: month = (byte) (calendar.get(Calendar.MONTH) + 1);
060: day = (byte) (calendar.get(Calendar.DATE));
061: hour = (byte) (calendar.get(Calendar.HOUR_OF_DAY));
062: minute = (byte) (calendar.get(Calendar.MINUTE));
063: second = (byte) (calendar.get(Calendar.SECOND));
064: microsecond = calendar.get(Calendar.MILLISECOND) * 1000;
065: if (tzSpecified) {
066: int tz = (calendar.get(Calendar.ZONE_OFFSET) + calendar
067: .get(Calendar.DST_OFFSET)) / 60000;
068: setTimezoneInMinutes(tz);
069: }
070: }
071:
072: /**
073: * Constructor: create a dateTime value given a date and a time.
074: * @param date the date
075: * @param time the time
076: * @throws net.sf.saxon.trans.XPathException if the timezones are both present and inconsistent
077: */
078:
079: public DateTimeValue(DateValue date, TimeValue time)
080: throws XPathException {
081: SecondsDurationValue tz1 = (SecondsDurationValue) date
082: .getComponent(Component.TIMEZONE);
083: SecondsDurationValue tz2 = (SecondsDurationValue) time
084: .getComponent(Component.TIMEZONE);
085: boolean zoneSpecified = (tz1 != null || tz2 != null);
086: if (tz1 != null && tz2 != null && !tz1.equals(tz2)) {
087: DynamicError err = new DynamicError(
088: "Supplied date and time are in different timezones");
089: err.setErrorCode("FORG0008");
090: throw err;
091: }
092:
093: year = (int) ((IntegerValue) date.getComponent(Component.YEAR))
094: .longValue();
095: month = (byte) ((IntegerValue) date
096: .getComponent(Component.MONTH)).longValue();
097: day = (byte) ((IntegerValue) date.getComponent(Component.DAY))
098: .longValue();
099: hour = (byte) ((IntegerValue) time
100: .getComponent(Component.HOURS)).longValue();
101: minute = (byte) ((IntegerValue) time
102: .getComponent(Component.MINUTES)).longValue();
103: final BigDecimal secs = ((DecimalValue) time
104: .getComponent(Component.SECONDS)).getValue();
105: second = (byte) secs.intValue();
106: microsecond = secs.multiply(BigDecimal.valueOf(1000000))
107: .intValue() % 1000000;
108: if (zoneSpecified) {
109: if (tz1 == null) {
110: tz1 = tz2;
111: }
112: setTimezoneInMinutes((int) (tz1.getLengthInMicroseconds() / 60000000));
113: }
114: }
115:
116: /**
117: * Constructor: create a dateTime value from a supplied string, in
118: * ISO 8601 format
119: */
120:
121: public DateTimeValue(CharSequence s) throws XPathException {
122: // input must have format yyyy-mm-ddThh:mm:ss[.fff*][([+|-]hh:mm | Z)]
123: StringTokenizer tok = new StringTokenizer(trimWhitespace(s)
124: .toString(), "-:.+TZ", true);
125: try {
126: if (!tok.hasMoreElements())
127: badDate("too short");
128: String part = (String) tok.nextElement();
129: int era = +1;
130: if ("+".equals(part)) {
131: part = (String) tok.nextElement();
132: } else if ("-".equals(part)) {
133: era = -1;
134: part = (String) tok.nextElement();
135: }
136: year = Integer.parseInt(part) * era;
137: if (part.length() < 4)
138: badDate("Year is less than four digits");
139: if (year == 0)
140: badDate("Year zero is not allowed");
141: if (era < 0) {
142: year++; // internal representation allows a year zero.
143: }
144: if (!tok.hasMoreElements())
145: badDate("Too short");
146: if (!"-".equals(tok.nextElement()))
147: badDate("Wrong delimiter after year");
148:
149: if (!tok.hasMoreElements())
150: badDate("Too short");
151: part = (String) tok.nextElement();
152: if (part.length() != 2)
153: badDate("Month must be two digits");
154: month = (byte) Integer.parseInt(part);
155: if (month < 1 || month > 12)
156: badDate("Month is out of range");
157:
158: if (!tok.hasMoreElements())
159: badDate("Too short");
160: if (!"-".equals(tok.nextElement()))
161: badDate("Wrong delimiter after month");
162: if (!tok.hasMoreElements())
163: badDate("Too short");
164: part = (String) tok.nextElement();
165: if (part.length() != 2)
166: badDate("Day must be two digits");
167: day = (byte) Integer.parseInt(part);
168: if (day < 1 || day > 31)
169: badDate("Day is out of range");
170:
171: if (!tok.hasMoreElements())
172: badDate("Too short");
173: if (!"T".equals(tok.nextElement()))
174: badDate("Wrong delimiter after day");
175:
176: if (!tok.hasMoreElements())
177: badDate("Too short");
178: part = (String) tok.nextElement();
179: if (part.length() != 2)
180: badDate("Hour must be two digits");
181: hour = (byte) Integer.parseInt(part);
182: if (hour > 24)
183: badDate("Hour is out of range");
184:
185: if (!tok.hasMoreElements())
186: badDate("Too short");
187: if (!":".equals(tok.nextElement()))
188: badDate("Wrong delimiter after hour");
189:
190: if (!tok.hasMoreElements())
191: badDate("Too short");
192: part = (String) tok.nextElement();
193: if (part.length() != 2)
194: badDate("Minute must be two digits");
195: minute = (byte) Integer.parseInt(part);
196: if (minute > 59)
197: badDate("Minute is out of range");
198: if (hour == 24 && minute != 0)
199: badDate("If hour is 24, minute must be 00");
200: if (!tok.hasMoreElements())
201: badDate("Too short");
202: if (!":".equals(tok.nextElement()))
203: badDate("Wrong delimiter after minute");
204:
205: if (!tok.hasMoreElements())
206: badDate("Too short");
207: part = (String) tok.nextElement();
208: if (part.length() != 2)
209: badDate("Second must be two digits");
210: second = (byte) Integer.parseInt(part);
211:
212: if (second > 59)
213: badDate("Second is out of range");
214: if (hour == 24 && second != 0)
215: badDate("If hour is 24, second must be 00");
216:
217: int tz = 0;
218:
219: int state = 0;
220: while (tok.hasMoreElements()) {
221: if (state == 9) {
222: badDate("Characters after the end");
223: }
224: String delim = (String) tok.nextElement();
225: if (".".equals(delim)) {
226: if (state != 0) {
227: badDate("Decimal separator occurs twice");
228: }
229: part = (String) tok.nextElement();
230: double fractionalSeconds = Double
231: .parseDouble('.' + part);
232: microsecond = (int) (Math
233: .round(fractionalSeconds * 1000000));
234: if (hour == 24 && microsecond != 0) {
235: badDate("If hour is 24, fractional seconds must be 0");
236: }
237: state = 1;
238: } else if ("Z".equals(delim)) {
239: if (state > 1) {
240: badDate("Z cannot occur here");
241: }
242: tz = 0;
243: state = 9; // we've finished
244: setTimezoneInMinutes(0);
245: } else if ("+".equals(delim) || "-".equals(delim)) {
246: if (state > 1) {
247: badDate(delim + " cannot occur here");
248: }
249: state = 2;
250: if (!tok.hasMoreElements())
251: badDate("Missing timezone");
252: part = (String) tok.nextElement();
253: if (part.length() != 2)
254: badDate("Timezone hour must be two digits");
255:
256: tz = Integer.parseInt(part);
257: if (tz > 14)
258: badDate("Timezone is out of range (-14:00 to +14:00)");
259: tz *= 60;
260:
261: //if (tz > 12*60) badDate("Because of Java limitations, Saxon currently limits the timezone to +/- 12 hours");
262: if ("-".equals(delim))
263: tz = -tz;
264:
265: } else if (":".equals(delim)) {
266: if (state != 2) {
267: badDate("Misplaced ':'");
268: }
269: state = 9;
270: part = (String) tok.nextElement();
271: int tzminute = Integer.parseInt(part);
272: if (part.length() != 2)
273: badDate("Timezone minute must be two digits");
274: if (tzminute > 59)
275: badDate("Timezone minute is out of range");
276: if (tz < 0)
277: tzminute = -tzminute;
278: if (Math.abs(tz) == 14 * 60 && tzminute != 0) {
279: badDate("Timezone is out of range (-14:00 to +14:00)");
280: }
281: tz += tzminute;
282: setTimezoneInMinutes(tz);
283: } else {
284: badDate("Timezone format is incorrect");
285: }
286: }
287:
288: if (state == 2 || state == 3) {
289: badDate("Timezone incomplete");
290: }
291:
292: boolean midnight = false;
293: if (hour == 24) {
294: hour = 0;
295: midnight = true;
296: }
297:
298: // Check that this is a valid calendar date
299: if (!DateValue.isValidDate(year, month, day)) {
300: badDate("Non-existent date");
301: }
302:
303: // Adjust midnight to 00:00:00 on the next day
304: if (midnight) {
305: DateValue t = DateValue.tomorrow(year, month, day);
306: year = t.getYear();
307: month = t.getMonth();
308: day = t.getDay();
309: }
310:
311: } catch (NumberFormatException err) {
312: badDate("Non-numeric component");
313: }
314: }
315:
316: private void badDate(String msg) throws XPathException {
317: DynamicError err = new DynamicError("Invalid dateTime value. "
318: + msg);
319: err.setErrorCode("FORG0001");
320: throw err;
321: }
322:
323: /**
324: * Constructor: construct a DateTimeValue from its components.
325: * This constructor performs no validation.
326: */
327:
328: public DateTimeValue(int year, byte month, byte day, byte hour,
329: byte minute, byte second, int microsecond, int tz) {
330: this .year = year;
331: this .month = month;
332: this .day = day;
333: this .hour = hour;
334: this .minute = minute;
335: this .second = second;
336: this .microsecond = microsecond;
337: setTimezoneInMinutes(tz);
338: }
339:
340: /**
341: * Get the year component
342: */
343:
344: public int getYear() {
345: return year;
346: }
347:
348: /**
349: * Get the month component
350: */
351:
352: public byte getMonth() {
353: return month;
354: }
355:
356: /**
357: * Get the day component
358: */
359:
360: public byte getDay() {
361: return day;
362: }
363:
364: /**
365: * Get the hour component
366: */
367:
368: public byte getHour() {
369: return hour;
370: }
371:
372: /**
373: * Get the minute component
374: */
375:
376: public byte getMinute() {
377: return minute;
378: }
379:
380: /**
381: * Get the second component
382: */
383:
384: public byte getSecond() {
385: return second;
386: }
387:
388: /**
389: * Get the year component
390: */
391:
392: public int getMicrosecond() {
393: return microsecond;
394: }
395:
396: /**
397: * Convert the value to a DateTime, retaining all the components that are actually present, and
398: * substituting conventional values for components that are missing
399: */
400:
401: public DateTimeValue toDateTime() {
402: return this ;
403: }
404:
405: /**
406: * Normalize the date and time to be in timezone Z.
407: * @param cc used to supply the implicit timezone, used when the value has
408: * no explicit timezone
409: * @return in general, a new DateTimeValue in timezone Z, representing the same instant in time.
410: * Returns the original DateTimeValue if this is already in timezone Z.
411: */
412:
413: public DateTimeValue normalize(XPathContext cc) {
414: if (hasTimezone()) {
415: return (DateTimeValue) adjustTimezone(0);
416: } else {
417: DateTimeValue dt = (DateTimeValue) copy();
418: dt.setTimezoneInMinutes(cc.getConfiguration()
419: .getImplicitTimezone());
420: return (DateTimeValue) dt.adjustTimezone(0);
421: }
422: }
423:
424: /**
425: * Get the Julian instant: a decimal value whose integer part is the Julian day number
426: * multiplied by the number of seconds per day,
427: * and whose fractional part is the fraction of the second.
428: * This method operates on the local time, ignoring the timezone. The caller should call normalize()
429: * before calling this method to get a normalized time.
430: */
431:
432: public BigDecimal toJulianInstant() {
433: int julianDay = DateValue.getJulianDayNumber(year, month, day);
434: long julianSecond = julianDay * (24L * 60L * 60L);
435: julianSecond += (((hour * 60L + minute) * 60L) + second);
436: BigDecimal j = new BigDecimal(julianSecond);
437: if (microsecond == 0) {
438: return j;
439: } else {
440: return j.add(new BigDecimal(microsecond).divide(
441: DecimalValue.ONE_MILLION, 6,
442: BigDecimal.ROUND_HALF_EVEN));
443: }
444: }
445:
446: /**
447: * Get the DateTimeValue corresponding to a given Julian instant
448: */
449:
450: public static DateTimeValue fromJulianInstant(BigDecimal instant) {
451: BigInteger julianSecond = instant.toBigInteger();
452: BigDecimal microseconds = instant.subtract(
453: new BigDecimal(julianSecond)).multiply(
454: DecimalValue.ONE_MILLION);
455: long js = julianSecond.longValue();
456: long jd = js / (24L * 60L * 60L);
457: DateValue date = DateValue.dateFromJulianDayNumber((int) jd);
458: js = js % (24L * 60L * 60L);
459: byte hour = (byte) (js / (60L * 60L));
460: js = js % (60L * 60L);
461: byte minute = (byte) (js / (60L));
462: js = js % (60L);
463: return new DateTimeValue(date.getYear(), date.getMonth(), date
464: .getDay(), hour, minute, (byte) js, microseconds
465: .intValue(), 0);
466: }
467:
468: /**
469: * Get a Calendar object representing the value of this DateTime. This will respect the timezone
470: * if there is one, or be in GMT otherwise.
471: */
472:
473: public GregorianCalendar getCalendar() {
474: int tz = (hasTimezone() ? getTimezoneInMinutes() * 60000 : 0);
475: TimeZone zone = new SimpleTimeZone(tz, "LLL");
476: GregorianCalendar calendar = new GregorianCalendar(zone);
477: calendar.setLenient(false);
478: calendar.set(Math.abs(year), month - 1, day, hour, minute,
479: second);
480: if (year < 0) {
481: calendar.set(Calendar.ERA, GregorianCalendar.BC);
482: }
483: calendar.set(Calendar.MILLISECOND, microsecond / 1000); // loses precision unavoidably
484: calendar.set(Calendar.ZONE_OFFSET, tz);
485: calendar.set(Calendar.DST_OFFSET, 0);
486: return calendar;
487: }
488:
489: /**
490: * Convert to target data type
491: * @param requiredType an integer identifying the required atomic type
492: * @param context
493: * @return an AtomicValue, a value of the required type; or an ErrorValue
494: */
495:
496: public AtomicValue convertPrimitive(BuiltInAtomicType requiredType,
497: boolean validate, XPathContext context) {
498: switch (requiredType.getPrimitiveType()) {
499: case Type.DATE_TIME:
500: case Type.ANY_ATOMIC:
501: case Type.ITEM:
502: return this ;
503:
504: case Type.DATE:
505: return new DateValue(year, month, day,
506: getTimezoneInMinutes());
507:
508: case Type.TIME:
509: return new TimeValue(hour, minute, second, microsecond,
510: getTimezoneInMinutes());
511:
512: case Type.G_YEAR:
513: return new GYearValue(year, getTimezoneInMinutes());
514:
515: case Type.G_YEAR_MONTH:
516: return new GYearMonthValue(year, month,
517: getTimezoneInMinutes());
518:
519: case Type.G_MONTH:
520: return new GMonthValue(month, getTimezoneInMinutes());
521:
522: case Type.G_MONTH_DAY:
523: return new GMonthDayValue(month, day,
524: getTimezoneInMinutes());
525:
526: case Type.G_DAY:
527: return new GDayValue(day, getTimezoneInMinutes());
528:
529: case Type.STRING:
530: return new StringValue(getStringValueCS());
531:
532: case Type.UNTYPED_ATOMIC:
533: return new UntypedAtomicValue(getStringValueCS());
534:
535: default:
536: ValidationException err = new ValidationException(
537: "Cannot convert dateTime to "
538: + requiredType.getDisplayName());
539: err.setErrorCode("XPTY0004");
540: err.setIsTypeError(true);
541: return new ValidationErrorValue(err);
542: }
543: }
544:
545: /**
546: * Convert to string
547: * @return ISO 8601 representation. The value returned is the localized representation,
548: * that is it uses the timezone contained within the value itself.
549: */
550:
551: public CharSequence getStringValueCS() {
552:
553: FastStringBuffer sb = new FastStringBuffer(30);
554: int yr = year;
555: if (year <= 0) {
556: sb.append('-');
557: yr = -yr + 1; // no year zero in lexical space
558: }
559: appendString(sb, yr, (yr > 9999 ? (yr + "").length() : 4));
560: sb.append('-');
561: appendTwoDigits(sb, month);
562: sb.append('-');
563: appendTwoDigits(sb, day);
564: sb.append('T');
565: appendTwoDigits(sb, hour);
566: sb.append(':');
567: appendTwoDigits(sb, minute);
568: sb.append(':');
569: appendTwoDigits(sb, second);
570: if (microsecond != 0) {
571: sb.append('.');
572: int ms = microsecond;
573: int div = 100000;
574: while (ms > 0) {
575: int d = ms / div;
576: sb.append((char) (d + '0'));
577: ms = ms % div;
578: div /= 10;
579: }
580: }
581:
582: if (hasTimezone()) {
583: appendTimezone(sb);
584: }
585:
586: return sb;
587:
588: }
589:
590: /**
591: * Determine the data type of the exprssion
592: * @return Type.DATE_TIME,
593: * @param th
594: */
595:
596: public ItemType getItemType(TypeHierarchy th) {
597: return Type.DATE_TIME_TYPE;
598: }
599:
600: /**
601: * Make a copy of this date, time, or dateTime value
602: */
603:
604: public CalendarValue copy() {
605: return new DateTimeValue(year, month, day, hour, minute,
606: second, microsecond, getTimezoneInMinutes());
607: }
608:
609: /**
610: * Return a new dateTime with the same normalized value, but
611: * in a different timezone. This is called only for a DateTimeValue that has an explicit timezone
612: * @param timezone the new timezone offset, in minutes
613: * @return the date/time in the new timezone. This will be a new DateTimeValue unless no change
614: * was required to the original value
615: */
616:
617: public CalendarValue adjustTimezone(int timezone) {
618: if (!hasTimezone()) {
619: CalendarValue in = copy();
620: in.setTimezoneInMinutes(timezone);
621: return in;
622: }
623: int oldtz = getTimezoneInMinutes();
624: if (oldtz == timezone) {
625: return this ;
626: }
627: int tz = timezone - oldtz;
628: int h = hour;
629: int mi = minute;
630: mi += tz;
631: if (mi < 0 || mi > 59) {
632: h += Math.floor(mi / 60.0);
633: mi = (mi + 60 * 24) % 60;
634: }
635:
636: if (h >= 0 && h < 24) {
637: return new DateTimeValue(year, month, day, (byte) h,
638: (byte) mi, second, microsecond, timezone);
639: }
640:
641: // Following code is designed to handle the corner case of adjusting from -14:00 to +14:00 or
642: // vice versa, which can cause a change of two days in the date
643: DateTimeValue dt = this ;
644: while (h < 0) {
645: h += 24;
646: DateValue t = DateValue.yesterday(dt.getYear(), dt
647: .getMonth(), dt.getDay());
648: dt = new DateTimeValue(t.getYear(), t.getMonth(), t
649: .getDay(), (byte) h, (byte) mi, second,
650: microsecond, timezone);
651: }
652: while (h > 23) {
653: h -= 24;
654: DateValue t = DateValue.tomorrow(year, month, day);
655: return new DateTimeValue(t.getYear(), t.getMonth(), t
656: .getDay(), (byte) h, (byte) mi, second,
657: microsecond, timezone);
658: }
659: return dt;
660: }
661:
662: /**
663: * Add a duration to a dateTime
664: * @param duration the duration to be added (may be negative)
665: * @return the new date
666: * @throws net.sf.saxon.trans.XPathException if the duration is an xs:duration, as distinct from
667: * a subclass thereof
668: */
669:
670: public CalendarValue add(DurationValue duration)
671: throws XPathException {
672: if (duration instanceof SecondsDurationValue) {
673: long microseconds = ((SecondsDurationValue) duration)
674: .getLengthInMicroseconds();
675: BigDecimal seconds = new BigDecimal(microseconds).divide(
676: DecimalValue.ONE_MILLION, 6,
677: BigDecimal.ROUND_HALF_EVEN);
678: BigDecimal julian = toJulianInstant();
679: julian = julian.add(seconds);
680: DateTimeValue dt = fromJulianInstant(julian);
681: dt.setTimezoneInMinutes(getTimezoneInMinutes());
682: return dt;
683: } else if (duration instanceof MonthDurationValue) {
684: int months = ((MonthDurationValue) duration)
685: .getLengthInMonths();
686: int m = (month - 1) + months;
687: int y = year + m / 12;
688: m = m % 12;
689: if (m < 0) {
690: m += 12;
691: y -= 1;
692: }
693: m++;
694: int d = day;
695: while (!DateValue.isValidDate(y, m, d)) {
696: d -= 1;
697: }
698: return new DateTimeValue(y, (byte) m, (byte) d, hour,
699: minute, second, microsecond, getTimezoneInMinutes());
700: } else {
701: DynamicError err = new DynamicError(
702: "DateTime arithmetic is not supported on xs:duration, only on its subtypes");
703: err.setIsTypeError(true);
704: throw err;
705: }
706: }
707:
708: /**
709: * Determine the difference between two points in time, as a duration
710: * @param other the other point in time
711: * @param context
712: * @return the duration as an xdt:dayTimeDuration
713: * @throws net.sf.saxon.trans.XPathException for example if one value is a date and the other is a time
714: */
715:
716: public SecondsDurationValue subtract(CalendarValue other,
717: XPathContext context) throws XPathException {
718: if (!(other instanceof DateTimeValue)) {
719: DynamicError err = new DynamicError(
720: "First operand of '-' is a dateTime, but the second is not");
721: err.setIsTypeError(true);
722: throw err;
723: }
724: return super .subtract(other, context);
725: }
726:
727: /**
728: * Convert to Java object (for passing to external functions)
729: */
730:
731: public Object convertToJava(Class target, XPathContext context)
732: throws XPathException {
733: if (target.isAssignableFrom(Date.class)) {
734: return getCalendar().getTime();
735: } else if (target.isAssignableFrom(GregorianCalendar.class)) {
736: return getCalendar();
737: } else if (target.isAssignableFrom(DateTimeValue.class)) {
738: return this ;
739: } else if (target == String.class
740: || target == CharSequence.class) {
741: return getStringValue();
742: } else if (target == Object.class) {
743: return getStringValue();
744: } else {
745: Object o = super .convertToJava(target, context);
746: if (o == null) {
747: throw new DynamicError("Conversion of dateTime to "
748: + target.getName() + " is not supported");
749: }
750: return o;
751: }
752: }
753:
754: /**
755: * Get a component of the value. Returns null if the timezone component is
756: * requested and is not present.
757: */
758:
759: public AtomicValue getComponent(int component)
760: throws XPathException {
761: switch (component) {
762: case Component.YEAR:
763: return new IntegerValue((year > 0 ? year : year - 1));
764: case Component.MONTH:
765: return new IntegerValue(month);
766: case Component.DAY:
767: return new IntegerValue(day);
768: case Component.HOURS:
769: return new IntegerValue(hour);
770: case Component.MINUTES:
771: return new IntegerValue(minute);
772: case Component.SECONDS:
773: BigDecimal d = new BigDecimal(microsecond);
774: d = d.divide(DecimalValue.ONE_MILLION, 6,
775: BigDecimal.ROUND_HALF_UP);
776: d = d.add(new BigDecimal(second));
777: return new DecimalValue(d);
778: case Component.MICROSECONDS:
779: // internal use only
780: return new IntegerValue(microsecond);
781: case Component.TIMEZONE:
782: if (hasTimezone()) {
783: return SecondsDurationValue
784: .fromMilliseconds(getTimezoneInMinutes() * 60 * 1000);
785: } else {
786: return null;
787: }
788: default:
789: throw new IllegalArgumentException(
790: "Unknown component for dateTime: " + component);
791: }
792: }
793:
794: /**
795: * Compare the value to another dateTime value.
796: * <p>
797: * This method is not used for XPath comparisons because it does not have access to the implicitTimezone
798: * from the dynamic context. It is available for schema comparisons, although it does not currently
799: * implement the XML Schema semantics for timezone comparison (which involve partial ordering)
800: * @param other The other dateTime value
801: * @return negative value if this one is the earler, 0 if they are chronologically equal,
802: * positive value if this one is the later. For this purpose, dateTime values with an unknown
803: * timezone are considered to be values in the implicit timezone (the Comparable interface requires
804: * a total ordering).
805: * @throws ClassCastException if the other value is not a DateTimeValue (the parameter
806: * is declared as Object to satisfy the Comparable interface)
807: */
808:
809: public int compareTo(Object other) {
810: // TODO: implement the XML Schema comparison semantics (and remove the gross inefficiency)
811: if (!(other instanceof DateTimeValue)) {
812: throw new ClassCastException(
813: "DateTime values are not comparable to "
814: + other.getClass());
815: }
816: return compareTo((DateTimeValue) other,
817: new IndependentContext().makeEarlyEvaluationContext());
818: }
819:
820: /**
821: * Compare the value to another dateTime value, following the XPath comparison semantics
822: * @param other The other dateTime value
823: * @param cc A ConversionContext used to supply the implicit timezone
824: * @return negative value if this one is the earler, 0 if they are chronologically equal,
825: * positive value if this one is the later. For this purpose, dateTime values with an unknown
826: * timezone are considered to be values in the implicit timezone (the Comparable interface requires
827: * a total ordering).
828: * @throws ClassCastException if the other value is not a DateTimeValue (the parameter
829: * is declared as Object to satisfy the Comparable interface)
830: */
831:
832: public int compareTo(CalendarValue other, XPathContext cc) {
833: if (!(other instanceof DateTimeValue)) {
834: throw new ClassCastException(
835: "DateTime values are not comparable to "
836: + other.getClass());
837: }
838: DateTimeValue v2 = (DateTimeValue) other;
839: if (getTimezoneInMinutes() == v2.getTimezoneInMinutes()) {
840: // both values are in the same timezone (explicitly or implicitly)
841: if (year != v2.year) {
842: return year - v2.year;
843: }
844: if (month != v2.month) {
845: return month - v2.month;
846: }
847: if (day != v2.day) {
848: return day - v2.day;
849: }
850: if (hour != v2.hour) {
851: return hour - v2.hour;
852: }
853: if (minute != v2.minute) {
854: return minute - v2.minute;
855: }
856: if (second != v2.second) {
857: return second - v2.second;
858: }
859: if (microsecond != v2.microsecond) {
860: return microsecond - v2.microsecond;
861: }
862: return 0;
863: }
864: return normalize(cc).compareTo(v2.normalize(cc), cc);
865: }
866:
867: public boolean equals(Object other) {
868: return compareTo(other) == 0;
869: }
870:
871: public int hashCode() {
872: if (hasTimezone()) {
873: return getCalendar().getTime().hashCode();
874: } else {
875: GregorianCalendar cal = new GregorianCalendar();
876: int tz = (cal.get(Calendar.ZONE_OFFSET) + cal
877: .get(Calendar.DST_OFFSET)) / 60000;
878: DateTimeValue v1 = (DateTimeValue) adjustTimezone(tz);
879: return v1.getCalendar().getTime().hashCode();
880: }
881: }
882:
883: }
884:
885: //
886: // The contents of this file are subject to the Mozilla Public License Version 1.0 (the "License");
887: // you may not use this file except in compliance with the License. You may obtain a copy of the
888: // License at http://www.mozilla.org/MPL/
889: //
890: // Software distributed under the License is distributed on an "AS IS" basis,
891: // WITHOUT WARRANTY OF ANY KIND, either express or implied.
892: // See the License for the specific language governing rights and limitations under the License.
893: //
894: // The Original Code is: all this file.
895: //
896: // The Initial Developer of the Original Code is Michael H. Kay
897: //
898: // Portions created by (your name) are Copyright (C) (your legal entity). All Rights Reserved.
899: //
900: // Contributor(s): none.
901: //
|