blob: 8f43c7e9e4058335fac551add3f8f18bbff4a17e [file] [log] [blame]
/**
* 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;
}
}