| /** |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package org.apache.tajo.util.datetime; |
| |
| import com.google.common.annotations.VisibleForTesting; |
| import org.apache.tajo.conf.TajoConf; |
| import org.apache.tajo.datum.Int8Datum; |
| import org.apache.tajo.exception.ValueOutOfRangeException; |
| import org.apache.tajo.util.datetime.DateTimeConstants.DateStyle; |
| import org.apache.tajo.util.datetime.DateTimeConstants.DateToken; |
| import org.apache.tajo.util.datetime.DateTimeConstants.TokenField; |
| |
| import javax.annotation.Nullable; |
| import java.sql.Date; |
| import java.sql.Time; |
| import java.sql.Timestamp; |
| import java.util.TimeZone; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| import java.util.concurrent.atomic.AtomicInteger; |
| import java.util.concurrent.atomic.AtomicLong; |
| |
| /** |
| * This Class is originated from j2date in datetime.c of PostgreSQL. |
| */ |
| public class DateTimeUtil { |
| private static int MAX_FRACTION_LENGTH = 6; |
| |
| /** maximum possible number of fields in a date * string */ |
| private static int MAXDATEFIELDS = 25; |
| |
| public static boolean isJulianCalendar(int year, int month, int day) { |
| return year <= 1752 && month <= 9 && day < 14; |
| } |
| |
| public static int getCenturyOfEra(int year) { |
| if (year > 0) { |
| return (year - 1) / 100 + 1; |
| } else if (year < 0) { |
| //600BC to 501BC -> -6 |
| int pYear = -year; |
| return -((pYear - 1) / 100 + 1); |
| } else { |
| return 0; |
| } |
| } |
| |
| public static boolean isLeapYear(int year) { |
| return ((year & 3) == 0) && ((year % 100) != 0 || (year % 400) == 0); |
| } |
| |
| public static int getDaysInYearMonth(int year, int month) { |
| if (isLeapYear(year)) { |
| return DateTimeConstants.DAY_OF_MONTH[1][month - 1]; |
| } else { |
| return DateTimeConstants.DAY_OF_MONTH[0][month - 1]; |
| } |
| } |
| |
| /** |
| * Julian date support. |
| * |
| * isValidJulianDate checks the minimum date exactly, but is a bit sloppy |
| * about the maximum, since it's far enough out to not be especially |
| * interesting. |
| * @param years |
| * @param months |
| * @param days |
| * @return |
| */ |
| public static boolean isValidJulianDate(int years, int months, int days) { |
| return years > DateTimeConstants.JULIAN_MINYEAR || years == DateTimeConstants.JULIAN_MINYEAR && |
| months > DateTimeConstants.JULIAN_MINMONTH || months == DateTimeConstants.JULIAN_MINMONTH && |
| days >= DateTimeConstants.JULIAN_MINDAY && years < DateTimeConstants.JULIAN_MAXYEAR; |
| } |
| |
| /** |
| * Calendar time to Julian date conversions. |
| * Julian date is commonly used in astronomical applications, |
| * since it is numerically accurate and computationally simple. |
| * The algorithms here will accurately convert between Julian day |
| * and calendar date for all non-negative Julian days_full |
| * (i.e. from Nov 24, -4713 on). |
| * |
| * These routines will be used by other date/time packages |
| * - thomas 97/02/25 |
| * |
| * Rewritten to eliminate overflow problems. This now allows the |
| * routines to work correctly for all Julian day counts from |
| * 0 to 2147483647 (Nov 24, -4713 to Jun 3, 5874898) assuming |
| * a 32-bit integer. Longer types should also work to the limits |
| * of their precision. |
| * @param year |
| * @param month |
| * @param day |
| * @return |
| */ |
| public static int date2j(int year, int month, int day) { |
| int julian; |
| int century; |
| |
| if (month > 2) { |
| month += 1; |
| year += 4800; |
| } else { |
| month += 13; |
| year += 4799; |
| } |
| |
| century = year / 100; |
| julian = (year * 365) - 32167; |
| julian += (((year / 4) - century) + (century / 4)); |
| julian += ((7834 * month) / 256) + day; |
| |
| return julian; |
| } |
| |
| public static TimeMeta j2date(int julianDate) { |
| TimeMeta tm = new TimeMeta(); |
| j2date(julianDate, tm); |
| return tm; |
| } |
| |
| /** |
| * Set TimeMeta's date fields. |
| * @param julianDate |
| * @param tm |
| */ |
| public static void j2date(int julianDate, TimeMeta tm) { |
| long julian; |
| long quad; |
| long extra; |
| long y; |
| |
| julian = julianDate; |
| julian += 32044; |
| quad = julian / 146097; |
| extra = (julian - quad * 146097) * 4 + 3; |
| julian += 60 + quad * 3 + extra / 146097; |
| quad = julian / 1461; |
| julian -= quad * 1461; |
| y = julian * 4 / 1461; |
| julian = ((y != 0) ? ((julian + 305) % 365) : ((julian + 306) % 366)) |
| + 123; |
| y += quad * 4; |
| |
| |
| tm.years = (int)(y - 4800); |
| quad = julian * 2141 / 65536; |
| tm.dayOfMonth = (int)(julian - 7834 * quad / 256); |
| tm.monthOfYear = (int) ((quad + 10) % DateTimeConstants.MONTHS_PER_YEAR + 1); |
| } |
| |
| /** |
| * This method is originated from j2date in datetime.c of PostgreSQL. |
| * |
| * julianToDay - convert Julian date to day-of-week (0..6 == Sun..Sat) |
| * |
| * Note: various places use the locution julianToDay(date - 1) to produce a |
| * result according to the convention 0..6 = Mon..Sun. This is a bit of |
| * a crock, but will work as long as the computation here is just a modulo. |
| * @param julianDate |
| * @return |
| */ |
| public static int j2day(int julianDate) { |
| long day; |
| |
| day = julianDate; |
| |
| day += 1; |
| day %= 7; |
| |
| return (int) day; |
| } |
| |
| /** |
| * This method is originated from date2isoweek in timestamp.c of PostgreSQL. |
| * Returns ISO week number of year. |
| * @param year |
| * @param mon |
| * @param mday |
| * @return |
| */ |
| public static int date2isoweek(int year, int mon, int mday) { |
| double result; |
| int day0; |
| int day4; |
| int dayn; |
| |
| /* current day */ |
| dayn = date2j(year, mon, mday); |
| |
| /* fourth day of current year */ |
| day4 = date2j(year, 1, 4); |
| |
| /* day0 == offset to first day of week (Monday) */ |
| day0 = j2day(day4 - 1); |
| |
| /* |
| * We need the first week containing a Thursday, otherwise this day falls |
| * into the previous year for purposes of counting weeks |
| */ |
| if (dayn < day4 - day0) { |
| day4 = date2j(year - 1, 1, 4); |
| |
| /* day0 == offset to first day of week (Monday) */ |
| day0 = j2day(day4 - 1); |
| } |
| |
| result = (dayn - (day4 - day0)) / 7 + 1; |
| |
| /* |
| * Sometimes the last few days_full in a year will fall into the first week of |
| * the next year, so check for this. |
| */ |
| if (result >= 52) { |
| day4 = date2j(year + 1, 1, 4); |
| |
| /* day0 == offset to first day of week (Monday) */ |
| day0 = j2day(day4 - 1); |
| |
| if (dayn >= day4 - day0) { |
| result = (dayn - (day4 - day0)) / 7 + 1; |
| } |
| } |
| |
| return (int) result; |
| } |
| |
| /** |
| * date2isoyear() |
| * |
| * Returns ISO 8601 year number. |
| * @param year |
| * @param mon |
| * @param mday |
| * @return |
| */ |
| public static int date2isoyear(int year, int mon, int mday) { |
| /* current day */ |
| int dayn = date2j(year, mon, mday); |
| |
| /* fourth day of current year */ |
| int day4 = date2j(year, 1, 4); |
| |
| /* day0 == offset to first day of week (Monday) */ |
| int day0 = j2day(day4 - 1); |
| |
| /* |
| * We need the first week containing a Thursday, otherwise this day falls |
| * into the previous year for purposes of counting weeks |
| */ |
| if (dayn < day4 - day0) { |
| day4 = date2j(year - 1, 1, 4); |
| |
| /* day0 == offset to first day of week (Monday) */ |
| day0 = j2day(day4 - 1); |
| |
| year--; |
| } |
| |
| double result = (dayn - (day4 - day0)) / 7 + 1; |
| |
| /* |
| * Sometimes the last few days in a year will fall into the first week of |
| * the next year, so check for this. |
| */ |
| if (result >= 52) { |
| day4 = date2j(year + 1, 1, 4); |
| |
| /* day0 == offset to first day of week (Monday) */ |
| day0 = j2day(day4 - 1); |
| |
| if (dayn >= day4 - day0) { |
| year++; |
| } |
| } |
| |
| return year; |
| } |
| |
| /** |
| * Converts julian timestamp to epoch. |
| * @param timestamp |
| * @return |
| */ |
| public static int julianTimeToEpoch(long timestamp) { |
| long totalSecs = timestamp / DateTimeConstants.USECS_PER_SEC; |
| return (int)(totalSecs + DateTimeConstants.SECS_DIFFERENCE_BETWEEN_JULIAN_AND_UNIXTIME); |
| } |
| |
| /** |
| * Converts julian timestamp to java timestamp. |
| * @param timestamp julian time in millisecond |
| * @return java time in millisecond |
| */ |
| public static long julianTimeToJavaTime(long timestamp) { |
| double totalSecs = (double)timestamp / (double)DateTimeConstants.MSECS_PER_SEC; |
| return Math.round(totalSecs + DateTimeConstants.SECS_DIFFERENCE_BETWEEN_JULIAN_AND_UNIXTIME * 1000.0); |
| } |
| |
| /** |
| * Converts java timestamp to julian timestamp. |
| * @param javaTimestamp |
| * @return |
| */ |
| public static long javaTimeToJulianTime(long javaTimestamp) { |
| double totalSecs = javaTimestamp / 1000.0; |
| return (long)((totalSecs - |
| DateTimeConstants.SECS_DIFFERENCE_BETWEEN_JULIAN_AND_UNIXTIME) * DateTimeConstants.USECS_PER_SEC); |
| } |
| |
| /** |
| * Calculate the time value(hour, minute, sec, fsec) |
| * If tm.TomeZone is set, the result value is adjusted. |
| * @param tm |
| * @return |
| */ |
| public static long toTime(TimeMeta tm) { |
| if (tm.timeZone != 0 && tm.timeZone != Integer.MAX_VALUE) { |
| int timeZoneSecs = tm.timeZone; |
| tm.timeZone = Integer.MAX_VALUE; |
| tm.plusMillis(0 - timeZoneSecs * 1000); |
| } |
| return toTime(tm.hours, tm.minutes, tm.secs, tm.fsecs); |
| } |
| |
| /** |
| * Calculate the time value(hour, minute, sec, fsec) |
| * @param hour |
| * @param min |
| * @param sec |
| * @param fsec |
| * @return |
| */ |
| public static long toTime(int hour, int min, int sec, int fsec) { |
| return (((((hour * DateTimeConstants.MINS_PER_HOUR) + min) * |
| DateTimeConstants.SECS_PER_MINUTE) + sec) * |
| DateTimeConstants.USECS_PER_SEC) + fsec; |
| } |
| |
| public static long toJavaTime(int hour, int min, int sec, int fsec) { |
| return toTime(hour, min, sec, fsec)/DateTimeConstants.MSECS_PER_SEC; |
| } |
| |
| public static Timestamp toJavaTimestamp(TimeMeta tm, @Nullable TimeZone tz) { |
| long javaTime = DateTimeUtil.julianTimeToJavaTime(DateTimeUtil.toJulianTimestamp(tm)); |
| |
| if (tz != null) { |
| int offset = tz.getOffset(javaTime) - TimeZone.getDefault().getOffset(javaTime); |
| return new Timestamp(javaTime + offset); |
| } else { |
| return new Timestamp(javaTime); |
| } |
| } |
| |
| public static Time toJavaTime(TimeMeta tm, @Nullable TimeZone tz) { |
| if (tz != null) { |
| DateTimeUtil.toUserTimezone(tm, tz); |
| } |
| return new Time(tm.hours, tm.minutes, tm.secs); |
| } |
| |
| public static Date toJavaDate(TimeMeta tm, @Nullable TimeZone tz) { |
| if (tz != null) { |
| DateTimeUtil.toUserTimezone(tm, tz); |
| } |
| return new Date(tm.years - 1900, tm.monthOfYear - 1 , tm.dayOfMonth); |
| } |
| |
| /** |
| * Calculate julian timestamp. |
| * @param years |
| * @param months |
| * @param days |
| * @param hours |
| * @param minutes |
| * @param seconds |
| * @param fsec |
| * @return |
| */ |
| public static long toJulianTimestamp( |
| int years, int months, int days, int hours, int minutes, int seconds, int fsec) { |
| /* Julian day routines are not correct for negative Julian days_full */ |
| if (!isValidJulianDate(years, months, days)) { |
| throw new ValueOutOfRangeException("Out of Range Julian days_full"); |
| } |
| |
| long numJulianDays = date2j(years, months, days) - DateTimeConstants.POSTGRES_EPOCH_JDATE; |
| |
| return toJulianTimestamp(numJulianDays, hours, minutes, seconds, fsec); |
| } |
| |
| /** |
| * Calculate julian timestamp. |
| * @param numJulianDays |
| * @param hours |
| * @param minutes |
| * @param seconds |
| * @param fsec |
| * @return |
| */ |
| private static long toJulianTimestamp(long numJulianDays, int hours, int minutes, int seconds, int fsec) { |
| long time = toTime(hours, minutes, seconds, fsec); |
| |
| long timestamp = numJulianDays * DateTimeConstants.USECS_PER_DAY + time; |
| /* check for major overflow */ |
| if ((timestamp - time) / DateTimeConstants.USECS_PER_DAY != numJulianDays) { |
| throw new RuntimeException("Out of Range of Time"); |
| } |
| /* check for just-barely overflow (okay except time-of-day wraps) */ |
| /* caution: we want to allow 1999-12-31 24:00:00 */ |
| if ((timestamp < 0 && numJulianDays > 0) || (timestamp > 0 && numJulianDays < -1)) { |
| throw new RuntimeException("Out of Range of Date"); |
| } |
| |
| return timestamp; |
| } |
| |
| /** |
| * Calculate julian timestamp. |
| * If tm.TomeZone is set, the result value is adjusted. |
| * @param tm |
| * @return |
| */ |
| public static long toJulianTimestamp(TimeMeta tm) { |
| if (tm.timeZone != 0 && tm.timeZone != Integer.MAX_VALUE) { |
| int timeZoneSecs = tm.timeZone; |
| tm.timeZone = Integer.MAX_VALUE; |
| tm.plusMillis(0 - timeZoneSecs * 1000); |
| } |
| if (tm.dayOfYear > 0) { |
| return toJulianTimestamp(date2j(tm.years, 1, 1) + tm.dayOfYear - 1, tm.hours, tm.minutes, tm.secs, tm.fsecs); |
| } else { |
| return toJulianTimestamp(tm.years, tm.monthOfYear, tm.dayOfMonth, tm.hours, tm.minutes, tm.secs, tm.fsecs); |
| } |
| } |
| |
| /** |
| * Set TimeMeta's field value using given julian timestamp. |
| * Note that year is _not_ 1900-based, but is an explicit full value. |
| * Also, month is one-based, _not_ zero-based. |
| * Returns: |
| * 0 on success |
| * -1 on out of range |
| * |
| * If attimezone is NULL, the global timezone (including possibly brute forced |
| * timezone) will be used. |
| */ |
| public static void toJulianTimeMeta(long julianTimestamp, TimeMeta tm) { |
| long date; |
| long time; |
| |
| // TODO - If timezone is set, timestamp value should be adjusted here. |
| time = julianTimestamp; |
| |
| // TMODULO |
| date = time / DateTimeConstants.USECS_PER_DAY; |
| if (date != 0) { |
| time -= date * DateTimeConstants.USECS_PER_DAY; |
| } |
| if (time < 0) { |
| time += DateTimeConstants.USECS_PER_DAY; |
| date -= 1; |
| } |
| |
| /* add offset to go from J2000 back to standard Julian date */ |
| date += DateTimeConstants.POSTGRES_EPOCH_JDATE; |
| |
| /* Julian day routine does not work for negative Julian days_full */ |
| if (date < 0 || date > Integer.MAX_VALUE) { |
| throw new RuntimeException("Timestamp Out Of Scope"); |
| } |
| |
| j2date((int) date, tm); |
| date2j(time, tm); |
| } |
| |
| /** |
| * This method is originated from dt2time in timestamp.c of PostgreSQL. |
| * |
| * @param julianDate |
| * @return hour, min, sec, fsec |
| */ |
| public static void date2j(long julianDate, TimeMeta tm) { |
| long time = julianDate; |
| |
| tm.hours = (int) (time / DateTimeConstants.USECS_PER_HOUR); |
| time -= tm.hours * DateTimeConstants.USECS_PER_HOUR; |
| tm.minutes = (int) (time / DateTimeConstants.USECS_PER_MINUTE); |
| time -= tm.minutes * DateTimeConstants.USECS_PER_MINUTE; |
| tm.secs = (int) (time / DateTimeConstants.USECS_PER_SEC); |
| tm.fsecs = (int) (time - (tm.secs * DateTimeConstants.USECS_PER_SEC)); |
| } |
| |
| /** |
| * Decode date string which includes delimiters. |
| * |
| * This method is originated from DecodeDate() in datetime.c of PostgreSQL. |
| * @param str The date string like '2013-12-25'. |
| * @param fmask |
| * @param tmaskValue |
| * @param is2digits |
| * @param tm |
| */ |
| private static void decodeDate(String str, int fmask, AtomicInteger tmaskValue, AtomicBoolean is2digits, TimeMeta tm) { |
| |
| int idx = 0; |
| int nf = 0; |
| TokenField type = null; |
| int val = 0; |
| |
| AtomicInteger dmask = new AtomicInteger(0); |
| int tmask = tmaskValue.get(); |
| boolean haveTextMonth = false; |
| |
| int length = str.length(); |
| char[] dateStr = str.toCharArray(); |
| String[] fields = new String[MAXDATEFIELDS]; |
| |
| while(idx < length && nf < MAXDATEFIELDS) { |
| |
| /* skip field separators */ |
| while (idx < length && !Character.isLetterOrDigit(dateStr[idx])) { |
| idx++; |
| } |
| |
| if (idx == length) { |
| throw new IllegalArgumentException("BAD Format: " + str); |
| } |
| |
| int fieldStartIdx = idx; |
| int fieldLength = idx; |
| if (Character.isDigit(dateStr[idx])) { |
| while (idx < length && Character.isDigit(dateStr[idx])) { |
| idx++; |
| } |
| fieldLength = idx; |
| } else if (Character.isLetterOrDigit(dateStr[idx])) { |
| while (idx < length && Character.isLetterOrDigit(dateStr[idx])) { |
| idx++; |
| } |
| fieldLength = idx; |
| } |
| |
| fields[nf] = str.substring(fieldStartIdx, fieldLength); |
| nf++; |
| } |
| |
| /* look first for text fields, since that will be unambiguous month */ |
| for (int i = 0; i < nf; i++) { |
| if (Character.isLetter(fields[i].charAt(0))) { |
| DateToken dateToken = DateTimeConstants.dateTokenMap.get(fields[i].toLowerCase()); |
| type = dateToken.getType(); |
| |
| if (type == TokenField.IGNORE_DTF) { |
| continue; |
| } |
| |
| dmask.set(DateTimeConstants.DTK_M(type)); |
| switch (type) { |
| case MONTH: |
| tm.monthOfYear = type.getValue(); |
| haveTextMonth = true; |
| break; |
| |
| default: |
| throw new IllegalArgumentException("BAD Format: " + str); |
| } |
| if ((fmask & dmask.get()) != 0) { |
| throw new IllegalArgumentException("BAD Format: " + str); |
| } |
| |
| fmask |= dmask.get(); |
| tmask |= dmask.get(); |
| |
| /* mark this field as being completed */ |
| fields[i] = null; |
| } |
| } |
| |
| /* now pick up remaining numeric fields */ |
| for (int i = 0; i < nf; i++) { |
| if (fields[i] == null) { |
| continue; |
| } |
| |
| length = fields[i].length(); |
| if (length <= 0) { |
| throw new IllegalArgumentException("BAD Format: " + str); |
| } |
| |
| decodeNumber(length, fields[i], haveTextMonth, fmask, dmask, tm, new AtomicLong(0), is2digits); |
| |
| if ( (fmask & dmask.get()) != 0 ) { |
| throw new IllegalArgumentException("BAD Format: " + str); |
| } |
| fmask |= dmask.get(); |
| tmask |= dmask.get(); |
| } |
| |
| tmaskValue.set(tmask); |
| |
| if ((fmask & ~(DateTimeConstants.DTK_M(TokenField.DOY) | DateTimeConstants.DTK_M(TokenField.TZ))) != DateTimeConstants.DTK_DATE_M) { |
| throw new IllegalArgumentException("BAD Format: " + str); |
| } |
| } |
| |
| /** |
| * Decode time string which includes delimiters. |
| * Return 0 if okay, a DTERR code if not. |
| * |
| * Only check the lower limit on hours, since this same code can be |
| * used to represent time spans. |
| * @param str |
| * @param fmask |
| * @param range |
| * @param tmask |
| * @param tm |
| * @param fsec |
| */ |
| private static void decodeTime(String str, int fmask, int range, |
| AtomicInteger tmask, TimeMeta tm, AtomicLong fsec) { |
| StringBuilder cp = new StringBuilder(); |
| |
| tmask.set(DateTimeConstants.DTK_TIME_M); |
| |
| tm.hours = strtoi(str, 0, cp); |
| if (cp.charAt(0) != ':') { |
| throw new IllegalArgumentException("BAD Format: " + str); |
| } |
| |
| tm.minutes = strtoi(cp.toString(), 1, cp); |
| |
| if (cp.length() == 0) { |
| tm.secs = 0; |
| fsec.set(0); |
| /* If it's a MINUTE TO SECOND interval, take 2 fields as being mm:ss */ |
| if (range == (DateTimeConstants.INTERVAL_MASK(TokenField.MINUTE) | DateTimeConstants.INTERVAL_MASK(TokenField.SECOND))) { |
| tm.secs = tm.minutes; |
| tm.minutes = tm.hours; |
| tm.hours = 0; |
| } |
| } else if (cp.charAt(0) == '.') { |
| /* always assume mm:ss.sss is MINUTE TO SECOND */ |
| parseFractionalSecond(cp, fsec); |
| tm.secs = tm.minutes; |
| tm.minutes = tm.hours; |
| tm.hours = 0; |
| } |
| else if (cp.charAt(0) == ':') { |
| tm.secs = strtoi(cp.toString(), 1, cp); |
| if (cp.length() == 0){ |
| fsec.set(0); |
| } else if (cp.charAt(0) == '.') { |
| parseFractionalSecond(cp, fsec); |
| } else{ |
| throw new IllegalArgumentException("BAD Format: " + str); |
| } |
| } else { |
| throw new IllegalArgumentException("BAD Format: " + str); |
| } |
| |
| /* do a sanity check */ |
| if (tm.hours < 0 || tm.minutes < 0 || tm.minutes > DateTimeConstants.MINS_PER_HOUR - 1 || |
| tm.secs < 0 || tm.secs > DateTimeConstants.SECS_PER_MINUTE || |
| fsec.get() < 0 || |
| fsec.get() > DateTimeConstants.USECS_PER_SEC) { |
| throw new IllegalArgumentException("BAD Format: FIELD_OVERFLOW: " + str); |
| } |
| } |
| |
| /** |
| * Parse datetime string to julian time. |
| * The result is the UTC time basis. |
| * @param str |
| * @return |
| */ |
| public static long toJulianTimestamp(String str) { |
| TimeMeta tm = decodeDateTime(str, MAXDATEFIELDS); |
| return toJulianTimestamp(tm); |
| } |
| |
| |
| /** |
| * Parse datetime string to UTC-based julian time. |
| * The result julian time is adjusted by local timezone. |
| * |
| * @param timestampStr |
| * @param tz Local timezone. If it is NULL, UTC will be used by default. |
| * @return UTC-based julian time |
| */ |
| public static long toJulianTimestampWithTZ(String timestampStr, @Nullable TimeZone tz) { |
| long timestamp = DateTimeUtil.toJulianTimestamp(timestampStr); |
| TimeMeta tm = new TimeMeta(); |
| DateTimeUtil.toJulianTimeMeta(timestamp, tm); |
| if (tz != null) { |
| DateTimeUtil.toUTCTimezone(tm, tz); |
| } |
| return DateTimeUtil.toJulianTimestamp(tm); |
| } |
| |
| /** |
| * Parse datetime string to julian date. |
| * @param dateStr |
| * @return |
| */ |
| public static int toJulianDate(String dateStr) { |
| TimeMeta tm = DateTimeUtil.decodeDateTime(dateStr); |
| return DateTimeUtil.date2j(tm.years, tm.monthOfYear, tm.dayOfMonth); |
| } |
| |
| /** |
| * Parse datetime string to julian time. |
| * @param timeStr |
| * @return |
| */ |
| public static long toJulianTime(String timeStr) { |
| TimeMeta tm = DateTimeUtil.decodeDateTime(timeStr); |
| return DateTimeUtil.toTime(tm); |
| } |
| |
| public static TimeMeta decodeDateTime(String str) { |
| return decodeDateTime(str, MAXDATEFIELDS); |
| } |
| |
| /** |
| * Break string into tokens based on a date/time context. |
| * |
| * This method is originated form ParseDateTime() in datetime.c of PostgreSQL. |
| * |
| * @param str The input string |
| * @param maxFields |
| */ |
| public static TimeMeta decodeDateTime(String str, int maxFields) { |
| int idx = 0; |
| int nf = 0; |
| int length = str.length(); |
| char [] timeStr = str.toCharArray(); |
| String [] fields = new String[maxFields]; |
| TokenField[] fieldTypes = new TokenField[maxFields]; |
| |
| while (idx < length) { |
| |
| /* Ignore spaces between fields */ |
| if (Character.isSpaceChar(timeStr[idx])) { |
| idx++; |
| continue; |
| } |
| |
| /* Record start of current field */ |
| if (nf >= maxFields) { |
| throw new IllegalArgumentException("Too many fields"); |
| } |
| |
| int startIdx = idx; |
| |
| //January 8, 1999 |
| /* leading digit? then date or time */ |
| if (Character.isDigit(timeStr[idx])) { |
| idx++; |
| while (idx < length && Character.isDigit(timeStr[idx])) { |
| idx++; |
| } |
| |
| if (idx < length && timeStr[idx] == ':') { |
| fieldTypes[nf] = TokenField.DTK_TIME; |
| |
| while (idx <length && (Character.isDigit(timeStr[idx]) || timeStr[idx] == ':' || timeStr[idx] == '.')) { |
| idx++; |
| } |
| } |
| |
| /* date field? allow embedded text month */ |
| else if (idx < length && (timeStr[idx] == '-' || timeStr[idx] == '/' || timeStr[idx] == '.')) { |
| |
| /* save delimiting character to use later */ |
| char delim = timeStr[idx]; |
| idx++; |
| |
| /* second field is all digits? then no embedded text month */ |
| if (Character.isDigit(timeStr[idx])) { |
| fieldTypes[nf] = delim == '.' ? TokenField.DTK_NUMBER : TokenField.DTK_DATE; |
| |
| while (idx < length && Character.isDigit(timeStr[idx])) { |
| idx++; |
| } |
| |
| /* |
| * insist that the delimiters match to get a three-field |
| * date. |
| */ |
| if (idx < length && timeStr[idx] == delim) { |
| fieldTypes[nf] = TokenField.DTK_DATE; |
| idx++; |
| while (idx < length && (Character.isDigit(timeStr[idx]) || timeStr[idx] == delim)) { |
| idx++; |
| } |
| } |
| } else { |
| fieldTypes[nf] = TokenField.DTK_DATE; |
| while (idx < length && Character.isLetterOrDigit(timeStr[idx]) || timeStr[idx] == delim) { |
| idx++; |
| } |
| } |
| } else { |
| /* |
| * otherwise, number only and will determine year, month, day, or |
| * concatenated fields later... |
| */ |
| fieldTypes[nf] = TokenField.DTK_NUMBER; |
| } |
| } |
| |
| /* Leading decimal point? Then fractional seconds... */ |
| else if (timeStr[idx] == '.') { |
| idx++; |
| while (idx < length && Character.isDigit(timeStr[idx])) { |
| idx++; |
| continue; |
| } |
| fieldTypes[nf] = TokenField.DTK_NUMBER; |
| } |
| |
| // text? then date string, month, day of week, special, or timezone |
| else if (Character.isLetter(timeStr[idx])) { |
| boolean isDate; |
| idx++; |
| while (idx < length && Character.isLetter(timeStr[idx])) { |
| idx++; |
| } |
| |
| // Dates can have embedded '-', '/', or '.' separators. It could |
| // also be a timezone name containing embedded '/', '+', '-', '_', |
| // or ':' (but '_' or ':' can't be the first punctuation). If the |
| // next character is a digit or '+', we need to check whether what |
| // we have so far is a recognized non-timezone keyword --- if so, |
| // don't believe that this is the start of a timezone. |
| |
| isDate = false; |
| if (idx < length && (timeStr[idx] == '-' || timeStr[idx] == '/' || timeStr[idx] == '.')) { |
| isDate = true; |
| } else if (idx < length && (timeStr[idx] == '+' || Character.isDigit(timeStr[idx]))) { |
| // The original ParseDateTime handles this case. But, we currently omit this case. |
| throw new IllegalArgumentException("Cannot parse this datetime field " + str.substring(startIdx, idx)); |
| } |
| |
| if (isDate) { |
| fieldTypes[nf] = TokenField.DTK_DATE; |
| |
| do { |
| idx++; |
| } while (idx <length && (timeStr[idx] == '+' || timeStr[idx] == '-' || timeStr[idx] == '/' || |
| timeStr[idx] == '_' || timeStr[idx] == '.' || timeStr[idx] == ':' || |
| Character.isLetterOrDigit(timeStr[idx]))); |
| } else { |
| fieldTypes[nf] = TokenField.DTK_STRING; |
| } |
| } |
| |
| // sign? then special or numeric timezone |
| else if (timeStr[idx] == '+' || timeStr[idx] == '-') { |
| idx++; |
| |
| // soak up leading whitespace |
| while (idx < length && Character.isSpaceChar(timeStr[idx])) { |
| idx++; |
| } |
| |
| // numeric timezone? |
| // note that "DTK_TZ" could also be a signed float or yyyy-mm */ |
| if (idx < length && Character.isDigit(timeStr[idx])) { |
| fieldTypes[nf] = TokenField.DTK_TZ; |
| idx++; |
| |
| while (idx < length && (Character.isDigit(timeStr[idx]) || timeStr[idx] == ':' || timeStr[idx] == '.' || |
| timeStr[idx] == '-')) { |
| idx++; |
| } |
| } |
| /* special? */ |
| else if (idx < length && Character.isLetter(timeStr[idx])) { |
| fieldTypes[nf] = TokenField.DTK_SPECIAL; |
| idx++; |
| |
| while (idx < length && Character.isLetter(timeStr[idx])) { |
| idx++; |
| } |
| } else { |
| throw new IllegalArgumentException("BAD Format: " + str.substring(startIdx, idx)); |
| } |
| } |
| /* ignore other punctuation but use as delimiter */ |
| else if (isPunctuation(timeStr[idx])) { |
| idx++; |
| continue; |
| } else { // otherwise, something is not right... |
| throw new IllegalArgumentException("BAD datetime format: " + str.substring(startIdx, idx)); |
| } |
| |
| fields[nf] = str.substring(startIdx, idx); |
| nf++; |
| } |
| return decodeDateTime(fields, fieldTypes, nf); |
| } |
| |
| /** |
| * Fetch a fractional-second value with suitable error checking |
| * @param cp |
| * @param fsec |
| */ |
| public static void parseFractionalSecond(StringBuilder cp, AtomicLong fsec) { |
| /* Caller should always pass the start of the fraction part */ |
| double frac = strtod(cp.toString(), 1, cp); |
| fsec.set(Math.round(frac * 1000000)); |
| } |
| |
| /** |
| * Interpret string as a numeric timezone. |
| * |
| * Return 0 if okay (and set *tzp), a DTERR code if not okay. |
| * |
| * NB: this must *not* ereport on failure; see commands/variable.c. |
| * @param str |
| * @param tz |
| */ |
| public static void decodeTimezone(String str, AtomicInteger tz) { |
| int min = 0; |
| int sec = 0; |
| StringBuilder sb = new StringBuilder(); |
| |
| int strIndex = 0; |
| /* leading character must be "+" or "-" */ |
| if (str.charAt(strIndex) != '+' && str.charAt(strIndex) != '-') { |
| throw new IllegalArgumentException("BAD Format: " + str); |
| } |
| int hr = strtoi(str, 1, sb); |
| |
| /* explicit delimiter? */ |
| if (sb.length() > 0 && sb.charAt(0) == ':') { |
| min = strtoi(sb.toString(), 1, sb); |
| if (sb.charAt(0) == ':') { |
| sec = strtoi(sb.toString(), 1, sb); |
| } |
| } |
| /* otherwise, might have run things together... */ |
| else if (sb.length() == 0 && str.length() > 3) { |
| min = hr % 100; |
| hr = hr / 100; |
| /* we could, but don't, support a run-together hhmmss format */ |
| } else { |
| min = 0; |
| } |
| /* Range-check the values; see notes in datatype/timestamp.h */ |
| if (hr < 0 || hr > DateTimeConstants.MAX_TZDISP_HOUR) { |
| throw new IllegalArgumentException("BAD Format: TZDISP_OVERFLOW: " + str); |
| } |
| if (min < 0 || min >= DateTimeConstants.MINS_PER_HOUR) { |
| throw new IllegalArgumentException("BAD Format: TZDISP_OVERFLOW: " + str); |
| } |
| if (sec < 0 || sec >= DateTimeConstants.SECS_PER_MINUTE) { |
| throw new IllegalArgumentException("BAD Format: TZDISP_OVERFLOW: " + str); |
| } |
| |
| int tzValue = (hr * DateTimeConstants.MINS_PER_HOUR + min) * DateTimeConstants.SECS_PER_MINUTE + sec; |
| if (str.charAt(strIndex) == '-') { |
| tzValue = -tzValue; |
| } |
| tz.set(tzValue); |
| } |
| |
| /** |
| * Interpret plain numeric field as a date value in context. |
| * @param flen |
| * @param str |
| * @param haveTextMonth |
| * @param fmask |
| * @param tmaskValue |
| * @param tm |
| * @param fsec |
| * @param is2digits |
| */ |
| private static void decodeNumber(int flen, String str, boolean haveTextMonth, int fmask, |
| AtomicInteger tmaskValue, TimeMeta tm, AtomicLong fsec, AtomicBoolean is2digits) { |
| int val; |
| StringBuilder cp = new StringBuilder(); |
| |
| int tmask = 0; |
| tmaskValue.set(tmask); |
| |
| val = strtoi(str, 0, cp); |
| if (cp.toString().equals(str)) { |
| throw new IllegalArgumentException("BAD Format: " + str); |
| } |
| |
| if (cp.length() > 0 && cp.charAt(0) == '.') { |
| /* |
| * More than two digits before decimal point? Then could be a date or |
| * a run-together time: 2001.360 20011225 040506.789 |
| */ |
| if (cp.length() - str.length() > 2) { |
| decodeNumberField(flen, str, |
| (fmask | DateTimeConstants.DTK_DATE_M), |
| tmaskValue, tm, |
| fsec, is2digits); |
| return; |
| } |
| parseFractionalSecond(cp, fsec); |
| } |
| |
| // Special case for day of year |
| if (flen == 3 && (fmask & DateTimeConstants.DTK_DATE_M) == DateTimeConstants.DTK_M(TokenField.YEAR) && |
| val >= 1 && val <= 366) { |
| tmaskValue.set((DateTimeConstants.DTK_M(TokenField.DOY) | |
| DateTimeConstants.DTK_M(TokenField.MONTH) | |
| DateTimeConstants.DTK_M(TokenField.DAY))); |
| tm.dayOfYear = val; |
| // tm_mon and tm_mday can't actually be set yet ... |
| return; |
| } |
| |
| /* Switch based on what we have so far */ |
| int checkValue = fmask & DateTimeConstants.DTK_DATE_M; |
| if (checkValue == 0) { |
| /* |
| * Nothing so far; make a decision about what we think the input |
| * is. There used to be lots of heuristics here, but the |
| * consensus now is to be paranoid. It *must* be either |
| * YYYY-MM-DD (with a more-than-two-digit year field), or the |
| * field order defined by DateOrder. |
| */ |
| if (flen >= 3 || TajoConf.getDateOrder() == DateTimeConstants.DATEORDER_YMD) { |
| tmaskValue.set(DateTimeConstants.DTK_M(TokenField.YEAR)); |
| tm.years = val; |
| } else if (TajoConf.getDateOrder() == DateTimeConstants.DATEORDER_DMY) { |
| tmaskValue.set(DateTimeConstants.DTK_M(TokenField.DAY)); |
| tm.dayOfMonth = val; |
| } else { |
| tmaskValue.set(DateTimeConstants.DTK_M(TokenField.MONTH)); |
| tm.monthOfYear = val; |
| } |
| } else if (checkValue == (DateTimeConstants.DTK_M(TokenField.YEAR))) { |
| /* Must be at second field of YY-MM-DD */ |
| tmaskValue.set(DateTimeConstants.DTK_M(TokenField.MONTH)); |
| tm.monthOfYear = val; |
| } else if (checkValue == (DateTimeConstants.DTK_M(TokenField.MONTH))) { |
| if (haveTextMonth) { |
| /* |
| * We are at the first numeric field of a date that included a |
| * textual month name. We want to support the variants |
| * MON-DD-YYYY, DD-MON-YYYY, and YYYY-MON-DD as unambiguous |
| * inputs. We will also accept MON-DD-YY or DD-MON-YY in |
| * either DMY or MDY modes, as well as YY-MON-DD in YMD mode. |
| */ |
| if (flen >= 3 || TajoConf.getDateOrder() == DateTimeConstants.DATEORDER_YMD) { |
| tmaskValue.set(DateTimeConstants.DTK_M(TokenField.YEAR)); |
| tm.years = val; |
| } else { |
| tmaskValue.set(DateTimeConstants.DTK_M(TokenField.DAY)); |
| tm.dayOfMonth = val; |
| } |
| } else { |
| /* Must be at second field of MM-DD-YY */ |
| tmaskValue.set(DateTimeConstants.DTK_M(TokenField.DAY)); |
| tm.dayOfMonth = val; |
| } |
| } else if (checkValue == (DateTimeConstants.DTK_M(TokenField.YEAR) | DateTimeConstants.DTK_M(TokenField.MONTH))) { |
| if (haveTextMonth) { |
| /* Need to accept DD-MON-YYYY even in YMD mode */ |
| if (flen >= 3 && is2digits.get()) { |
| /* Guess that first numeric field is day was wrong */ |
| tmaskValue.set(DateTimeConstants.DTK_M(TokenField.DAY)); /* YEAR is already set */ |
| tm.dayOfMonth = tm.years; |
| tm.years = val; |
| is2digits.set(false); |
| } else { |
| tmaskValue.set(DateTimeConstants.DTK_M(TokenField.DAY)); |
| tm.dayOfMonth = val; |
| } |
| } else { |
| /* Must be at third field of YY-MM-DD */ |
| tmaskValue.set(DateTimeConstants.DTK_M(TokenField.DAY)); |
| tm.dayOfMonth = val; |
| } |
| } else if (checkValue == DateTimeConstants.DTK_M(TokenField.DAY)) { |
| /* Must be at second field of DD-MM-YY */ |
| tmaskValue.set(DateTimeConstants.DTK_M(TokenField.MONTH)); |
| tm.monthOfYear = val; |
| } else if (checkValue == (DateTimeConstants.DTK_M(TokenField.MONTH) | DateTimeConstants.DTK_M(TokenField.DAY))) { |
| /* Must be at third field of DD-MM-YY or MM-DD-YY */ |
| tmaskValue.set(DateTimeConstants.DTK_M(TokenField.YEAR)); |
| tm.years = val; |
| } else if (checkValue == (DateTimeConstants.DTK_M(TokenField.YEAR) | DateTimeConstants.DTK_M(TokenField.MONTH) | DateTimeConstants.DTK_M(TokenField.DAY))) { |
| /* we have all the date, so it must be a time field */ |
| decodeNumberField(flen, str, fmask, |
| tmaskValue, tm, |
| fsec, is2digits); |
| return; |
| |
| } else { |
| throw new IllegalArgumentException("BAD Format: " + str); |
| } |
| |
| /* |
| * When processing a year field, mark it for adjustment if it's only one |
| * or two digits. |
| */ |
| if (tmaskValue.get() == DateTimeConstants.DTK_M(TokenField.YEAR)) { |
| is2digits.set(flen <= 2); |
| } |
| } |
| |
| /** |
| * Interpret numeric string as a concatenated date or time field. |
| * |
| * Use the context of previously decoded fields to help with |
| * the interpretation. |
| * @param len |
| * @param str |
| * @param fmask |
| * @param tmaskValue |
| * @param tm |
| * @param fsec |
| * @param is2digits |
| * @return |
| */ |
| static TokenField decodeNumberField(int len, String str, int fmask, |
| AtomicInteger tmaskValue, TimeMeta tm, AtomicLong fsec, AtomicBoolean is2digits) { |
| /* |
| * Have a decimal point? Then this is a date or something with a seconds |
| * field... |
| */ |
| int index = str.indexOf('.'); |
| |
| if (index >= 0) { |
| String cp = str.substring(index + 1); |
| /* |
| * Can we use ParseFractionalSecond here? Not clear whether trailing |
| * junk should be rejected ... |
| */ |
| double frac = strtod(cp, 0, null); |
| fsec.set(Math.round(frac * 1000000)); |
| /* Now truncate off the fraction for further processing */ |
| len = str.length(); |
| } |
| /* No decimal point and no complete date yet? */ |
| else if ((fmask & DateTimeConstants.DTK_DATE_M) != DateTimeConstants.DTK_DATE_M) { |
| /* yyyymmdd? */ |
| if (len == 8) { |
| tmaskValue.set(DateTimeConstants.DTK_DATE_M); |
| |
| tm.dayOfMonth = Integer.parseInt(str.substring(6)); |
| tm.monthOfYear = Integer.parseInt(str.substring(4, 6)); |
| tm.years = Integer.parseInt(str.substring(0, 4)); |
| |
| return TokenField.DTK_DATE; |
| } |
| /* yymmdd? */ |
| else if (len == 6) { |
| tmaskValue.set(DateTimeConstants.DTK_DATE_M); |
| tm.dayOfMonth = Integer.parseInt(str.substring(4)); |
| tm.monthOfYear = Integer.parseInt(str.substring(2, 4)); |
| tm.years = Integer.parseInt(str.substring(0, 2)); |
| is2digits.set(true); |
| |
| return TokenField.DTK_DATE; |
| } |
| } |
| |
| /* not all time fields are specified? */ |
| if ((fmask & DateTimeConstants.DTK_TIME_M) != DateTimeConstants.DTK_TIME_M) { |
| /* hhmmss */ |
| if (len == 6) { |
| tmaskValue.set(DateTimeConstants.DTK_TIME_M); |
| tm.secs = Integer.parseInt(str.substring(4)); |
| tm.minutes = Integer.parseInt(str.substring(2, 4)); |
| tm.hours = Integer.parseInt(str.substring(0, 2)); |
| |
| return TokenField.DTK_TIME; |
| } |
| /* hhmm? */ |
| else if (len == 4) { |
| tmaskValue.set(DateTimeConstants.DTK_TIME_M); |
| tm.secs = 0; |
| tm.minutes = Integer.parseInt(str.substring(2, 4)); |
| tm.hours = Integer.parseInt(str.substring(0, 2)); |
| |
| return TokenField.DTK_TIME; |
| } |
| } |
| |
| throw new IllegalArgumentException("BAD Format: " + str); |
| } |
| |
| private static TimeMeta decodeDateTime(String[] fields, TokenField[] fieldTypes, int nf) { |
| int fmask = 0; |
| AtomicInteger tmask = new AtomicInteger(0); |
| int type; |
| |
| /* "prefix type" for ISO y2001m02d04 format */ |
| TokenField ptype = null; |
| |
| boolean haveTextMonth = false; |
| boolean isjulian = false; |
| AtomicBoolean is2digits = new AtomicBoolean(false); |
| boolean bc = false; |
| |
| int tzp = Integer.MAX_VALUE; |
| String namedTimeZone = null; |
| |
| StringBuilder sb = new StringBuilder(); |
| // We'll insist on at least all of the date fields, but initialize the |
| // remaining fields in case they are not set later... |
| TokenField dtype = TokenField.DTK_DATE; |
| TokenField mer = null; |
| |
| TimeMeta tm = new TimeMeta(); |
| TimeMeta cur_tm = new TimeMeta(); |
| |
| AtomicLong fsec = new AtomicLong(); |
| AtomicInteger tz = new AtomicInteger(Integer.MAX_VALUE); |
| |
| // don't know daylight savings time status apriori */ |
| tm.isDST = false; |
| |
| for (int i = 0; i < nf; i++) { |
| if (fieldTypes[i] == null) { |
| continue; |
| } |
| switch (fieldTypes[i]) { |
| case DTK_DATE: |
| /*** |
| * Integral julian day with attached time zone? |
| * All other forms with JD will be separated into |
| * distinct fields, so we handle just this case here. |
| ***/ |
| if (ptype == TokenField.DTK_JULIAN) { |
| int val; |
| |
| if (tzp == Integer.MAX_VALUE) { |
| throw new IllegalArgumentException("BAD Format: " + fields[i]); |
| } |
| |
| val = strtoi(fields[i], 0, sb); |
| |
| date2j(val, tm); |
| isjulian = true; |
| |
| /* Get the time zone from the end of the string */ |
| decodeTimezone(sb.toString(), tz); |
| |
| tmask.set(DateTimeConstants.DTK_DATE_M | DateTimeConstants.DTK_TIME_M | DateTimeConstants.DTK_M(TokenField.TZ)); |
| ptype = null; |
| break; |
| } |
| /*** |
| * Already have a date? Then this might be a time zone name |
| * with embedded punctuation (e.g. "America/New_York") or a |
| * run-together time with trailing time zone (e.g. hhmmss-zz). |
| * - thomas 2001-12-25 |
| * |
| * We consider it a time zone if we already have month & day. |
| * This is to allow the form "mmm dd hhmmss tz year", which |
| * we've historically accepted. |
| ***/ |
| else if (ptype != null || |
| ((fmask & (DateTimeConstants.DTK_M(TokenField.MONTH) | DateTimeConstants.DTK_M(TokenField.DAY))) == |
| (DateTimeConstants.DTK_M(TokenField.MONTH) | DateTimeConstants.DTK_M(TokenField.DAY)))) |
| { |
| /* No time zone accepted? Then quit... */ |
| if (tzp == Integer.MAX_VALUE) { |
| throw new IllegalArgumentException("BAD Format: " + fields[i]); |
| } |
| |
| if (Character.isDigit(fields[i].charAt(0)) || ptype != null) { |
| if (ptype != null) { |
| /* Sanity check; should not fail this test */ |
| if (ptype != TokenField.DTK_TIME) { |
| throw new IllegalArgumentException("BAD Format: " + fields[i]); |
| } |
| ptype = null; |
| } |
| |
| /* |
| * Starts with a digit but we already have a time |
| * field? Then we are in trouble with a date and time |
| * already... |
| */ |
| if ((fmask & DateTimeConstants.DTK_TIME_M) == DateTimeConstants.DTK_TIME_M) { |
| throw new IllegalArgumentException("BAD Format: " + fields[i]); |
| } |
| |
| int index = fields[i].indexOf("-"); |
| if (index < 0) { |
| throw new IllegalArgumentException("BAD Format: " + fields[i]); |
| } |
| |
| /* Get the time zone from the end of the string */ |
| decodeTimezone(fields[i].substring(index + 1), tz); |
| |
| /* |
| * Then read the rest of the field as a concatenated |
| * time |
| */ |
| decodeNumberField(fields[i].length(), fields[i], |
| fmask, |
| tmask, tm, |
| fsec, is2digits); |
| |
| /* |
| * modify tmask after returning from |
| * DecodeNumberField() |
| */ |
| tmask.set(tmask.get() | DateTimeConstants.DTK_M(TokenField.TZ)); |
| } |
| else { |
| namedTimeZone = pg_tzset(fields[i]); |
| if (namedTimeZone == null) { |
| /* |
| * We should return an error code instead of |
| * ereport'ing directly, but then there is no way |
| * to report the bad time zone name. |
| */ |
| throw new IllegalArgumentException("BAD Format: time zone \"%s\" not recognized: " + fields[i]); |
| } |
| /* we'll apply the zone setting below */ |
| tmask.set(DateTimeConstants.DTK_M(TokenField.TZ)); |
| } |
| } else { |
| decodeDate(fields[i], fmask, tmask, is2digits, tm); |
| } |
| break; |
| |
| case DTK_TIME: |
| decodeTime(fields[i], (fmask | DateTimeConstants.DTK_DATE_M), |
| DateTimeConstants.INTERVAL_FULL_RANGE, |
| tmask, tm, fsec); |
| break; |
| |
| case DTK_TZ: { |
| decodeTimezone(fields[i], tz); |
| tmask.set(DateTimeConstants.DTK_M(TokenField.TZ)); |
| break; |
| } |
| |
| case DTK_NUMBER: |
| |
| /* |
| * Was this an "ISO date" with embedded field labels? An |
| * example is "y2001m02d04" - thomas 2001-02-04 |
| */ |
| if (ptype != null) { |
| int val = strtoi(fields[i], 0, sb); |
| |
| /* |
| * only a few kinds are allowed to have an embedded |
| * decimal |
| */ |
| if (sb.length() == 0) { |
| continue; |
| } |
| if (sb.charAt(0) == '.') { |
| switch (ptype) { |
| case DTK_JULIAN: |
| case DTK_TIME: |
| case DTK_SECOND: |
| break; |
| default: |
| throw new IllegalArgumentException("BAD Format: " + fields[i]); |
| } |
| } else { |
| throw new IllegalArgumentException("BAD Format: " + fields[i]); |
| } |
| switch (ptype) { |
| case DTK_YEAR: |
| tm.years = val; |
| tmask.set(DateTimeConstants.DTK_M(TokenField.YEAR)); |
| break; |
| |
| case DTK_MONTH: |
| |
| /* |
| * already have a month and hour? then assume |
| * minutes |
| */ |
| if ((fmask & DateTimeConstants.DTK_M(TokenField.MONTH)) != 0 && |
| (fmask & DateTimeConstants.DTK_M(TokenField.HOUR)) != 0) { |
| tm.minutes = val; |
| tmask.set(DateTimeConstants.DTK_M(TokenField.MINUTE)); |
| } |
| else { |
| tm.monthOfYear = val; |
| tmask.set(DateTimeConstants.DTK_M(TokenField.MONTH)); |
| } |
| break; |
| |
| case DTK_DAY: |
| tm.dayOfMonth = val; |
| tmask.set(DateTimeConstants.DTK_M(TokenField.DAY)); |
| break; |
| |
| case DTK_HOUR: |
| tm.hours = val; |
| tmask.set(DateTimeConstants.DTK_M(TokenField.HOUR)); |
| break; |
| |
| case DTK_MINUTE: |
| tm.minutes = val; |
| tmask.set(DateTimeConstants.DTK_M(TokenField.MINUTE)); |
| break; |
| |
| case DTK_SECOND: |
| tm.secs = val; |
| tmask.set(DateTimeConstants.DTK_M(TokenField.SECOND)); |
| if (sb.charAt(0) == '.') { |
| parseFractionalSecond(sb, fsec); |
| tmask.set(DateTimeConstants.DTK_ALL_SECS_M); |
| } |
| break; |
| case DTK_TZ: |
| tmask.set(DateTimeConstants.DTK_M(TokenField.TZ)); |
| decodeTimezone(fields[i], tz); |
| break; |
| |
| case DTK_JULIAN: |
| /* previous field was a label for "julian date" */ |
| if (val < 0) { |
| throw new IllegalArgumentException("BAD Format: FIELD_OVERFLOW: " + fields[i]); |
| } |
| tmask.set(DateTimeConstants.DTK_DATE_M); |
| date2j(val, tm); |
| isjulian = true; |
| |
| /* fractional Julian Day? */ |
| if (sb.charAt(0) == '.') { |
| double time = strtod(sb.toString(), 0, sb); |
| |
| time *= DateTimeConstants.USECS_PER_DAY; |
| date2j((long)time, tm); |
| tmask.set(tmask.get() | DateTimeConstants.DTK_TIME_M); |
| } |
| break; |
| |
| case DTK_TIME: |
| /* previous field was "t" for ISO time */ |
| decodeNumberField(fields[i].length(), fields[i], |
| (fmask | DateTimeConstants.DTK_DATE_M), |
| tmask, tm, |
| fsec, is2digits); |
| if (tmask.get() != DateTimeConstants.DTK_TIME_M) { |
| throw new IllegalArgumentException("BAD Format: FIELD_OVERFLOW: " + fields[i]); |
| } |
| break; |
| |
| default: |
| throw new IllegalArgumentException("BAD Format: " + fields[i]); |
| } |
| |
| ptype = null; |
| dtype = TokenField.DTK_DATE; |
| } else { |
| int flen = fields[i].length(); |
| int index = fields[i].indexOf("."); |
| String cp = null; |
| if (index > 0) { |
| cp = fields[i].substring(index + 1); |
| } |
| |
| /* Embedded decimal and no date yet? */ |
| if (cp != null && ((fmask & DateTimeConstants.DTK_DATE_M) == 0 )) { |
| decodeDate(fields[i], fmask, |
| tmask, is2digits, tm); |
| } |
| /* embedded decimal and several digits before? */ |
| else if (cp != null && flen - cp.length() > 2) { |
| /* |
| * Interpret as a concatenated date or time Set the |
| * type field to allow decoding other fields later. |
| * Example: 20011223 or 040506 |
| */ |
| decodeNumberField(flen, fields[i], fmask, |
| tmask, tm, |
| fsec, is2digits); |
| } |
| else if (flen > 4) { |
| decodeNumberField(flen, fields[i], fmask, |
| tmask, tm, |
| fsec, is2digits); |
| } |
| /* otherwise it is a single date/time field... */ |
| else { |
| decodeNumber(flen, fields[i], |
| haveTextMonth, fmask, |
| tmask, tm, |
| fsec, is2digits); |
| } |
| } |
| break; |
| case DTK_STRING: |
| case DTK_SPECIAL: |
| DateToken dateToken = DateTimeConstants.dateTokenMap.get(fields[i].toLowerCase()); |
| if (dateToken == null) { |
| throw new IllegalArgumentException("BAD Format: " + fields[i]); |
| } |
| tmask.set(DateTimeConstants.DTK_M(dateToken.getType())); |
| switch (dateToken.getType()) { |
| case RESERV: |
| switch(dateToken.getValueType()) { |
| case DTK_CURRENT: |
| throw new IllegalArgumentException("BAD Format: date/time value \"current\" is no longer supported" + fields[i]); |
| |
| case DTK_NOW: |
| tmask.set(DateTimeConstants.DTK_DATE_M | DateTimeConstants.DTK_TIME_M | DateTimeConstants.DTK_M(TokenField.TZ)); |
| dtype = TokenField.DTK_DATE; |
| date2j(javaTimeToJulianTime(System.currentTimeMillis()), tm); |
| break; |
| |
| case DTK_YESTERDAY: |
| tmask.set(DateTimeConstants.DTK_DATE_M); |
| dtype = TokenField.DTK_DATE; |
| date2j(javaTimeToJulianTime(System.currentTimeMillis()), tm); |
| tm.plusDays(-1); |
| break; |
| |
| case DTK_TODAY: |
| tmask.set(DateTimeConstants.DTK_DATE_M); |
| dtype = TokenField.DTK_DATE; |
| date2j(javaTimeToJulianTime(System.currentTimeMillis()), cur_tm); |
| tm.years = cur_tm.years; |
| tm.monthOfYear = cur_tm.monthOfYear; |
| tm.dayOfMonth = cur_tm.dayOfMonth; |
| break; |
| |
| case DTK_TOMORROW: |
| tmask.set(DateTimeConstants.DTK_DATE_M); |
| dtype = TokenField.DTK_DATE; |
| date2j(javaTimeToJulianTime(System.currentTimeMillis()), tm); |
| tm.plusDays(1); |
| break; |
| |
| case DTK_ZULU: |
| tmask.set(DateTimeConstants.DTK_TIME_M | DateTimeConstants.DTK_M(TokenField.TZ)); |
| dtype = TokenField.DTK_DATE; |
| tm.hours = 0; |
| tm.minutes = 0; |
| tm.secs = 0; |
| break; |
| |
| default: |
| dtype = dateToken.getValueType(); |
| } |
| break; |
| |
| case MONTH: |
| /* |
| * already have a (numeric) month? then see if we can |
| * substitute... |
| */ |
| if ((fmask & DateTimeConstants.DTK_M(TokenField.MONTH)) != 0 && !haveTextMonth && |
| (fmask & DateTimeConstants.DTK_M(TokenField.DAY)) == 0 && |
| tm.monthOfYear >= 1 && tm.monthOfYear <= 31) { |
| tm.dayOfMonth = tm.monthOfYear; |
| tmask.set(DateTimeConstants.DTK_M(TokenField.DAY)); |
| } |
| haveTextMonth = true; |
| tm.monthOfYear = dateToken.getValue(); |
| break; |
| |
| case DTZMOD: |
| |
| /* |
| * daylight savings time modifier (solves "MET DST" |
| * syntax) |
| */ |
| tmask.set(tmask.get() | DateTimeConstants.DTK_M(TokenField.DTZ)); |
| tm.isDST = true; |
| if (tzp == Integer.MAX_VALUE) { |
| throw new IllegalArgumentException("BAD Format: " + fields[i]); |
| } |
| tzp += dateToken.getValue() * DateTimeConstants.MINS_PER_HOUR; |
| break; |
| |
| case DTZ: |
| |
| /* |
| * set mask for TZ here _or_ check for DTZ later when |
| * getting default timezone |
| */ |
| tmask.set(tmask.get() | DateTimeConstants.DTK_M(TokenField.TZ)); |
| tm.isDST = true; |
| if (tzp == Integer.MAX_VALUE) { |
| throw new IllegalArgumentException("BAD Format: " + fields[i]); |
| } |
| tzp = dateToken.getValue() * DateTimeConstants.MINS_PER_HOUR; |
| break; |
| |
| case TZ: |
| tm.isDST = false; |
| if (tzp == Integer.MAX_VALUE) { |
| throw new IllegalArgumentException("BAD Format: " + fields[i]); |
| } |
| tzp = dateToken.getValue() * DateTimeConstants.MINS_PER_HOUR; |
| break; |
| |
| case IGNORE_DTF: |
| break; |
| |
| case AMPM: |
| mer = dateToken.getValueType(); |
| break; |
| |
| case ADBC: |
| bc = (dateToken.getValueType() == TokenField.BC); |
| break; |
| |
| case DOW: |
| tm.dayOfWeek = dateToken.getValue(); |
| break; |
| |
| case UNITS: |
| tmask.set(0); |
| ptype = dateToken.getValueType(); |
| break; |
| |
| case ISOTIME: |
| |
| /* |
| * This is a filler field "t" indicating that the next |
| * field is time. Try to verify that this is sensible. |
| */ |
| tmask.set(0); |
| |
| /* No preceding date? Then quit... */ |
| if ((fmask & DateTimeConstants.DTK_DATE_M) != DateTimeConstants.DTK_DATE_M) { |
| throw new IllegalArgumentException("BAD Format: " + fields[i]); |
| } |
| |
| /*** |
| * We will need one of the following fields: |
| * DTK_NUMBER should be hhmmss.fff |
| * DTK_TIME should be hh:mm:ss.fff |
| * DTK_DATE should be hhmmss-zz |
| ***/ |
| if (i >= nf - 1 || |
| (fieldTypes[i + 1] != TokenField.DTK_NUMBER && |
| fieldTypes[i + 1] != TokenField.DTK_TIME && |
| fieldTypes[i + 1] != TokenField.DTK_DATE)) { |
| throw new IllegalArgumentException("BAD Format: " + fields[i]); |
| } |
| |
| ptype = dateToken.getValueType(); |
| break; |
| |
| case UNKNOWN_FIELD: |
| |
| /* |
| * Before giving up and declaring error, check to see |
| * if it is an all-alpha timezone name. |
| */ |
| namedTimeZone = pg_tzset(fields[i]); |
| if (namedTimeZone == null) { |
| throw new IllegalArgumentException("BAD Format: " + fields[i]); |
| } |
| /* we'll apply the zone setting below */ |
| tmask.set(DateTimeConstants.DTK_M(TokenField.TZ)); |
| break; |
| |
| default: |
| throw new IllegalArgumentException("BAD Format: " + fields[i]); |
| } |
| break; |
| } |
| if ((tmask.get() & fmask) != 0) { |
| throw new IllegalArgumentException("BAD Format: " + fields[i]); |
| } |
| fmask |= tmask.get(); |
| } /* end loop over fields */ |
| |
| tm.fsecs = fsec.intValue(); |
| tm.timeZone = tz.get(); |
| /* do final checking/adjustment of Y/M/D fields */ |
| validateDate(fmask, isjulian, is2digits.get(), bc, tm); |
| |
| /* handle AM/PM */ |
| if (mer != null && mer != TokenField.HR24 && tm.hours > DateTimeConstants.HOURS_PER_DAY / 2) { |
| throw new IllegalArgumentException("BAD Format: overflow hour: " + tm.hours); |
| } |
| if (mer != null && mer == TokenField.AM && tm.hours == DateTimeConstants.HOURS_PER_DAY / 2) { |
| tm.hours = 0; |
| } else if (mer != null && mer == TokenField.PM && tm.hours != DateTimeConstants.HOURS_PER_DAY / 2) { |
| tm.hours += DateTimeConstants.HOURS_PER_DAY / 2; |
| } |
| /* do additional checking for full date specs... */ |
| if (dtype == TokenField.DTK_DATE) { |
| if ((fmask & DateTimeConstants.DTK_DATE_M) != DateTimeConstants.DTK_DATE_M) { |
| if ((fmask & DateTimeConstants.DTK_TIME_M) == DateTimeConstants.DTK_TIME_M) { |
| return tm; |
| } |
| throw new IllegalArgumentException("BAD Format: " + tm); |
| } |
| |
| /* |
| * If we had a full timezone spec, compute the offset (we could not do |
| * it before, because we need the date to resolve DST status). |
| */ |
| if (namedTimeZone != null) { |
| /* daylight savings time modifier disallowed with full TZ */ |
| if ( (fmask & DateTimeConstants.DTK_M(TokenField.DTZMOD)) != 0 ) { |
| throw new IllegalArgumentException("BAD Format: " + tm); |
| } |
| } |
| } |
| |
| return tm; |
| } |
| |
| private static String pg_tzset(String str) { |
| //TODO implements logic |
| return null; |
| } |
| |
| /** |
| * Check valid year/month/day values, handle BC and DOY cases |
| * Return 0 if okay, a DTERR code if not. |
| * @param fmask |
| * @param isjulian |
| * @param is2digits |
| * @param bc |
| * @param tm |
| * @return |
| */ |
| private static int validateDate(int fmask, boolean isjulian, boolean is2digits, boolean bc, TimeMeta tm) { |
| if ( (fmask & DateTimeConstants.DTK_M(TokenField.YEAR)) != 0 ) { |
| if (isjulian) { |
| /* tm_year is correct and should not be touched */ |
| } else if (bc) { |
| /* there is no year zero in AD/BC notation */ |
| if (tm.years <= 0) { |
| throw new IllegalArgumentException("BAD Format: year overflow:" + tm.years); |
| } |
| /* internally, we represent 1 BC as year zero, 2 BC as -1, etc */ |
| tm.years = -(tm.years - 1); |
| } |
| else if (is2digits) { |
| /* process 1 or 2-digit input as 1970-2069 AD, allow '0' and '00' */ |
| if (tm.years < 0) { /* just paranoia */ |
| throw new IllegalArgumentException("BAD Format: year overflow:" + tm.years); |
| } |
| if (tm.years < 70) { |
| tm.years += 2000; |
| } else if (tm.years < 100) { |
| tm.years += 1900; |
| } |
| } |
| else { |
| /* there is no year zero in AD/BC notation */ |
| if (tm.years <= 0) { |
| throw new IllegalArgumentException("BAD Format: year overflow:" + tm.years); |
| } |
| } |
| } |
| |
| /* now that we have correct year, decode DOY */ |
| if ( (fmask & DateTimeConstants.DTK_M(TokenField.DOY)) != 0 ) { |
| j2date(date2j(tm.years, 1, 1) + tm.dayOfYear - 1, tm); |
| } |
| |
| /* check for valid month */ |
| if ( (fmask & DateTimeConstants.DTK_M(TokenField.MONTH)) != 0 ) { |
| if (tm.monthOfYear < 1 || tm.monthOfYear > DateTimeConstants.MONTHS_PER_YEAR) { |
| throw new IllegalArgumentException("BAD Format: month overflow:" + tm.monthOfYear); |
| } |
| } |
| |
| /* minimal check for valid day */ |
| if ( (fmask & DateTimeConstants.DTK_M(TokenField.DAY)) != 0 ) { |
| if (tm.dayOfMonth < 1 || tm.dayOfMonth > 31) { |
| throw new IllegalArgumentException("BAD Format: day overflow:" + tm.dayOfMonth); |
| } |
| } |
| |
| if ((fmask & DateTimeConstants.DTK_DATE_M) == DateTimeConstants.DTK_DATE_M) { |
| /* |
| * Check for valid day of month, now that we know for sure the month |
| * and year. Note we don't use MD_FIELD_OVERFLOW here, since it seems |
| * unlikely that "Feb 29" is a YMD-order error. |
| */ |
| boolean leapYear = isLeapYear(tm.years); |
| if (tm.dayOfMonth > DateTimeConstants.DAY_OF_MONTH[leapYear ? 1: 0][tm.monthOfYear - 1]) |
| throw new IllegalArgumentException("BAD Format: day overflow:" + tm.dayOfMonth); |
| } |
| |
| return 0; |
| } |
| |
| public static int strtoi(String str, int startIndex, StringBuilder sb) { |
| sb.setLength(0); |
| char[] chars = str.toCharArray(); |
| |
| int index = startIndex; |
| for (; index < chars.length; index++) { |
| if (!Character.isDigit(chars[index])) { |
| break; |
| } |
| } |
| |
| int val = index == startIndex ? 0 : Integer.parseInt(str.substring(startIndex, index)); |
| sb.append(chars, index, chars.length - index); |
| |
| return val; |
| } |
| |
| public static long strtol(String str, int startIndex, StringBuilder sb) { |
| sb.setLength(0); |
| char[] chars = str.toCharArray(); |
| |
| int index = startIndex; |
| for (; index < chars.length; index++) { |
| if (!Character.isDigit(chars[index])) { |
| break; |
| } |
| } |
| |
| long val = index == startIndex ? 0 : Long.parseLong(str.substring(startIndex, index)); |
| sb.append(chars, index, chars.length - index); |
| |
| return val; |
| } |
| |
| public static double strtod(String str, int strIndex, StringBuilder sb) { |
| if (sb != null) { |
| sb.setLength(0); |
| } |
| char[] chars = str.toCharArray(); |
| |
| int index = strIndex; |
| for (; index < chars.length; index++) { |
| if (!Character.isDigit(chars[index])) { |
| break; |
| } |
| } |
| |
| double val = Double.parseDouble(str.substring(0, index)); |
| if (sb != null) { |
| sb.append(chars, index, chars.length - index); |
| } |
| return val; |
| } |
| |
| /** |
| * Check whether it is a punctuation character or not. |
| * @param c The character to be checked |
| * @return True if it is a punctuation character. Otherwise, false. |
| */ |
| public static boolean isPunctuation(char c) { |
| return ((c >= '!' && c <= '/') || |
| (c >= ':' && c <= '@') || |
| (c >= '[' && c <= '`') || |
| (c >= '{' && c <= '~')); |
| } |
| |
| public static String toString(TimeMeta tm) { |
| return encodeDateTime(tm, DateStyle.ISO_DATES); |
| } |
| |
| /** |
| * Encode date and time interpreted as local time. |
| * |
| * tm and fsec are the value to encode, print_tz determines whether to include |
| * a time zone (the difference between timestamp and timestamptz types), tz is |
| * the numeric time zone offset, tzn is the textual time zone, which if |
| * specified will be used instead of tz by some styles, style is the date |
| * style, str is where to write the output. |
| * |
| * Supported date styles: |
| * Postgres - day mon hh:mm:ss yyyy tz |
| * SQL - mm/dd/yyyy hh:mm:ss.ss tz |
| * ISO - yyyy-mm-dd hh:mm:ss+/-tz |
| * German - dd.mm.yyyy hh:mm:ss tz |
| * XSD - yyyy-mm-ddThh:mm:ss.ss+/-tz |
| * |
| * This method is originated from EncodeDateTime of datetime.c of PostgreSQL. |
| * @param tm |
| * @param style |
| * @return |
| */ |
| public static String encodeDateTime(TimeMeta tm, DateStyle style) { |
| |
| StringBuilder sb = new StringBuilder(); |
| switch (style) { |
| |
| case ISO_DATES: |
| case XSO_DATES: |
| if (style == DateTimeConstants.DateStyle.ISO_DATES) { |
| sb.append(String.format("%04d-%02d-%02d %02d:%02d:", |
| (tm.years > 0) ? tm.years : -(tm.years - 1), |
| tm.monthOfYear, tm.dayOfMonth, tm.hours, tm.minutes)); |
| } else { |
| sb.append(String.format("%04d-%02d-%02dT%02d:%02d:", |
| (tm.years > 0) ? tm.years : -(tm.years - 1), |
| tm.monthOfYear, tm.dayOfMonth, tm.hours, tm.minutes)); |
| } |
| |
| appendSecondsToEncodeOutput(sb, tm.secs, tm.fsecs, 6, true); |
| if (tm.timeZone != 0 && tm.timeZone != Integer.MAX_VALUE) { |
| sb.append(getDisplayTimeZoneOffset(tm.timeZone)); |
| } |
| if (tm.years <= 0) { |
| sb.append(" BC"); |
| } |
| break; |
| |
| case SQL_DATES: |
| // Compatible with Oracle/Ingres date formats |
| |
| } |
| |
| return sb.toString(); |
| } |
| |
| public static String encodeDate(TimeMeta tm, DateStyle style) { |
| return encodeDate(tm.years, tm.monthOfYear, tm.dayOfMonth, style); |
| } |
| |
| public static String encodeDate(int years, int monthOfYear, int dayOfMonth, DateStyle style) { |
| StringBuilder sb = new StringBuilder(); |
| switch (style) { |
| case ISO_DATES: |
| case XSO_DATES: |
| case SQL_DATES: |
| // Compatible with Oracle/Ingres date formats |
| default: |
| sb.append(String.format("%04d-%02d-%02d", |
| (years > 0) ? years : -(years - 1), |
| monthOfYear, dayOfMonth)); |
| } |
| |
| return sb.toString(); |
| } |
| |
| public static String encodeTime(TimeMeta tm, DateStyle style) { |
| StringBuilder sb = new StringBuilder(); |
| switch (style) { |
| |
| case ISO_DATES: |
| case XSO_DATES: |
| case SQL_DATES: |
| // Compatible with Oracle/Ingres date formats |
| default : |
| sb.append(String.format("%02d:%02d:", tm.hours, tm.minutes)); |
| appendSecondsToEncodeOutput(sb, tm.secs, tm.fsecs, 6, true); |
| if (tm.timeZone != 0 && tm.timeZone != Integer.MAX_VALUE) { |
| sb.append(getDisplayTimeZoneOffset(tm.timeZone)); |
| } |
| break; |
| } |
| |
| return sb.toString(); |
| } |
| |
| /** |
| * Append sections and fractional seconds (if any) at *cp. |
| * precision is the max number of fraction digits, fillzeros says to |
| * pad to two integral-seconds digits. |
| * Note that any sign is stripped from the input seconds values. |
| * |
| * This method is originated form AppendSeconds in datetime.c of PostgreSQL. |
| */ |
| public static void appendSecondsToEncodeOutput( |
| StringBuilder sb, int sec, int fsec, int precision, boolean fillzeros) { |
| if (fsec == 0) { |
| if (fillzeros) |
| sb.append(String.format("%02d", Math.abs(sec))); |
| else |
| sb.append(String.format("%d", Math.abs(sec))); |
| } else { |
| if (fillzeros) { |
| sb.append(String.format("%02d", Math.abs(sec))); |
| } else { |
| sb.append(String.format("%d", Math.abs(sec))); |
| } |
| |
| if (precision > MAX_FRACTION_LENGTH) { |
| precision = MAX_FRACTION_LENGTH; |
| } |
| |
| if (precision > 0) { |
| char[] fracChars = String.valueOf(fsec).toCharArray(); |
| char[] resultChars = new char[MAX_FRACTION_LENGTH]; |
| |
| int numFillZero = MAX_FRACTION_LENGTH - fracChars.length; |
| for (int i = 0, fracIdx = 0; i < MAX_FRACTION_LENGTH; i++) { |
| if (i < numFillZero) { |
| resultChars[i] = '0'; |
| } else { |
| resultChars[i] = fracChars[fracIdx]; |
| fracIdx++; |
| } |
| } |
| sb.append(".").append(resultChars, 0, precision); |
| } |
| trimTrailingZeros(sb); |
| } |
| } |
| |
| /** |
| * ... resulting from printing numbers with full precision. |
| * |
| * Before Postgres 8.4, this always left at least 2 fractional digits, |
| * but conversations on the lists suggest this isn't desired |
| * since showing '0.10' is misleading with values of precision(1). |
| * |
| * This method is originated form AppendSeconds in datetime.c of PostgreSQL. |
| * @param sb |
| */ |
| public static void trimTrailingZeros(StringBuilder sb) { |
| int len = sb.length(); |
| while (len > 1 && sb.charAt(len - 1) == '0' && sb.charAt(len - 2) != '.') { |
| len--; |
| sb.setLength(len); |
| } |
| } |
| |
| /** |
| * Return the Julian day which corresponds to the first day (Monday) of the given ISO 8601 year and week. |
| * Julian days_full are used to convert between ISO week dates and Gregorian dates. |
| * |
| * This method is originated form AppendSeconds in timestamp.c of PostgreSQL. |
| * @param year |
| * @param week |
| * @return |
| */ |
| public static int isoweek2j(int year, int week) { |
| /* fourth day of current year */ |
| int day4 = date2j(year, 1, 4); |
| |
| /* day0 == offset to first day of week (Monday) */ |
| int day0 = j2day(day4 - 1); |
| |
| return ((week - 1) * 7) + (day4 - day0); |
| } |
| |
| /** |
| * Convert ISO week of year number to date. |
| * The year field must be specified with the ISO year! |
| * karel 2000/08/07 |
| * |
| * This method is originated form AppendSeconds in timestamp.c of PostgreSQL. |
| * @param woy |
| * @param tm |
| */ |
| public static void isoweek2date(int woy, TimeMeta tm) { |
| j2date(isoweek2j(tm.years, woy), tm); |
| } |
| |
| /** |
| * Convert an ISO 8601 week date (ISO year, ISO week) into a Gregorian date. |
| * Gregorian day of week sent so weekday strings can be supplied. |
| * Populates year, mon, and mday with the correct Gregorian values. |
| * year must be passed in as the ISO year. |
| * |
| * This method is originated form AppendSeconds in timestamp.c of PostgreSQL. |
| * @param isoweek |
| * @param wday |
| * @param tm |
| */ |
| public static void isoweekdate2date(int isoweek, int wday, TimeMeta tm) { |
| int jday; |
| jday = isoweek2j(tm.years, isoweek); |
| /* convert Gregorian week start (Sunday=1) to ISO week start (Monday=1) */ |
| if (wday > 1) { |
| jday += wday - 2; |
| } else { |
| jday += 6; |
| } |
| j2date(jday, tm); |
| } |
| |
| /** |
| * Returns the ISO 8601 day-of-year, given a Gregorian year, month and day. |
| * Possible return values are 1 through 371 (364 in non-leap years). |
| * @param year |
| * @param mon |
| * @param mday |
| * @return |
| */ |
| public static int date2isoyearday(int year, int mon, int mday) { |
| return date2j(year, mon, mday) - isoweek2j(date2isoyear(year, mon, mday), 1) + 1; |
| } |
| |
| public static void toUserTimezone(TimeMeta tm, TimeZone timeZone) { |
| tm.convertToLocalTime(timeZone); |
| } |
| |
| public static void toUTCTimezone(TimeMeta tm, TimeZone timeZone) { |
| tm.convertToUTC(timeZone); |
| } |
| |
| @VisibleForTesting |
| public static String getDisplayTimeZoneOffset(TimeZone timeZone, boolean dst) { |
| return getDisplayTimeZoneOffset((timeZone.getRawOffset() + (dst ? timeZone.getDSTSavings() : 0)) / 1000); |
| } |
| |
| public static String getDisplayTimeZoneOffset(int totalSecs) { |
| if (totalSecs == 0) { |
| return ""; |
| } |
| int minutes = Math.abs(totalSecs) / DateTimeConstants.SECS_PER_MINUTE; |
| int hours = minutes / DateTimeConstants.MINS_PER_HOUR; |
| minutes = minutes - hours * DateTimeConstants.MINS_PER_HOUR; |
| |
| StringBuilder sb = new StringBuilder(); |
| sb.append(totalSecs > 0 ? "+" : "-").append(String.format("%02d", hours)); |
| |
| if (minutes > 0) { |
| sb.append(":").append(String.format("%02d", minutes)); |
| } |
| |
| return sb.toString(); |
| } |
| |
| public static long getDay(TimeMeta dateTime) { |
| return julianTimeToJavaTime(toJulianTimestamp(dateTime.years, dateTime.monthOfYear, dateTime.dayOfMonth, |
| 0, 0, 0, 0)) * DateTimeConstants.USECS_PER_MSEC; |
| } |
| |
| public static long getHour(TimeMeta dateTime) { |
| return julianTimeToJavaTime(toJulianTimestamp(dateTime.years, dateTime.monthOfYear, dateTime.dayOfMonth, |
| dateTime.hours, 0, 0, 0)) * DateTimeConstants.USECS_PER_MSEC; |
| } |
| |
| public static long getMinute(TimeMeta dateTime) { |
| return julianTimeToJavaTime(toJulianTimestamp(dateTime.years, dateTime.monthOfYear, dateTime.dayOfMonth, |
| dateTime.hours, dateTime.minutes, 0, 0)) * DateTimeConstants.USECS_PER_MSEC; |
| } |
| |
| public static long getSecond(TimeMeta dateTime) { |
| return julianTimeToJavaTime(toJulianTimestamp(dateTime.years, dateTime.monthOfYear, dateTime.dayOfMonth, |
| dateTime.hours, dateTime.minutes, dateTime.secs, 0)) * DateTimeConstants.USECS_PER_MSEC; |
| } |
| |
| public static long getMonth(TimeMeta dateTime) { |
| return julianTimeToJavaTime(toJulianTimestamp(dateTime.years, dateTime.monthOfYear, 1, 0, 0, 0, 0)) * |
| DateTimeConstants.USECS_PER_MSEC; |
| } |
| |
| public static long getDayOfWeek(TimeMeta dateTime, int weekday) { |
| if (weekday < 1 || weekday > 7) { |
| throw new RuntimeException("Weekday is out of range. Actual : " + weekday); |
| } |
| |
| int week = date2isoweek(dateTime.years, dateTime.monthOfYear, dateTime.dayOfMonth); |
| int jday = isoweek2j(dateTime.years, week); |
| |
| jday += (weekday - 1); |
| |
| jday -= DateTimeConstants.POSTGRES_EPOCH_JDATE; |
| |
| return julianTimeToJavaTime(toJulianTimestamp(jday, 0, 0, 0, 0)) * DateTimeConstants.USECS_PER_MSEC; |
| } |
| |
| public static long getYear(TimeMeta dateTime) { |
| return julianTimeToJavaTime(toJulianTimestamp(dateTime.years, 1, 1, 0, 0, 0, 0)) * |
| DateTimeConstants.USECS_PER_MSEC; |
| } |
| |
| public static TimeMeta getUTCDateTime(Int8Datum int8Datum){ |
| return getUTCDateTime(int8Datum.asInt8()); |
| } |
| |
| public static TimeMeta getUTCDateTime(long time) { |
| long usecs = time % DateTimeConstants.USECS_PER_MSEC; |
| long julianTimestamp = javaTimeToJulianTime(time / DateTimeConstants.USECS_PER_MSEC); |
| TimeMeta tm = new TimeMeta(); |
| |
| julianTimestamp += usecs; |
| toJulianTimeMeta(julianTimestamp, tm); |
| return tm; |
| } |
| |
| } |