/**
* Copyright (C) 2009-2013 FoundationDB, LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.foundationdb.server.types.mcompat.mtypes;
import com.foundationdb.server.error.InvalidDateFormatException;
import com.foundationdb.server.types.TInstance;
import com.foundationdb.server.types.TBundleID;
import com.foundationdb.server.types.TClass;
import com.foundationdb.server.types.TClassFormatter;
import com.foundationdb.server.types.TExecutionContext;
import com.foundationdb.server.types.mcompat.MParsers;
import com.foundationdb.server.types.FormatOptions;
import com.foundationdb.server.types.aksql.AkCategory;
import com.foundationdb.server.types.common.types.NoAttrTClass;
import com.foundationdb.server.types.mcompat.MBundle;
import com.foundationdb.server.types.mcompat.mcasts.CastUtils;
import com.foundationdb.server.types.value.UnderlyingType;
import com.foundationdb.server.types.value.ValueSource;
import java.text.DateFormatSymbols;
import java.util.Collections;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.foundationdb.sql.types.TypeId;
import com.foundationdb.util.AkibanAppender;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.joda.time.MutableDateTime;
import org.joda.time.base.AbstractDateTime;
import org.joda.time.base.BaseDateTime;
public class MDateAndTime
{
private static final TBundleID MBundleID = MBundle.INSTANCE.id();
public static final NoAttrTClass DATE = new NoAttrTClass(MBundleID,
"date", AkCategory.DATE_TIME, FORMAT.DATE, 1, 1, 3, UnderlyingType.INT_32, MParsers.DATE, 10, TypeId.DATE_ID)
{
@Override
public TClass widestComparable()
{
return DATETIME;
}
};
public static final NoAttrTClass DATETIME = new NoAttrTClass(MBundleID,
"datetime", AkCategory.DATE_TIME, FORMAT.DATETIME, 1, 1, 8, UnderlyingType.INT_64, MParsers.DATETIME, 19, TypeId.DATETIME_ID);
public static final NoAttrTClass TIME = new NoAttrTClass(MBundleID,
"time", AkCategory.DATE_TIME, FORMAT.TIME, 1, 1, 3, UnderlyingType.INT_32, MParsers.TIME, 8, TypeId.TIME_ID);
public static final NoAttrTClass YEAR = new NoAttrTClass(MBundleID,
"year", AkCategory.DATE_TIME, FORMAT.YEAR, 1, 1, 1, UnderlyingType.INT_16, MParsers.YEAR, 4, TypeId.YEAR_ID);
public static final NoAttrTClass TIMESTAMP = new NoAttrTClass(MBundleID,
"timestamp", AkCategory.DATE_TIME, FORMAT.TIMESTAMP, 1, 1, 4, UnderlyingType.INT_32, MParsers.TIMESTAMP, 19, TypeId.TIMESTAMP_ID);
/** Locale.getLanguage() -> String[] of month names. */
public static final Map<String, String[]> MONTHS;
static {
Locale[] supportedLocales = { Locale.ENGLISH };
Map<String, String[]> months = new HashMap<>();
for(Locale l : supportedLocales) {
DateFormatSymbols fm = new DateFormatSymbols(l);
months.put(l.getLanguage(), fm.getMonths());
}
MONTHS = Collections.unmodifiableMap(months);
}
public static enum FORMAT implements TClassFormatter {
DATE {
@Override
public void format(TInstance type, ValueSource source, AkibanAppender out) {
out.append(dateToString(source.getInt32()));
}
@Override
public void formatAsLiteral(TInstance type, ValueSource source, AkibanAppender out) {
out.append("DATE '");
out.append(dateToString(source.getInt32()));
out.append("'");
}
},
DATETIME {
@Override
public void format(TInstance type, ValueSource source, AkibanAppender out) {
out.append(dateTimeToString(source.getInt64()));
}
@Override
public void formatAsLiteral(TInstance type, ValueSource source, AkibanAppender out) {
out.append("TIMESTAMP '");
out.append(dateTimeToString(source.getInt64()));
out.append("'");
}
},
TIME {
@Override
public void format(TInstance type, ValueSource source, AkibanAppender out) {
out.append(timeToString(source.getInt32()));
}
@Override
public void formatAsLiteral(TInstance type, ValueSource source, AkibanAppender out) {
out.append("TIME '");
out.append(timeToString(source.getInt32()));
out.append("'");
}
},
YEAR {
@Override
public void format(TInstance type, ValueSource source, AkibanAppender out)
{
short raw = source.getInt16();
if (raw == 0)
out.append("0000");
else
out.append(raw + 1900);
}
@Override
public void formatAsLiteral(TInstance type, ValueSource source, AkibanAppender out) {
format(type, source, out);
}
},
TIMESTAMP {
@Override
public void format(TInstance type, ValueSource source, AkibanAppender out) {
out.append(timestampToString(source.getInt32(), null));
}
@Override
public void formatAsLiteral(TInstance type, ValueSource source, AkibanAppender out) {
out.append("TIMESTAMP '");
out.append(timestampToString(source.getInt32(), null));
out.append("'");
}
};
@Override
public void formatAsJson(TInstance type, ValueSource source, AkibanAppender out, FormatOptions options) {
out.append('"');
format(type, source, out);
out.append('"');
}
}
public static String getMonthName(int numericRep, String locale, TExecutionContext context) {
String[] monthNames = MONTHS.get(locale);
if(monthNames == null) {
context.reportBadValue("Unsupported locale: " + locale);
return null;
}
numericRep -= 1;
if(numericRep > monthNames.length || numericRep < 0) {
context.reportBadValue("Month out of range: " + numericRep);
return null;
}
return monthNames[numericRep];
}
public static long[] fromJodaDateTime(AbstractDateTime date) {
return new long[] {
date.getYear(),
date.getMonthOfYear(),
date.getDayOfMonth(),
date.getHourOfDay(),
date.getMinuteOfHour(),
date.getSecondOfMinute()
};
}
public static MutableDateTime toJodaDateTime(long[] dt, String tz) {
return toJodaDateTime(dt, DateTimeZone.forID(tz));
}
public static MutableDateTime toJodaDateTime(long[] dt, DateTimeZone dtz) {
return new MutableDateTime((int)dt[YEAR_INDEX],
(int)dt[MONTH_INDEX],
(int)dt[DAY_INDEX],
(int)dt[HOUR_INDEX],
(int)dt[MIN_INDEX],
(int)dt[SEC_INDEX],
0,
dtz);
}
public static String dateToString(int encodedDate) {
long[] dt = decodeDate(encodedDate);
return String.format("%04d-%02d-%02d", dt[YEAR_INDEX], dt[MONTH_INDEX], dt[DAY_INDEX]);
}
/**
* Parse {@code input} as a DATE and then {@link #encodeDate(long, long, long)}.
* @throws InvalidDateFormatException
*/
public static int parseAndEncodeDate(String input) {
long[] dt = new long[6];
StringType type = parseDateOrTime(input, dt);
switch(type) {
case DATE_ST:
case DATETIME_ST:
return encodeDate(dt);
}
throw new InvalidDateFormatException("date", input);
}
/** Convert millis to a date in the given timezone and then {@link #encodeDate(long, long, long)}. */
public static int encodeDate(long millis, String tz) {
DateTime dt = new DateTime(millis, DateTimeZone.forID(tz));
return encodeDate(dt.getYear(), dt.getMonthOfYear(), dt.getDayOfMonth());
}
/** Convenience for {@link #encodeDate(long, long, long)}. */
public static int encodeDate(long[] dt) {
return encodeDate(dt[YEAR_INDEX], dt[MONTH_INDEX], dt[DAY_INDEX]);
}
/** Encode the year, month and date in the MySQL internal DATE format. */
public static int encodeDate(long y, long m, long d) {
return (int)(y * 512 + m * 32 + d);
}
/** Decode the MySQL internal DATE format long into an array. */
public static long[] decodeDate(long encodedDate) {
return new long[] {
encodedDate / 512,
encodedDate / 32 % 16,
encodedDate % 32,
0,
0,
0
};
}
/** Parse an *external* date long (e.g. YYYYMMDD => 20130130) into a array. */
public static long[] parseDate(long val) {
long[] dt = parseDateTime(val);
dt[HOUR_INDEX] = dt[MIN_INDEX] = dt[SEC_INDEX] = 0;
return dt;
}
/** Parse an *external* datetime long (e.g. YYYYMMDDHHMMSS => 20130130) into a array. */
public static long[] parseDateTime(long val) {
if((val != 0) && (val <= 100)) {
throw new InvalidDateFormatException("date", Long.toString(val));
}
// Pad out HHMMSS if needed
if(val < DATETIME_MONTH_SCALE) {
val *= DATETIME_DATE_SCALE;
}
// External is same as internal, though a two-digit year may need converted
long[] dt = decodeDateTime(val);
if(val != 0) {
dt[YEAR_INDEX] = adjustTwoDigitYear(dt[YEAR_INDEX]);
}
if(!isValidDateTime_Zeros(dt)) {
throw new InvalidDateFormatException("date", Long.toString(val));
}
if(!isValidHrMinSec(dt, true, true)) {
throw new InvalidDateFormatException("datetime", Long.toString(val));
}
return dt;
}
/** Decode {@code encodedDateTime} and format it as a string. */
public static String dateTimeToString(long encodedDateTime) {
long[] dt = decodeDateTime(encodedDateTime);
return dateTimeToString(dt);
}
/** Format {@code dt} as a string. */
public static String dateTimeToString(long[] dt) {
return String.format("%04d-%02d-%02d %02d:%02d:%02d",
dt[YEAR_INDEX],
dt[MONTH_INDEX],
dt[DAY_INDEX],
dt[HOUR_INDEX],
dt[MIN_INDEX],
dt[SEC_INDEX]);
}
/**
* Attempt to parse {@code str} into as DATE and/or TIME.
* Return type indicates success, almost success (INVALID_*) or completely unparsable.
*/
public static StringType parseDateOrTime(String st, long[] dt) {
assert dt.length >= MAX_INDEX;
st = st.trim();
String year = "0";
String month = "0";
String day = "0";
String hour = "0";
String minute = "0";
String seconds = "0";
Matcher matcher;
if((matcher = DATE_PATTERN.matcher(st)).matches()) {
StringType type = StringType.DATE_ST;
year = matcher.group(DATE_YEAR_GROUP);
month = matcher.group(DATE_MONTH_GROUP);
day = matcher.group(DATE_DAY_GROUP);
if(matcher.group(TIME_GROUP) != null) {
type = StringType.DATETIME_ST;
hour = matcher.group(TIME_HOUR_GROUP);
minute = matcher.group(TIME_MINUTE_GROUP);
seconds = matcher.group(TIME_SECOND_GROUP);
}
if(stringsToLongs(dt, true, year, month, day, hour, minute, seconds) &&
isValidDateTime_Zeros(dt)) {
return type;
}
return (type == StringType.DATETIME_ST) ? StringType.INVALID_DATETIME_ST : StringType.INVALID_DATE_ST;
}
else if((matcher = TIME_WITH_DAY_PATTERN.matcher(st)).matches())
{
day = matcher.group(MDateAndTime.TIME_WITH_DAY_DAY_GROUP);
hour = matcher.group(MDateAndTime.TIME_WITH_DAY_HOUR_GROUP);
minute = matcher.group(MDateAndTime.TIME_WITH_DAY_MIN_GROUP);
seconds = matcher.group(MDateAndTime.TIME_WITH_DAY_SEC_GROUP);
if(stringsToLongs(dt, false, year, month, day, hour, minute, seconds) &&
isValidHrMinSec(dt, false, false)) {
// adjust DAY to HOUR
int sign = 1;
if(dt[DAY_INDEX] < 0) {
dt[DAY_INDEX] *= (sign = -1);
}
dt[HOUR_INDEX] = sign * (dt[HOUR_INDEX] += dt[DAY_INDEX] * 24);
dt[DAY_INDEX] = 0;
return StringType.TIME_ST;
}
return StringType.INVALID_TIME_ST;
}
else if((matcher = TIME_WITHOUT_DAY_PATTERN.matcher(st)).matches())
{
hour = matcher.group(MDateAndTime.TIME_WITHOUT_DAY_HOUR_GROUP);
minute = matcher.group(MDateAndTime.TIME_WITHOUT_DAY_MIN_GROUP);
seconds = matcher.group(MDateAndTime.TIME_WITHOUT_DAY_SEC_GROUP);
if(stringsToLongs(dt, false, year, month, day, hour, minute, seconds) &&
isValidHrMinSec(dt, false, false)) {
return StringType.TIME_ST;
}
return StringType.INVALID_TIME_ST;
}
else // last attempt, split by any DELIM and look for 3 or 6 parts
{
String[] parts = st.split("\\s++");
if(parts.length == 2) {
String[] dTok = parts[0].split(DELIM);
String[] tTok = parts[1].split(DELIM);
if((dTok.length == 3) && (tTok.length == 3)) {
if(stringsToLongs(dt, true, dTok[0], dTok[1], dTok[2], tTok[0], tTok[1], tTok[2]) &&
isValidDateTime_Zeros(dt)) {
return StringType.DATETIME_ST;
}
return StringType.INVALID_DATETIME_ST;
}
} else if(parts.length == 1) {
String[] dTok = parts[0].split(DELIM);
if(dTok.length == 3) {
if(stringsToLongs(dt, true, dTok[0], dTok[1], dTok[2]) &&
isValidDateTime_Zeros(dt)) {
return StringType.DATE_ST;
}
return StringType.INVALID_DATE_ST;
}
}
}
return StringType.UNPARSABLE;
}
/**
* Parse {@code input} as a DATETIME and then {@link #encodeDateTime(long, long, long, long, long, long)}.
* @throws InvalidDateFormatException
*/
public static long parseAndEncodeDateTime(String input) {
long[] dt = new long[6];
StringType type = parseDateOrTime(input, dt);
switch(type) {
case DATE_ST:
case DATETIME_ST:
return encodeDateTime(dt);
}
throw new InvalidDateFormatException("datetime", input);
}
/** Convert millis to a DateTime and {@link #encodeDateTime(BaseDateTime)}. */
public static long encodeDateTime(long millis, String tz) {
DateTime dt = new DateTime(millis, DateTimeZone.forID(tz));
return encodeDateTime(dt);
}
/** Pass components of {@code dt} to {@link #encodeDateTime(long, long, long, long, long, long)}. */
public static long encodeDateTime(BaseDateTime dt) {
return encodeDateTime(dt.getYear(),
dt.getMonthOfYear(),
dt.getDayOfMonth(),
dt.getHourOfDay(),
dt.getMinuteOfHour(),
dt.getSecondOfMinute());
}
/** Convenience for {@link #encodeDateTime(long, long, long, long, long, long)}. */
public static long encodeDateTime(long[] dt) {
return encodeDateTime(dt[YEAR_INDEX],
dt[MONTH_INDEX],
dt[DAY_INDEX],
dt[HOUR_INDEX],
dt[MIN_INDEX],
dt[SEC_INDEX]);
}
/** Encode the given date and time in the MySQL DATETIME internal format long. */
public static long encodeDateTime(long year, long month, long day, long hour, long min, long sec) {
return year * DATETIME_YEAR_SCALE +
month * DATETIME_MONTH_SCALE +
day * DATETIME_DAY_SCALE +
hour * DATETIME_HOUR_SCALE +
min * DATETIME_MIN_SCALE +
sec;
}
/** Decode the MySQL DATETIME internal format long into a array. */
public static long[] decodeDateTime(long encodedDateTime) {
return new long[] {
encodedDateTime / DATETIME_YEAR_SCALE,
encodedDateTime / DATETIME_MONTH_SCALE % 100,
encodedDateTime / DATETIME_DAY_SCALE % 100,
encodedDateTime / DATETIME_HOUR_SCALE % 100,
encodedDateTime / DATETIME_MIN_SCALE % 100,
encodedDateTime % 100
};
}
public static String timeToString(int encodedTime) {
long[] dt = decodeTime(encodedTime);
return timeToString(dt);
}
public static String timeToString(long[] dt) {
return timeToString(dt[HOUR_INDEX], dt[MIN_INDEX], dt[SEC_INDEX]);
}
public static String timeToString(long h, long m, long s) {
return String.format("%s%02d:%02d:%02d",
isHrMinSecNegative(h, m, s) ? "-" : "",
Math.abs(h),
Math.abs(m),
Math.abs(s));
}
public static void timeToDatetime(long[] dt) {
dt[YEAR_INDEX] = adjustTwoDigitYear(dt[HOUR_INDEX]);
dt[MONTH_INDEX] = dt[MIN_INDEX];
dt[DAY_INDEX] = dt[SEC_INDEX];
// erase the time portion
dt[HOUR_INDEX] = 0;
dt[MIN_INDEX] = 0;
dt[SEC_INDEX] = 0;
}
public static int parseTime(String string, TExecutionContext context) {
long[] dt = new long[6];
StringType type = parseDateOrTime(string, dt);
switch(type) {
case TIME_ST:
case DATE_ST:
case DATETIME_ST:
if(isValidDate_Zeros(dt) && isValidHrMinSec(dt, true, true)) {
dt[YEAR_INDEX] = dt[MONTH_INDEX] = dt[DAY_INDEX] = 0;
break;
}
// fall
break;
default:
throw new InvalidDateFormatException("TIME", string);
}
if(!isValidHrMinSec(dt, false, false)) {
throw new InvalidDateFormatException("time", string);
}
return encodeTime(dt, context);
}
public static long[] decodeTime(long encodedTime) {
boolean isNegative = (encodedTime < 0);
if(isNegative) {
encodedTime = -encodedTime;
}
// TODO: Fake date is just asking for trouble but numerous callers depend on it.
long ret[] = new long[] {
1970,
1,
1,
encodedTime / DATETIME_HOUR_SCALE,
encodedTime / DATETIME_MIN_SCALE % 100,
encodedTime % 100
};
if(isNegative) {
for(int i = HOUR_INDEX; i < ret.length; ++i) {
if(ret[i] != 0) {
ret[i] = -ret[i];
break;
}
}
}
return ret;
}
/** Convert {@code millis} to a DateTime and {@link #encodeTime(long, long, long, TExecutionContext)}. */
public static int encodeTime(long millis, String tz) {
DateTime dt = new DateTime(millis, DateTimeZone.forID(tz));
return encodeTime(dt.getHourOfDay(),
dt.getMinuteOfHour(),
dt.getSecondOfMinute(),
null);
}
/** Convenience for {@link #encodeTime(long, long, long, TExecutionContext)}. */
public static int encodeTime(long[] dt, TExecutionContext context) {
return encodeTime(dt[HOUR_INDEX],
dt[MIN_INDEX],
dt[SEC_INDEX],
context);
}
/** Encode hour, minute and second as a MySQL internal TIME value. {@code context} may be null. */
public static int encodeTime(long h, long m, long s, TExecutionContext context) {
int sign = isHrMinSecNegative(h, m, s) ? -1 : 1;
long ret = sign * (Math.abs(h) * DATETIME_HOUR_SCALE + (Math.abs(m) * DATETIME_MIN_SCALE) + Math.abs(s));
if(context != null) {
return (int)CastUtils.getInRange(TIME_MAX, TIME_MIN, ret, context);
}
return (int)((ret < TIME_MIN) ? TIME_MIN : (ret > TIME_MAX ? TIME_MAX : ret));
}
/** Parse {@code input} as a TIMESTAMP in the {@code tz} timezone. */
public static int parseAndEncodeTimestamp(String input, String tz, TExecutionContext context) {
long[] dt = new long[6];
StringType type = parseDateOrTime(input, dt);
switch(type) {
case DATE_ST:
case DATETIME_ST:
return encodeTimestamp(dt, tz, context);
default:
// e.g. SELECT UNIX_TIMESTAMP('1920-21-01 00:00:00') -> 0
context.warnClient(new InvalidDateFormatException("timestamp", input));
return 0;
}
}
/** Decode {@code encodedTimestamp} using the {@code tz} timezone. */
public static long[] decodeTimestamp(long encodedTimestamp, String tz) {
DateTime dt = new DateTime(encodedTimestamp * 1000L, DateTimeZone.forID(tz));
return new long[] {
dt.getYear(),
dt.getMonthOfYear(),
dt.getDayOfMonth(),
dt.getHourOfDay(),
dt.getMinuteOfHour(),
dt.getSecondOfMinute()
};
}
/** Convenience for {@link #toJodaDateTime(long[], String)} and {@link #encodeTimestamp(long, TExecutionContext)}. */
public static int encodeTimestamp(long[] dt, String tz, TExecutionContext context) {
return encodeTimestamp(toJodaDateTime(dt, tz), context);
}
/** Convert {@code dateTime} to milliseconds and {@link #encodeTimestamp(long, TExecutionContext)}. */
public static int encodeTimestamp(BaseDateTime dateTime, TExecutionContext context) {
return encodeTimestamp(dateTime.getMillis(), context);
}
/** Encode {@code millis} as an internal MySQL TIMESTAMP. Clamps to MIN/MAX range. */
public static int encodeTimestamp(long millis, TExecutionContext context) {
return (int)CastUtils.getInRange(TIMESTAMP_MAX, TIMESTAMP_MIN, millis / 1000L, TS_ERROR_VALUE, context);
}
public static boolean isValidTimestamp(BaseDateTime dt) {
long millis = dt.getMillis();
return (millis >= TIMESTAMP_MIN) && (millis <= TIMESTAMP_MAX);
}
/** Encode {@code dt} as a MySQL internal TIMESTAMP. Range is unchecked. */
public static int getTimestamp(long[] dt, String tz) {
MutableDateTime dateTime = toJodaDateTime(dt, tz);
return (int)(dateTime.getMillis() / 1000L);
}
/** Decode {@code encodedTimestamp} and format as a string. */
public static String timestampToString(long encodedTimestamp, String tz) {
long[] dt = decodeTimestamp(encodedTimestamp, tz);
return MDateAndTime.dateTimeToString(dt);
}
/** Parse an hour:min or named timezone. */
public static DateTimeZone parseTimeZone(String tz) {
try {
Matcher m = TZ_PATTERN.matcher(tz);
if(m.matches()) {
int hourSign = "-".equals(m.group(TZ_SIGN_GROUP)) ? -1 : 1;
int hour = Integer.parseInt(m.group(TZ_HOUR_GROUP));
int min = Integer.parseInt(m.group(TZ_MINUTE_GROUP));
return DateTimeZone.forOffsetHoursMinutes(hourSign * hour, min);
} else {
// Upper 'utc' but not 'America/New_York'
if(tz.indexOf('/') == -1) {
tz = tz.toUpperCase();
}
return DateTimeZone.forID(tz);
}
} catch(IllegalArgumentException e) {
throw new InvalidDateFormatException("timezone", tz);
}
}
/** {@code true} if date from {@code dt} is valid, disallowing the zero year. */
public static boolean isValidDateTime_Zeros(long[] dt) {
return isValidDateTime(dt, ZeroFlag.YEAR, ZeroFlag.MONTH, ZeroFlag.DAY);
}
public static boolean isValidDateTime(long[] dt, ZeroFlag... flags) {
return (dt != null) &&
isValidDate(dt, flags) &&
isValidHrMinSec(dt, true, true);
}
public static boolean isHrMinSecNegative(long[] dt) {
return isHrMinSecNegative(dt[HOUR_INDEX], dt[MIN_INDEX], dt[SEC_INDEX]);
}
public static boolean isHrMinSecNegative(long h, long m, long s) {
return (h < 0) || (m < 0) || (s < 0);
}
/** {@code true} if time from {@code dt} is usable in functions and expressions. */
public static boolean isValidHrMinSec(long[] dt, boolean checkHour, boolean isFromDateTime) {
return isValidHrMinSec(dt[HOUR_INDEX], dt[MIN_INDEX], dt[SEC_INDEX], checkHour, isFromDateTime);
}
public static boolean isValidHrMinSec(long h, long m, long s, boolean checkHour, boolean isFromDateTime) {
if(isFromDateTime) {
// DATETIME limited to a single, positive day
return (h >= 0) && (h <= 23) &&
(m >= 0) && (m <= 59) &&
(s >= 0) && (s <= 59);
}
// Otherwise must be in +-838:59:59
int zeroCount = (h < 0 ? 1 : 0) + (m < 0 ? 1 : 0) + (s < 0 ? 1 : 0);
return (!checkHour || (h >= -838) && (h <= 838)) &&
(m >= -59) && (m <= 59) &&
(s >= -59) && (s <= 59) &&
(zeroCount <= 1);
}
public static boolean isZeroDayMonth(long[] dt) {
return (dt[DAY_INDEX] == 0) || (dt[MONTH_INDEX] == 0);
}
public static boolean isValidDate_Zeros(long[] dt) {
return isValidDate(dt, ZeroFlag.YEAR, ZeroFlag.MONTH, ZeroFlag.DAY);
}
public static boolean isValidDate_NoZeros(long[] dt) {
return isValidDate(dt);
}
public static boolean isValidDate(long[] dt, ZeroFlag... flags) {
return isValidDate(dt[YEAR_INDEX], dt[MONTH_INDEX], dt[DAY_INDEX], flags);
}
public static boolean isValidDate(long y, long m, long d, ZeroFlag... flags) {
long last = getLastDay(y, m);
return (last > 0) &&
(d <= last) &&
(y > 0 || contains(flags, ZeroFlag.YEAR)) &&
(m > 0 || contains(flags, ZeroFlag.MONTH)) &&
(d > 0 || contains(flags, ZeroFlag.DAY));
}
/** Convenience for {@link #getLastDay(long, long)}. */
public static long getLastDay(long[] dt) {
return getLastDay((int)dt[YEAR_INDEX], (int)dt[MONTH_INDEX]);
}
/** Get the last day for the given month in the given year. */
public static long getLastDay(long year, long month) {
switch((int)month) {
case 2:
if((year % 400 == 0) || ((year % 4 == 0) && (year % 100 != 0))) {
return 29;
}
return 28;
case 4:
case 6:
case 9:
case 11:
return 30L;
case 0:
case 1:
case 3:
case 5:
case 7:
case 8:
case 10:
case 12:
return 31L;
default:
return -1;
}
}
/**
* MySQL docs for DATE, DATETIME and TIMESTAMP:
* Year values in the range 00-69 are converted to 2000-2069.
* Year values in the range 70-99 are converted to 1970-1999.
*/
protected static long adjustTwoDigitYear(long year) {
if(year <= 69) {
year = year + 2000;
} else if(year < 100) {
year = 1900 + year;
}
return year;
}
/** {@link Long#parseLong(String)} each string. Return false if any failed. */
private static boolean stringsToLongs(long[] dt, boolean convertYear, String... parts) {
assert parts.length <= dt.length;
for(int i = 0; i < parts.length; ++i) {
try {
if(parts[i] == null) {
return false;
}
dt[i] = Long.parseLong(parts[i].trim());
// Must be *exactly* two-digit to get converted
if(convertYear && (i == YEAR_INDEX) && (parts[i].length() == 2)) {
dt[i] = adjustTwoDigitYear(dt[i]);
}
} catch(NumberFormatException e) {
return false;
}
}
return true;
}
private static boolean contains(ZeroFlag[] flags, ZeroFlag flag) {
for(ZeroFlag f : flags) {
if(f == flag) {
return true;
}
}
return false;
}
public static final int YEAR_INDEX = 0;
public static final int MONTH_INDEX = 1;
public static final int DAY_INDEX = 2;
public static final int HOUR_INDEX = 3;
public static final int MIN_INDEX = 4;
public static final int SEC_INDEX = 5;
public static final int MAX_INDEX = SEC_INDEX;
private static final long DATETIME_DATE_SCALE = 1000000L;
private static final long DATETIME_YEAR_SCALE = 10000L * DATETIME_DATE_SCALE;
private static final long DATETIME_MONTH_SCALE = 100L * DATETIME_DATE_SCALE;
private static final long DATETIME_DAY_SCALE = DATETIME_DATE_SCALE;
private static final long DATETIME_HOUR_SCALE = 10000L;
private static final long DATETIME_MIN_SCALE = 100L;
private static final int DATE_GROUP = 1;
private static final int DATE_YEAR_GROUP = 2;
private static final int DATE_MONTH_GROUP = 3;
private static final int DATE_DAY_GROUP = 4;
private static final int TIME_GROUP = 5;
private static final int TIME_HOUR_GROUP = 7;
private static final int TIME_MINUTE_GROUP = 8;
private static final int TIME_SECOND_GROUP = 9;
private static final int TIME_FRAC_GROUP = 10;
private static final int TIME_TIMEZONE_GROUP = 11;
private static final Pattern DATE_PATTERN
= Pattern.compile("^((\\d+)-(\\d+)-(\\d+))(([T]{1}|\\s+)(\\d+):(\\d+):(\\d+)(\\.\\d+)?)?[Z]?(\\s*[+-]\\d+:?\\d+(:?\\d+)?)?$");
private static final int TIME_WITH_DAY_DAY_GROUP = 2;
private static final int TIME_WITH_DAY_HOUR_GROUP = 3;
private static final int TIME_WITH_DAY_MIN_GROUP = 4;
private static final int TIME_WITH_DAY_SEC_GROUP = 5;
private static final Pattern TIME_WITH_DAY_PATTERN
= Pattern.compile("^(([-+]?\\d+)\\s+(\\d+):(\\d+):(\\d+)(\\.\\d+)?[Z]?(\\s*[+-]\\d+:?\\d+(:?\\d+)?)?)?$");
private static final int TIME_WITHOUT_DAY_HOUR_GROUP = 2;
private static final int TIME_WITHOUT_DAY_MIN_GROUP = 3;
private static final int TIME_WITHOUT_DAY_SEC_GROUP = 4;
private static final Pattern TIME_WITHOUT_DAY_PATTERN
= Pattern.compile("^(([-+]?\\d+):(\\d+):(\\d+)(\\.\\d+)?[Z]?(\\s*[+-]\\d+:?\\d+(:?\\d+)?)?)?$");
private static final int TZ_SIGN_GROUP = 1;
private static final int TZ_HOUR_GROUP = 2;
private static final int TZ_MINUTE_GROUP = 3;
// This allows one digit hour or minute, which Joda does not.
private static final Pattern TZ_PATTERN = Pattern.compile("([-+]?)([\\d]{1,2}):([\\d]{1,2})");
// delimiter for a date/time/datetime string. MySQL allows almost anything to be the delimiter
private static final String DELIM = "\\W";
// upper and lower limit of TIMESTAMP value
// as per http://dev.mysql.com/doc/refman/5.5/en/datetime.html
public static final long TIMESTAMP_MIN = DateTime.parse("1970-01-01T00:00:01Z").getMillis();
public static final long TIMESTAMP_MAX = DateTime.parse("2038-01-19T03:14:07Z").getMillis();
public static final long TS_ERROR_VALUE = 0L;
// upper and lower limti of TIME value
// as per http://dev.mysql.com/doc/refman/5.5/en/time.html
public static final int TIME_MAX = 8385959;
public static final int TIME_MIN = -8385959;
public static boolean isValidType(StringType type)
{
switch(type) {
case DATE_ST:
case DATETIME_ST:
case TIME_ST:
return true;
default:
return false;
}
}
public static enum ZeroFlag {
YEAR,
MONTH,
DAY
}
public static enum StringType
{
DATE_ST,
DATETIME_ST,
TIME_ST,
INVALID_DATE_ST,
INVALID_DATETIME_ST,
INVALID_TIME_ST,
UNPARSABLE
}
}