001: // Copyright (c) 2006 Per M.A. Bothner.
002: // This is free software; for terms and warranty disclaimer see ../../COPYING.
003:
004: package gnu.math;
005:
006: import java.util.Date;
007: import java.util.Calendar;
008: import java.util.TimeZone;
009: import java.util.GregorianCalendar;
010: import gnu.math.IntNum;
011:
012: /**
013: * Represents a date and/or time.
014: * Similar functionality as java.util.Calendar (and uses GregorianCalendar
015: * internally) but supports arithmetic.
016: * Can be for XML Schema date/time types, specifically as used in XPath/Xquery..
017: */
018:
019: public class DateTime extends Quantity implements Cloneable {
020: Unit unit = Unit.date;
021:
022: /** Fractional seconds, in units of nanoseconds. */
023: int nanoSeconds;
024: GregorianCalendar calendar;
025: int mask;
026:
027: /*
028: static final int REFERENCE_YEAR = 1972;
029: static final int REFERENCE_MONTH = 0; // January
030: static final int REFERENCE_DAY = 0; // January 1
031: */
032:
033: static final int YEAR_COMPONENT = 1;
034: static final int MONTH_COMPONENT = 2;
035: static final int DAY_COMPONENT = 3;
036: static final int HOURS_COMPONENT = 4;
037: static final int MINUTES_COMPONENT = 5;
038: static final int SECONDS_COMPONENT = 6;
039: static final int TIMEZONE_COMPONENT = 7;
040:
041: public static final int YEAR_MASK = 1 << YEAR_COMPONENT;
042: public static final int MONTH_MASK = 1 << MONTH_COMPONENT;
043: public static final int DAY_MASK = 1 << DAY_COMPONENT;
044: public static final int HOURS_MASK = 1 << HOURS_COMPONENT;
045: public static final int MINUTES_MASK = 1 << MINUTES_COMPONENT;
046: public static final int SECONDS_MASK = 1 << SECONDS_COMPONENT;
047: public static final int TIMEZONE_MASK = 1 << TIMEZONE_COMPONENT;
048: public static final int DATE_MASK = YEAR_MASK | MONTH_MASK
049: | DAY_MASK;
050: public static final int TIME_MASK = HOURS_MASK | MINUTES_MASK
051: | SECONDS_MASK;
052:
053: public int components() {
054: return mask & ~TIMEZONE_MASK;
055: }
056:
057: public DateTime cast(int newComponents) {
058: int oldComponents = mask & ~TIMEZONE_MASK;
059: if (newComponents == oldComponents)
060: return this ;
061: DateTime copy = new DateTime(newComponents,
062: (GregorianCalendar) calendar.clone());
063: if ((newComponents & ~oldComponents) != 0
064: // Special case: Casting xs:date to xs:dateTime *is* allowed.
065: && !(oldComponents == DATE_MASK && newComponents == (DATE_MASK | TIME_MASK)))
066: throw new ClassCastException(
067: "cannot cast DateTime - missing conponents");
068: if (isZoneUnspecified())
069: copy.mask &= ~TIMEZONE_MASK;
070: else
071: copy.mask |= TIMEZONE_MASK;
072: int extraComponents = oldComponents & ~newComponents;
073: if ((extraComponents & TIME_MASK) != 0) {
074: copy.calendar.clear(Calendar.HOUR_OF_DAY);
075: copy.calendar.clear(Calendar.MINUTE);
076: copy.calendar.clear(Calendar.SECOND);
077: } else
078: copy.nanoSeconds = nanoSeconds;
079: if ((extraComponents & YEAR_MASK) != 0) {
080: copy.calendar.clear(Calendar.YEAR);
081: copy.calendar.clear(Calendar.ERA);
082: }
083: if ((extraComponents & MONTH_MASK) != 0)
084: copy.calendar.clear(Calendar.MONTH);
085: if ((extraComponents & DAY_MASK) != 0)
086: copy.calendar.clear(Calendar.DATE);
087: return copy;
088: }
089:
090: private static final Date minDate = new Date(Long.MIN_VALUE);
091:
092: public DateTime(int mask) {
093: calendar = new GregorianCalendar();
094: // Never use Julian calendar.
095: calendar.setGregorianChange(minDate);
096: calendar.clear();
097: this .mask = mask;
098: }
099:
100: public DateTime(int mask, GregorianCalendar calendar) {
101: this .calendar = calendar;
102: this .mask = mask;
103: }
104:
105: public static DateTime parse(String value, int mask) {
106: DateTime result = new DateTime(mask);
107: value = value.trim();
108: int len = value.length();
109: int pos = 0;
110: boolean wantDate = (mask & DATE_MASK) != 0;
111: boolean wantTime = (mask & TIME_MASK) != 0;
112: if (wantDate) {
113: pos = result.parseDate(value, pos, mask);
114: if (wantTime) {
115: if (pos < 0 || pos >= len || value.charAt(pos) != 'T')
116: pos = -1;
117: else
118: pos++;
119: }
120: }
121: if (wantTime)
122: pos = result.parseTime(value, pos);
123: pos = result.parseZone(value, pos);
124: if (pos != len)
125: throw new NumberFormatException("Unrecognized date/time '"
126: + value + '\'');
127: return result;
128: }
129:
130: int parseDate(String str, int start, int mask) {
131: if (start < 0)
132: return start;
133: int len = str.length();
134: boolean negYear = false;
135: if (start < len && str.charAt(start) == '-') {
136: start++;
137: negYear = true;
138: }
139: int pos = start;
140: int part, year, month;
141: if ((mask & YEAR_MASK) == 0) {
142: if (!negYear)
143: return -1;
144: year = -1;
145: } else {
146: part = parseDigits(str, pos);
147: year = part >> 16;
148: pos = part & 0xffff;
149: if (pos != start + 4
150: && (pos <= start + 4 || str.charAt(start) == '0'))
151: return -1;
152: if (negYear || year == 0) {
153: calendar.set(Calendar.ERA, GregorianCalendar.BC);
154: calendar.set(Calendar.YEAR, year + 1);
155: } else
156: calendar.set(Calendar.YEAR, year);
157: }
158: if ((mask & (MONTH_MASK | DAY_MASK)) == 0)
159: return pos;
160: if (pos >= len || str.charAt(pos) != '-')
161: return -1;
162: start = ++pos;
163: if ((mask & MONTH_MASK) != 0) {
164: part = parseDigits(str, start);
165: month = part >> 16;
166: pos = part & 0xffff;
167: if (month <= 0 || month > 12 || pos != start + 2)
168: return -1;
169: calendar.set(Calendar.MONTH, month - 1);
170: if ((mask & DAY_MASK) == 0)
171: return pos;
172: } else
173: month = -1;
174: if (pos >= len || str.charAt(pos) != '-')
175: return -1;
176: start = pos + 1;
177: part = parseDigits(str, start);
178: int day = part >> 16;
179: pos = part & 0xffff;
180: if (day > 0 && pos == start + 2) {
181: int maxDay;
182: if ((mask & MONTH_MASK) == 0)
183: maxDay = 31;
184: else
185: maxDay = daysInMonth(month - 1,
186: (mask & YEAR_MASK) != 0 ? year : 2000);
187: if (day <= maxDay) {
188: calendar.set(Calendar.DATE, day);
189: return pos;
190: }
191: }
192: return -1;
193: }
194:
195: public static boolean isLeapYear(int year) {
196: return (year % 4) == 0
197: && ((year % 100) != 0 || (year % 400) == 0);
198: }
199:
200: public static int daysInMonth(int month, int year) {
201: switch (month) {
202: case Calendar.APRIL:
203: case Calendar.JUNE:
204: case Calendar.SEPTEMBER:
205: case Calendar.NOVEMBER:
206: return 30;
207: case Calendar.FEBRUARY:
208: return isLeapYear(year) ? 29 : 28;
209: default:
210: return 31;
211: }
212: }
213:
214: public static TimeZone GMT = TimeZone.getTimeZone("GMT");
215:
216: int parseZone(String str, int start) {
217: if (start < 0)
218: return start;
219: int part = parseZoneMinutes(str, start);
220: if (part == 0)
221: return -1;
222: if (part == start)
223: return start;
224: int minutes = part >> 16;
225: TimeZone zone;
226: int pos = part & 0xffff;
227: if (minutes == 0)
228: zone = GMT;
229: else
230: zone = TimeZone.getTimeZone("GMT"
231: + str.substring(start, pos));
232: calendar.setTimeZone(zone);
233: mask |= TIMEZONE_MASK;
234: return pos;
235: }
236:
237: /** Return (MINUTES<<16)|END_POS if time-zone indicator was seen.
238: * Returns START otherwise, or 0 on an error. */
239: int parseZoneMinutes(String str, int start) {
240: int len = str.length();
241: if (start == len || start < 0)
242: return start;
243: char ch = str.charAt(start);
244: if (ch == 'Z')
245: return start + 1;
246: if (ch != '+' && ch != '-')
247: return start;
248: start++;
249: int part = parseDigits(str, start);
250: int hour = part >> 16;
251: if (hour > 14)
252: return 0;
253: int minute = 60 * hour;
254: int pos = part & 0xffff;
255: if (pos != start + 2)
256: return 0;
257: if (pos < len) {
258: if (str.charAt(pos) == ':') {
259: start = pos + 1;
260: part = parseDigits(str, start);
261: pos = part & 0xffff;
262: part >>= 16;
263: if (part > 0 && (part >= 60 || hour == 14))
264: return 0;
265: minute += part;
266: if (pos != start + 2)
267: return 0;
268: }
269: } else
270: // The minutes part is not optional.
271: return 0;
272: if (minute > 840)
273: return 0;
274: if (ch == '-')
275: minute = -minute;
276: return (minute << 16) | pos;
277: }
278:
279: int parseTime(String str, int start) {
280: if (start < 0)
281: return start;
282: int len = str.length();
283: int pos = start;
284: int part = parseDigits(str, start);
285: int hour = part >> 16;
286: pos = part & 0xffff;
287: if (hour <= 24 && pos == start + 2 && pos != len
288: && str.charAt(pos) == ':') {
289: start = pos + 1;
290: part = parseDigits(str, start);
291: int minute = part >> 16;
292: pos = part & 0xffff;
293: if (minute < 60 && pos == start + 2 && pos != len
294: && str.charAt(pos) == ':') {
295: start = pos + 1;
296: part = parseDigits(str, start);
297: int second = part >> 16;
298: pos = part & 0xffff;
299: // We don't allow/handle leap seconds.
300: if (second < 60 && pos == start + 2) {
301: if (pos + 1 < len
302: && str.charAt(pos) == '.'
303: && Character.digit(str.charAt(pos + 1), 10) >= 0) {
304: start = pos + 1;
305: pos = start;
306: int nanos = 0;
307: int nfrac = 0;
308: for (; pos < len; nfrac++, pos++) {
309: int dig = Character.digit(str.charAt(pos),
310: 10);
311: if (dig < 0)
312: break;
313: if (nfrac < 9)
314: nanos = 10 * nanos + dig;
315: else if (nfrac == 9 && dig >= 5)
316: nanos++;
317: }
318: while (nfrac++ < 9)
319: nanos = 10 * nanos;
320: nanoSeconds = nanos;
321: }
322: if (hour == 24
323: && (minute != 0 || second != 0 || nanoSeconds != 0))
324: return -1;
325: calendar.set(Calendar.HOUR_OF_DAY, hour);
326: calendar.set(Calendar.MINUTE, minute);
327: calendar.set(Calendar.SECOND, second);
328: return pos;
329: }
330: }
331: }
332: return -1;
333: }
334:
335: /** Return (VALUE << 16)|END. */
336: private static int parseDigits(String str, int start) {
337: int i = start;
338: int val = -1;
339: int len = str.length();
340: while (i < len) {
341: char ch = str.charAt(i);
342: int dig = Character.digit(ch, 10);
343: if (dig < 0)
344: break;
345: if (val > 20000)
346: return 0; // possible overflow
347: val = val < 0 ? dig : 10 * val + dig;
348: i++;
349: }
350: return val < 0 ? i : (val << 16) | i;
351: }
352:
353: public int getYear() {
354: int year = calendar.get(Calendar.YEAR);
355: if (calendar.get(Calendar.ERA) == GregorianCalendar.BC)
356: year = 1 - year;
357: return year;
358: }
359:
360: public int getMonth() {
361: return calendar.get(Calendar.MONTH) + 1;
362: }
363:
364: public int getDay() {
365: return calendar.get(Calendar.DATE);
366: }
367:
368: public int getHours() {
369: return calendar.get(Calendar.HOUR_OF_DAY);
370: }
371:
372: public int getMinutes() {
373: return calendar.get(Calendar.MINUTE);
374: }
375:
376: public int getSecondsOnly() {
377: return calendar.get(Calendar.SECOND);
378: }
379:
380: public int getWholeSeconds() // deprecated
381: {
382: return calendar.get(Calendar.SECOND);
383: }
384:
385: public int getNanoSecondsOnly() {
386: return nanoSeconds;
387: }
388:
389: /*
390: public Object getSecondsObject ()
391: {
392: return IntNum.make(getWholeSeconds());
393: }
394: */
395:
396: /** Return -1, 0, or 1, depending on which value is greater. */
397: public static int compare(DateTime date1, DateTime date2) {
398: long millis1 = date1.calendar.getTimeInMillis();
399: long millis2 = date2.calendar.getTimeInMillis();
400: if (((date1.mask | date2.mask) & DATE_MASK) == 0) {
401: if (millis1 < 0)
402: millis1 += 24 * 60 * 60 * 1000;
403: if (millis2 < 0)
404: millis2 += 24 * 60 * 60 * 1000;
405: }
406: int nanos1 = date1.nanoSeconds;
407: int nanos2 = date2.nanoSeconds;
408: millis1 += nanos1 / 1000000;
409: millis2 += nanos2 / 1000000;
410: nanos1 = nanos1 % 1000000;
411: nanos2 = nanos2 % 1000000;
412: return millis1 < millis2 ? -1 : millis1 > millis2 ? 1
413: : nanos1 < nanos2 ? -1 : nanos1 > nanos2 ? 1 : 0;
414: }
415:
416: public int compare(Object obj) {
417: if (obj instanceof DateTime)
418: return compare(this , (DateTime) obj);
419: return ((Numeric) obj).compareReversed(this );
420: }
421:
422: public static Duration sub(DateTime date1, DateTime date2) {
423: long millis1 = date1.calendar.getTimeInMillis();
424: long millis2 = date2.calendar.getTimeInMillis();
425: int nanos1 = date1.nanoSeconds;
426: int nanos2 = date2.nanoSeconds;
427: millis1 += nanos1 / 1000000;
428: millis2 += nanos2 / 1000000;
429: nanos1 = nanos1 % 1000000;
430: nanos2 = nanos2 % 1000000;
431: long millis = millis1 - millis2;
432: long seconds = millis / 1000;
433: int nanos = (int) ((millis % 1000) * 1000000 + nanos2 - nanos2);
434: seconds += nanos / 1000000000;
435: nanos = nanos % 1000000000;
436: return Duration.make(0, seconds, nanos, Unit.second);
437: }
438:
439: public DateTime withZoneUnspecified() {
440: if (isZoneUnspecified())
441: return this ;
442: DateTime r = new DateTime(mask, (GregorianCalendar) calendar
443: .clone());
444: r.calendar.setTimeZone(TimeZone.getDefault());
445: r.mask &= ~TIMEZONE_MASK;
446: return r;
447: }
448:
449: public DateTime adjustTimezone(int newOffset) {
450: DateTime r = new DateTime(mask, (GregorianCalendar) calendar
451: .clone());
452: TimeZone zone;
453: if (newOffset == 0)
454: zone = GMT;
455: else {
456: StringBuffer sbuf = new StringBuffer("GMT");
457: toStringZone(newOffset, sbuf);
458: zone = TimeZone.getTimeZone(sbuf.toString());
459: }
460: r.calendar.setTimeZone(zone);
461: if ((r.mask & TIMEZONE_MASK) != 0) {
462: long millis = calendar.getTimeInMillis();
463: r.calendar.setTimeInMillis(millis);
464: if ((mask & TIME_MASK) == 0) {
465: r.calendar.set(Calendar.HOUR_OF_DAY, 0);
466: r.calendar.set(Calendar.MINUTE, 0);
467: r.calendar.set(Calendar.SECOND, 0);
468: r.nanoSeconds = 0;
469: }
470: } else
471: r.mask |= TIMEZONE_MASK;
472: return r;
473: }
474:
475: public static DateTime add(DateTime x, Duration y, int k) {
476: if (y.unit == Unit.duration
477: || (y.unit == Unit.month && (x.mask & DATE_MASK) != DATE_MASK))
478: throw new IllegalArgumentException(
479: "invalid date/time +/- duration combinatuion");
480: DateTime r = new DateTime(x.mask,
481: (GregorianCalendar) x.calendar.clone());
482: if (y.months != 0) {
483: int month = 12 * r.getYear()
484: + r.calendar.get(Calendar.MONTH);
485: month += k * y.months;
486: int day = r.calendar.get(Calendar.DATE);
487: int year, daysInMonth;
488: if (month >= 12) {
489: year = month / 12;
490: month = month % 12;
491: r.calendar.set(Calendar.ERA, GregorianCalendar.AD);
492: daysInMonth = daysInMonth(month, year);
493: } else {
494: month = 11 - month;
495: r.calendar.set(Calendar.ERA, GregorianCalendar.BC);
496: year = (month / 12) + 1;
497: month = 11 - (month % 12);
498: daysInMonth = daysInMonth(month, 1);
499: }
500:
501: if (day > daysInMonth)
502: day = daysInMonth;
503: r.calendar.set(year, month, day);
504: }
505: long nanos = x.nanoSeconds + k
506: * (y.seconds * 1000000000L + y.nanos);
507: if (nanos != 0) {
508: if ((x.mask & TIME_MASK) == 0) { // Truncate to 00:00:00
509: long nanosPerDay = 1000000000L * 24 * 60 * 60;
510: long mod = nanos % nanosPerDay;
511: if (mod < 0)
512: mod += nanosPerDay;
513: nanos -= mod;
514: }
515: long millis = r.calendar.getTimeInMillis();
516: millis += (nanos / 1000000000L) * 1000;
517: r.calendar.setTimeInMillis(millis);
518: r.nanoSeconds = (int) (nanos % 1000000000L);
519: }
520: return r;
521: }
522:
523: public static DateTime addMinutes(DateTime x, int y) {
524: return addSeconds(x, 60 * y);
525: }
526:
527: public static DateTime addSeconds(DateTime x, int y) {
528: DateTime r = new DateTime(x.mask,
529: (GregorianCalendar) x.calendar.clone());
530: long nanos = y * 1000000000L;
531: if (nanos != 0) {
532: nanos = x.nanoSeconds + nanos;
533: long millis = x.calendar.getTimeInMillis();
534: millis += (nanos / 1000000L);
535: r.calendar.setTimeInMillis(millis);
536: r.nanoSeconds = (int) (nanos % 1000000L);
537: }
538: return r;
539: }
540:
541: public Numeric add(Object y, int k) {
542: if (y instanceof Duration)
543: return DateTime.add(this , (Duration) y, k);
544: if (y instanceof DateTime && k == -1)
545: return DateTime.sub(this , (DateTime) y);
546: throw new IllegalArgumentException();
547: }
548:
549: public Numeric addReversed(Numeric x, int k) {
550: if (x instanceof Duration && k == 1)
551: return DateTime.add(this , (Duration) x, k);
552: throw new IllegalArgumentException();
553: }
554:
555: private static void append(int value, StringBuffer sbuf,
556: int minWidth) {
557: int start = sbuf.length();
558: sbuf.append(value);
559: int padding = start + minWidth - sbuf.length();
560: while (--padding >= 0)
561: sbuf.insert(start, '0');
562: }
563:
564: public void toStringDate(StringBuffer sbuf) {
565: int mask = components();
566: if ((mask & YEAR_MASK) != 0) {
567: int year = calendar.get(Calendar.YEAR);
568: if (calendar.get(Calendar.ERA) == GregorianCalendar.BC) {
569: year--;
570: if (year != 0)
571: sbuf.append('-');
572: }
573: append(year, sbuf, 4);
574: } else
575: sbuf.append('-');
576: if ((mask & (MONTH_MASK | DAY_MASK)) != 0) {
577: sbuf.append('-');
578: if ((mask & MONTH_MASK) != 0)
579: append(getMonth(), sbuf, 2);
580: if ((mask & DAY_MASK) != 0) {
581: sbuf.append('-');
582: append(getDay(), sbuf, 2);
583: }
584: }
585: }
586:
587: public void toStringTime(StringBuffer sbuf) {
588: append(getHours(), sbuf, 2);
589: sbuf.append(':');
590: append(getMinutes(), sbuf, 2);
591: sbuf.append(':');
592: append(getWholeSeconds(), sbuf, 2);
593: Duration.appendNanoSeconds(nanoSeconds, sbuf);
594: }
595:
596: public boolean isZoneUnspecified() {
597: //TimeZone zone = calendar.getTimeZone();
598: //return zone.equals(TimeZone.getDefault()); // FIXME?
599: return (mask & TIMEZONE_MASK) == 0;
600: }
601:
602: public int getZoneMinutes() {
603: return calendar.getTimeZone().getRawOffset() / 60000;
604: }
605:
606: /** Get a TimeZone object for a given offset.
607: * @param minutes timezone offset in minutes.
608: */
609: public static TimeZone minutesToTimeZone(int minutes) {
610: if (minutes == 0)
611: return DateTime.GMT;
612: StringBuffer sbuf = new StringBuffer("GMT");
613: toStringZone(minutes, sbuf);
614: return TimeZone.getTimeZone(sbuf.toString());
615: }
616:
617: public void setTimeZone(TimeZone timeZone) {
618: calendar.setTimeZone(timeZone);
619: }
620:
621: public void toStringZone(StringBuffer sbuf) {
622: if (isZoneUnspecified())
623: return;
624: toStringZone(getZoneMinutes(), sbuf);
625: }
626:
627: public static void toStringZone(int minutes, StringBuffer sbuf) {
628: if (minutes == 0)
629: sbuf.append('Z');
630: else {
631: if (minutes < 0) {
632: sbuf.append('-');
633: minutes = -minutes;
634: } else
635: sbuf.append('+');
636: append(minutes / 60, sbuf, 2);
637: sbuf.append(':');
638: append(minutes % 60, sbuf, 2);
639: }
640: }
641:
642: public void toString(StringBuffer sbuf) {
643: int mask = components();
644: boolean hasDate = (mask & DATE_MASK) != 0;
645: boolean hasTime = (mask & TIME_MASK) != 0;
646: if (hasDate) {
647: toStringDate(sbuf);
648: if (hasTime)
649: sbuf.append('T');
650: }
651: if (hasTime)
652: toStringTime(sbuf);
653: toStringZone(sbuf);
654: }
655:
656: public String toString() {
657: StringBuffer sbuf = new StringBuffer();
658: toString(sbuf);
659: return sbuf.toString();
660: }
661:
662: public boolean isExact() {
663: return (mask & TIME_MASK) == 0;
664: }
665:
666: public boolean isZero() {
667: throw new Error("DateTime.isZero not meaningful!");
668: }
669:
670: public Unit unit() {
671: return unit;
672: }
673:
674: public Complex number() {
675: throw new Error("number needs to be implemented!");
676: }
677: }
|