001: /*-------------------------------------------------------------------------
002: *
003: * Copyright (c) 2003-2005, PostgreSQL Global Development Group
004: *
005: * IDENTIFICATION
006: * $PostgreSQL: pgjdbc/org/postgresql/jdbc2/TimestampUtils.java,v 1.22 2007/04/16 18:31:38 jurka Exp $
007: *
008: *-------------------------------------------------------------------------
009: */
010: package org.postgresql.jdbc2;
011:
012: import java.sql.*;
013:
014: import java.util.Calendar;
015: import java.util.GregorianCalendar;
016: import java.util.TimeZone;
017: import java.util.SimpleTimeZone;
018:
019: import org.postgresql.PGStatement;
020: import org.postgresql.util.GT;
021: import org.postgresql.util.PSQLState;
022: import org.postgresql.util.PSQLException;
023:
024: /**
025: * Misc utils for handling time and date values.
026: */
027: public class TimestampUtils {
028: private StringBuffer sbuf = new StringBuffer();
029:
030: private Calendar defaultCal = new GregorianCalendar();
031:
032: private Calendar calCache;
033: private int calCacheZone;
034:
035: private final boolean min74;
036: private final boolean min82;
037:
038: TimestampUtils(boolean min74, boolean min82) {
039: this .min74 = min74;
040: this .min82 = min82;
041: }
042:
043: private Calendar getCalendar(int sign, int hr, int min, int sec) {
044: int rawOffset = sign * (((hr * 60 + min) * 60 + sec) * 1000);
045: if (calCache != null && calCacheZone == rawOffset)
046: return calCache;
047:
048: StringBuffer zoneID = new StringBuffer("GMT");
049: zoneID.append(sign < 0 ? '-' : '+');
050: if (hr < 10)
051: zoneID.append('0');
052: zoneID.append(hr);
053: if (min < 10)
054: zoneID.append('0');
055: zoneID.append(min);
056: if (sec < 10)
057: zoneID.append('0');
058: zoneID.append(sec);
059:
060: TimeZone syntheticTZ = new SimpleTimeZone(rawOffset, zoneID
061: .toString());
062: calCache = new GregorianCalendar(syntheticTZ);
063: calCacheZone = rawOffset;
064: return calCache;
065: }
066:
067: private static class ParsedTimestamp {
068: boolean hasDate = false;
069: int era = GregorianCalendar.AD;
070: int year = 1970;
071: int month = 1;
072:
073: boolean hasTime = false;
074: int day = 1;
075: int hour = 0;
076: int minute = 0;
077: int second = 0;
078: int nanos = 0;
079:
080: Calendar tz = null;
081: }
082:
083: /**
084: * Load date/time information into the provided calendar
085: * returning the fractional seconds.
086: */
087: private ParsedTimestamp loadCalendar(Calendar defaultTz,
088: String str, String type) throws SQLException {
089: char[] s = str.toCharArray();
090: int slen = s.length;
091:
092: // This is pretty gross..
093: ParsedTimestamp result = new ParsedTimestamp();
094:
095: // We try to parse these fields in order; all are optional
096: // (but some combinations don't make sense, e.g. if you have
097: // both date and time then they must be whitespace-separated).
098: // At least one of date and time must be present.
099:
100: // leading whitespace
101: // yyyy-mm-dd
102: // whitespace
103: // hh:mm:ss
104: // whitespace
105: // timezone in one of the formats: +hh, -hh, +hh:mm, -hh:mm
106: // whitespace
107: // if date is present, an era specifier: AD or BC
108: // trailing whitespace
109:
110: try {
111: int start = skipWhitespace(s, 0); // Skip leading whitespace
112: int end = firstNonDigit(s, start);
113: int num;
114: char sep;
115:
116: // Possibly read date.
117: if (charAt(s, end) == '-') {
118: //
119: // Date
120: //
121: result.hasDate = true;
122:
123: // year
124: result.year = number(s, start, end);
125: start = end + 1; // Skip '-'
126:
127: // month
128: end = firstNonDigit(s, start);
129: result.month = number(s, start, end);
130:
131: sep = charAt(s, end);
132: if (sep != '-')
133: throw new NumberFormatException(
134: "Expected date to be dash-separated, got '"
135: + sep + "'");
136:
137: start = end + 1; // Skip '-'
138:
139: // day of month
140: end = firstNonDigit(s, start);
141: result.day = number(s, start, end);
142:
143: start = skipWhitespace(s, end); // Skip trailing whitespace
144: }
145:
146: // Possibly read time.
147: if (Character.isDigit(charAt(s, start))) {
148: //
149: // Time.
150: //
151:
152: result.hasTime = true;
153:
154: // Hours
155:
156: end = firstNonDigit(s, start);
157: result.hour = number(s, start, end);
158:
159: sep = charAt(s, end);
160: if (sep != ':')
161: throw new NumberFormatException(
162: "Expected time to be colon-separated, got '"
163: + sep + "'");
164:
165: start = end + 1; // Skip ':'
166:
167: // minutes
168:
169: end = firstNonDigit(s, start);
170: result.minute = number(s, start, end);
171:
172: sep = charAt(s, end);
173: if (sep != ':')
174: throw new NumberFormatException(
175: "Expected time to be colon-separated, got '"
176: + sep + "'");
177:
178: start = end + 1; // Skip ':'
179:
180: // seconds
181:
182: end = firstNonDigit(s, start);
183: result.second = number(s, start, end);
184: start = end;
185:
186: // Fractional seconds.
187: if (charAt(s, start) == '.') {
188: end = firstNonDigit(s, start + 1); // Skip '.'
189: num = number(s, start + 1, end);
190:
191: for (int numlength = (end - (start + 1)); numlength < 9; ++numlength)
192: num *= 10;
193:
194: result.nanos = num;
195: start = end;
196: }
197:
198: start = skipWhitespace(s, start); // Skip trailing whitespace
199: }
200:
201: // Possibly read timezone.
202: sep = charAt(s, start);
203: if (sep == '-' || sep == '+') {
204: int tzsign = (sep == '-') ? -1 : 1;
205: int tzhr, tzmin, tzsec;
206:
207: end = firstNonDigit(s, start + 1); // Skip +/-
208: tzhr = number(s, start + 1, end);
209: start = end;
210:
211: sep = charAt(s, start);
212: if (sep == ':') {
213: end = firstNonDigit(s, start + 1); // Skip ':'
214: tzmin = number(s, start + 1, end);
215: start = end;
216: } else {
217: tzmin = 0;
218: }
219:
220: tzsec = 0;
221: if (min82) {
222: sep = charAt(s, start);
223: if (sep == ':') {
224: end = firstNonDigit(s, start + 1); // Skip ':'
225: tzsec = number(s, start + 1, end);
226: start = end;
227: }
228: }
229:
230: // Setting offset does not seem to work correctly in all
231: // cases.. So get a fresh calendar for a synthetic timezone
232: // instead
233: result.tz = getCalendar(tzsign, tzhr, tzmin, tzsec);
234:
235: start = skipWhitespace(s, start); // Skip trailing whitespace
236: }
237:
238: if (result.hasDate && start < slen) {
239: String eraString = new String(s, start, slen - start);
240: if (eraString.startsWith("AD")) {
241: result.era = GregorianCalendar.AD;
242: start += 2;
243: } else if (eraString.startsWith("BC")) {
244: result.era = GregorianCalendar.BC;
245: start += 2;
246: }
247: }
248:
249: if (start < slen)
250: throw new NumberFormatException(
251: "Trailing junk on timestamp: '"
252: + new String(s, start, slen - start)
253: + "'");
254:
255: if (!result.hasTime && !result.hasDate)
256: throw new NumberFormatException(
257: "Timestamp has neither date nor time");
258:
259: } catch (NumberFormatException nfe) {
260: throw new PSQLException(GT.tr(
261: "Bad value for type {0} : {1}", new Object[] {
262: type, str }),
263: PSQLState.BAD_DATETIME_FORMAT, nfe);
264: }
265:
266: return result;
267: }
268:
269: //
270: // Debugging hooks, not normally used unless developing -- uncomment the
271: // bodies for stderr debug spam.
272: //
273:
274: private static void showParse(String type, String what,
275: Calendar cal, java.util.Date result, Calendar resultCal) {
276: // java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd G HH:mm:ss Z");
277: // sdf.setTimeZone(resultCal.getTimeZone());
278:
279: // StringBuffer sb = new StringBuffer("Parsed ");
280: // sb.append(type);
281: // sb.append(" '");
282: // sb.append(what);
283: // sb.append("' in zone ");
284: // sb.append(cal.getTimeZone().getID());
285: // sb.append(" as ");
286: // sb.append(sdf.format(result));
287: // sb.append(" (millis=");
288: // sb.append(result.getTime());
289: // sb.append(")");
290:
291: // System.err.println(sb.toString());
292: }
293:
294: private static void showString(String type, Calendar cal,
295: java.util.Date value, String result) {
296: // java.text.SimpleDateFormat sdf = new java.text.SimpleDateFormat("yyyy-MM-dd G HH:mm:ss Z");
297: // sdf.setTimeZone(cal.getTimeZone());
298:
299: // StringBuffer sb = new StringBuffer("Stringized ");
300: // sb.append(type);
301: // sb.append(" ");
302: // sb.append(sdf.format(value));
303: // sb.append(" (millis=");
304: // sb.append(value.getTime());
305: // sb.append(") as '");
306: // sb.append(result);
307: // sb.append("'");
308:
309: // System.err.println(sb.toString());
310: }
311:
312: /**
313: * Parse a string and return a timestamp representing its value.
314: *
315: * @param s The ISO formated date string to parse.
316: *
317: * @return null if s is null or a timestamp of the parsed string s.
318: *
319: * @throws SQLException if there is a problem parsing s.
320: **/
321: public synchronized Timestamp toTimestamp(Calendar cal, String s)
322: throws SQLException {
323: if (s == null)
324: return null;
325:
326: int slen = s.length();
327:
328: // convert postgres's infinity values to internal infinity magic value
329: if (slen == 8 && s.equals("infinity")) {
330: return new Timestamp(PGStatement.DATE_POSITIVE_INFINITY);
331: }
332:
333: if (slen == 9 && s.equals("-infinity")) {
334: return new Timestamp(PGStatement.DATE_NEGATIVE_INFINITY);
335: }
336:
337: if (cal == null)
338: cal = defaultCal;
339:
340: ParsedTimestamp ts = loadCalendar(cal, s, "timestamp");
341: Calendar useCal = (ts.tz == null ? cal : ts.tz);
342: useCal.set(Calendar.ERA, ts.era);
343: useCal.set(Calendar.YEAR, ts.year);
344: useCal.set(Calendar.MONTH, ts.month - 1);
345: useCal.set(Calendar.DAY_OF_MONTH, ts.day);
346: useCal.set(Calendar.HOUR_OF_DAY, ts.hour);
347: useCal.set(Calendar.MINUTE, ts.minute);
348: useCal.set(Calendar.SECOND, ts.second);
349: useCal.set(Calendar.MILLISECOND, 0);
350:
351: Timestamp result = new Timestamp(useCal.getTime().getTime());
352: result.setNanos(ts.nanos);
353: showParse("timestamp", s, cal, result, useCal);
354: return result;
355: }
356:
357: public synchronized Time toTime(Calendar cal, String s)
358: throws SQLException {
359: if (s == null)
360: return null;
361:
362: int slen = s.length();
363:
364: // infinity cannot be represented as Time
365: // so there's not much we can do here.
366: if ((slen == 8 && s.equals("infinity"))
367: || (slen == 9 && s.equals("-infinity"))) {
368: throw new PSQLException(
369: GT
370: .tr("Infinite value found for timestamp/date. This cannot be represented as time."),
371: PSQLState.DATETIME_OVERFLOW);
372: }
373:
374: if (cal == null)
375: cal = defaultCal;
376:
377: ParsedTimestamp ts = loadCalendar(cal, s, "time");
378:
379: Calendar useCal = (ts.tz == null ? cal : ts.tz);
380: useCal.set(Calendar.HOUR_OF_DAY, ts.hour);
381: useCal.set(Calendar.MINUTE, ts.minute);
382: useCal.set(Calendar.SECOND, ts.second);
383: useCal.set(Calendar.MILLISECOND, (ts.nanos + 500000) / 1000000);
384:
385: if (ts.hasDate) {
386: // Rotate it into the requested timezone before we zero out the date
387: useCal.set(Calendar.ERA, ts.era);
388: useCal.set(Calendar.YEAR, ts.year);
389: useCal.set(Calendar.MONTH, ts.month - 1);
390: useCal.set(Calendar.DAY_OF_MONTH, ts.day);
391: cal.setTime(new Date(useCal.getTime().getTime()));
392: useCal = cal;
393: }
394:
395: useCal.set(Calendar.ERA, GregorianCalendar.AD);
396: useCal.set(Calendar.YEAR, 1970);
397: useCal.set(Calendar.MONTH, 0);
398: useCal.set(Calendar.DAY_OF_MONTH, 1);
399:
400: Time result = new Time(useCal.getTime().getTime());
401: showParse("time", s, cal, result, useCal);
402: return result;
403: }
404:
405: public synchronized Date toDate(Calendar cal, String s)
406: throws SQLException {
407: if (s == null)
408: return null;
409:
410: int slen = s.length();
411:
412: // convert postgres's infinity values to internal infinity magic value
413: if (slen == 8 && s.equals("infinity")) {
414: return new Date(PGStatement.DATE_POSITIVE_INFINITY);
415: }
416:
417: if (slen == 9 && s.equals("-infinity")) {
418: return new Date(PGStatement.DATE_NEGATIVE_INFINITY);
419: }
420:
421: if (cal == null)
422: cal = defaultCal;
423:
424: ParsedTimestamp ts = loadCalendar(cal, s, "date");
425: Calendar useCal = (ts.tz == null ? cal : ts.tz);
426:
427: useCal.set(Calendar.ERA, ts.era);
428: useCal.set(Calendar.YEAR, ts.year);
429: useCal.set(Calendar.MONTH, ts.month - 1);
430: useCal.set(Calendar.DAY_OF_MONTH, ts.day);
431:
432: if (ts.hasTime) {
433: // Rotate it into the requested timezone before we zero out the time
434: useCal.set(Calendar.HOUR_OF_DAY, ts.hour);
435: useCal.set(Calendar.MINUTE, ts.minute);
436: useCal.set(Calendar.SECOND, ts.second);
437: useCal.set(Calendar.MILLISECOND,
438: (ts.nanos + 500000) / 1000000);
439: cal.setTime(new Date(useCal.getTime().getTime()));
440: useCal = cal;
441: }
442:
443: useCal.set(Calendar.HOUR_OF_DAY, 0);
444: useCal.set(Calendar.MINUTE, 0);
445: useCal.set(Calendar.SECOND, 0);
446: useCal.set(Calendar.MILLISECOND, 0);
447:
448: Date result = new Date(useCal.getTime().getTime());
449: showParse("date", s, cal, result, useCal);
450: return result;
451: }
452:
453: public synchronized String toString(Calendar cal, Timestamp x) {
454: if (cal == null)
455: cal = defaultCal;
456:
457: cal.setTime(x);
458: sbuf.setLength(0);
459:
460: if (x.getTime() == PGStatement.DATE_POSITIVE_INFINITY) {
461: sbuf.append("infinity");
462: } else if (x.getTime() == PGStatement.DATE_NEGATIVE_INFINITY) {
463: sbuf.append("-infinity");
464: } else {
465: appendDate(sbuf, cal);
466: sbuf.append(' ');
467: appendTime(sbuf, cal, x.getNanos());
468: appendTimeZone(sbuf, cal);
469: appendEra(sbuf, cal);
470: }
471:
472: showString("timestamp", cal, x, sbuf.toString());
473: return sbuf.toString();
474: }
475:
476: public synchronized String toString(Calendar cal, Date x) {
477: if (cal == null)
478: cal = defaultCal;
479:
480: cal.setTime(x);
481: sbuf.setLength(0);
482:
483: if (x.getTime() == PGStatement.DATE_POSITIVE_INFINITY) {
484: sbuf.append("infinity");
485: } else if (x.getTime() == PGStatement.DATE_NEGATIVE_INFINITY) {
486: sbuf.append("-infinity");
487: } else {
488: appendDate(sbuf, cal);
489: appendEra(sbuf, cal);
490: appendTimeZone(sbuf, cal);
491: }
492:
493: showString("date", cal, x, sbuf.toString());
494:
495: return sbuf.toString();
496: }
497:
498: public synchronized String toString(Calendar cal, Time x) {
499: if (cal == null)
500: cal = defaultCal;
501:
502: cal.setTime(x);
503: sbuf.setLength(0);
504:
505: appendTime(sbuf, cal, cal.get(Calendar.MILLISECOND) * 1000000);
506:
507: // The 'time' parser for <= 7.3 doesn't like timezones.
508: if (min74)
509: appendTimeZone(sbuf, cal);
510:
511: showString("time", cal, x, sbuf.toString());
512:
513: return sbuf.toString();
514: }
515:
516: private static void appendDate(StringBuffer sb, Calendar cal) {
517: int l_year = cal.get(Calendar.YEAR);
518: // always use at least four digits for the year so very
519: // early years, like 2, don't get misinterpreted
520: //
521: int l_yearlen = String.valueOf(l_year).length();
522: for (int i = 4; i > l_yearlen; i--) {
523: sb.append("0");
524: }
525:
526: sb.append(l_year);
527: sb.append('-');
528: int l_month = cal.get(Calendar.MONTH) + 1;
529: if (l_month < 10)
530: sb.append('0');
531: sb.append(l_month);
532: sb.append('-');
533: int l_day = cal.get(Calendar.DAY_OF_MONTH);
534: if (l_day < 10)
535: sb.append('0');
536: sb.append(l_day);
537: }
538:
539: private static void appendTime(StringBuffer sb, Calendar cal,
540: int nanos) {
541: int hours = cal.get(Calendar.HOUR_OF_DAY);
542: if (hours < 10)
543: sb.append('0');
544: sb.append(hours);
545:
546: sb.append(':');
547: int minutes = cal.get(Calendar.MINUTE);
548: if (minutes < 10)
549: sb.append('0');
550: sb.append(minutes);
551:
552: sb.append(':');
553: int seconds = cal.get(Calendar.SECOND);
554: if (seconds < 10)
555: sb.append('0');
556: sb.append(seconds);
557:
558: // Add nanoseconds.
559: // This won't work for server versions < 7.2 which only want
560: // a two digit fractional second, but we don't need to support 7.1
561: // anymore and getting the version number here is difficult.
562: //
563: char[] decimalStr = { '0', '0', '0', '0', '0', '0', '0', '0',
564: '0' };
565: char[] nanoStr = Integer.toString(nanos).toCharArray();
566: System.arraycopy(nanoStr, 0, decimalStr, decimalStr.length
567: - nanoStr.length, nanoStr.length);
568: sb.append('.');
569: sb.append(decimalStr, 0, 6);
570: }
571:
572: private void appendTimeZone(StringBuffer sb, java.util.Calendar cal) {
573: int offset = (cal.get(Calendar.ZONE_OFFSET) + cal
574: .get(Calendar.DST_OFFSET)) / 1000;
575:
576: int absoff = Math.abs(offset);
577: int hours = absoff / 60 / 60;
578: int mins = (absoff - hours * 60 * 60) / 60;
579: int secs = absoff - hours * 60 * 60 - mins * 60;
580:
581: sb.append((offset >= 0) ? " +" : " -");
582:
583: if (hours < 10)
584: sb.append('0');
585: sb.append(hours);
586:
587: sb.append(':');
588:
589: if (mins < 10)
590: sb.append('0');
591: sb.append(mins);
592:
593: if (min82) {
594: sb.append(':');
595: if (secs < 10)
596: sb.append('0');
597: sb.append(secs);
598: }
599: }
600:
601: private static void appendEra(StringBuffer sb, Calendar cal) {
602: if (cal.get(Calendar.ERA) == GregorianCalendar.BC) {
603: sb.append(" BC");
604: }
605: }
606:
607: private static int skipWhitespace(char[] s, int start) {
608: int slen = s.length;
609: for (int i = start; i < slen; i++) {
610: if (!Character.isSpace(s[i]))
611: return i;
612: }
613: return slen;
614: }
615:
616: private static int firstNonDigit(char[] s, int start) {
617: int slen = s.length;
618: for (int i = start; i < slen; i++) {
619: if (!Character.isDigit(s[i])) {
620: return i;
621: }
622: }
623: return slen;
624: }
625:
626: private static int number(char[] s, int start, int end) {
627: if (start >= end) {
628: throw new NumberFormatException();
629: }
630: int n = 0;
631: for (int i = start; i < end; i++) {
632: n = 10 * n + (s[i] - '0');
633: }
634: return n;
635: }
636:
637: private static char charAt(char[] s, int pos) {
638: if (pos >= 0 && pos < s.length) {
639: return s[pos];
640: }
641: return '\0';
642: }
643:
644: }
|