blob: 3c988a1408203f126fd36e0bdeae552b1f12632c [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.calcite.avatica.util;
import java.sql.Time;
import java.sql.Timestamp;
import java.text.DateFormat;
import java.text.NumberFormat;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.temporal.ChronoField;
import java.util.Calendar;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Utility functions for datetime types: date, time, timestamp.
*
* <p>Used by the JDBC driver.
*
* <p>TODO: review methods for performance. Due to allocations required, it may
* be preferable to introduce a "formatter" with the required state.
*/
public class DateTimeUtils {
/** The julian date of the epoch, 1970-01-01. */
public static final int EPOCH_JULIAN = 2440588;
private DateTimeUtils() {}
//~ Static fields/initializers ---------------------------------------------
/** The SimpleDateFormat string for ISO dates, "yyyy-MM-dd". */
public static final String DATE_FORMAT_STRING = "yyyy-MM-dd";
/** The SimpleDateFormat string for ISO times, "HH:mm:ss". */
public static final String TIME_FORMAT_STRING = "HH:mm:ss";
/** The SimpleDateFormat string for ISO timestamps, "yyyy-MM-dd HH:mm:ss". */
public static final String TIMESTAMP_FORMAT_STRING =
DATE_FORMAT_STRING + " " + TIME_FORMAT_STRING;
/** Regex for date, YYYY-MM-DD. */
private static final Pattern ISO_DATE_PATTERN =
Pattern.compile("^(\\d{4})-([0]\\d|1[0-2])-([0-2]\\d|3[01])$");
/** Regex for time, HH:MM:SS. */
private static final Pattern ISO_TIME_PATTERN =
Pattern.compile("^([0-2]\\d):[0-5]\\d:[0-5]\\d(\\.\\d*)*$");
/** The GMT time zone.
*
* @deprecated Use {@link #UTC_ZONE} */
@Deprecated // to be removed before 2.0
public static final TimeZone GMT_ZONE = TimeZone.getTimeZone("GMT");
/** The UTC time zone. */
public static final TimeZone UTC_ZONE = TimeZone.getTimeZone("UTC");
/** The Java default time zone. */
public static final TimeZone DEFAULT_ZONE = TimeZone.getDefault();
/**
* The number of milliseconds in a second.
*/
public static final long MILLIS_PER_SECOND = 1000L;
/**
* The number of milliseconds in a minute.
*/
public static final long MILLIS_PER_MINUTE = 60000L;
/**
* The number of milliseconds in an hour.
*/
public static final long MILLIS_PER_HOUR = 3600000L; // = 60 * 60 * 1000
/**
* The number of milliseconds in a day.
*
* <p>This is the modulo 'mask' used when converting
* TIMESTAMP values to DATE and TIME values.
*/
public static final long MILLIS_PER_DAY = 86400000; // = 24 * 60 * 60 * 1000
/**
* The number of seconds in a day.
*/
public static final long SECONDS_PER_DAY = 86_400; // = 24 * 60 * 60
/**
* The number of nanoseconds in a millisecond.
*/
public static final long NANOS_PER_MILLI = 1000000L;
/**
* Calendar set to the epoch (1970-01-01 00:00:00 UTC). Useful for
* initializing other values. Calendars are not immutable, so be careful not
* to screw up this object for everyone else.
*/
public static final Calendar ZERO_CALENDAR;
private static final OffsetDateTimeHandler OFFSET_DATE_TIME_HANDLER;
static {
ZERO_CALENDAR = Calendar.getInstance(DateTimeUtils.UTC_ZONE, Locale.ROOT);
ZERO_CALENDAR.setTimeInMillis(0);
OffsetDateTimeHandler h;
try {
h = new ReflectiveOffsetDateTimeHandler();
} catch (ClassNotFoundException e) {
h = new NoopOffsetDateTimeHandler();
}
OFFSET_DATE_TIME_HANDLER = h;
}
//~ Methods ----------------------------------------------------------------
/**
* Parses a string using {@link SimpleDateFormat} and a given pattern. This
* method parses a string at the specified parse position and if successful,
* updates the parse position to the index after the last character used.
* The parsing is strict and requires months to be less than 12, days to be
* less than 31, etc.
*
* @param s string to be parsed
* @param dateFormat Date format
* @param tz time zone in which to interpret string. Defaults to the Java
* default time zone
* @param pp position to start parsing from
* @return a Calendar initialized with the parsed value, or null if parsing
* failed. If returned, the Calendar is configured to the GMT time zone.
*/
private static Calendar parseDateFormat(String s, DateFormat dateFormat,
TimeZone tz, ParsePosition pp) {
if (tz == null) {
tz = DEFAULT_ZONE;
}
Calendar ret = Calendar.getInstance(tz, Locale.ROOT);
dateFormat.setCalendar(ret);
dateFormat.setLenient(false);
final Date d = dateFormat.parse(s, pp);
if (null == d) {
return null;
}
ret.setTime(d);
ret.setTimeZone(UTC_ZONE);
return ret;
}
@Deprecated // to be removed before 2.0
public static Calendar parseDateFormat(String s, String pattern,
TimeZone tz) {
return parseDateFormat(s, new SimpleDateFormat(pattern, Locale.ROOT), tz);
}
/**
* Parses a string using {@link SimpleDateFormat} and a given pattern. The
* entire string must match the pattern specified.
*
* @param s string to be parsed
* @param dateFormat Date format
* @param tz time zone in which to interpret string. Defaults to the Java
* default time zone
* @return a Calendar initialized with the parsed value, or null if parsing
* failed. If returned, the Calendar is configured to the UTC time zone.
*/
public static Calendar parseDateFormat(String s, DateFormat dateFormat,
TimeZone tz) {
ParsePosition pp = new ParsePosition(0);
Calendar ret = parseDateFormat(s, dateFormat, tz, pp);
if (pp.getIndex() != s.length()) {
// Didn't consume entire string - not good
return null;
}
return ret;
}
@Deprecated // to be removed before 2.0
public static PrecisionTime parsePrecisionDateTimeLiteral(
String s,
String pattern,
TimeZone tz) {
assert pattern != null;
return parsePrecisionDateTimeLiteral(s,
new SimpleDateFormat(pattern, Locale.ROOT), tz, 3);
}
/**
* Parses a string using {@link SimpleDateFormat} and a given pattern, and
* if present, parses a fractional seconds component. The fractional seconds
* component must begin with a decimal point ('.') followed by numeric
* digits. The precision is rounded to a maximum of 3 digits of fractional
* seconds precision (to obtain milliseconds).
*
* @param s string to be parsed
* @param dateFormat Date format
* @param tz time zone in which to interpret string. Defaults to the
* local time zone
* @return a {@link DateTimeUtils.PrecisionTime PrecisionTime} initialized
* with the parsed value, or null if parsing failed. The PrecisionTime
* contains a GMT Calendar and a precision.
*/
public static PrecisionTime parsePrecisionDateTimeLiteral(String s,
DateFormat dateFormat, TimeZone tz, int maxPrecision) {
final ParsePosition pp = new ParsePosition(0);
final Calendar cal = parseDateFormat(s, dateFormat, tz, pp);
if (cal == null) {
return null; // Invalid date/time format
}
// Note: the Java SimpleDateFormat 'S' treats any number after
// the decimal as milliseconds. That means 12:00:00.9 has 9
// milliseconds and 12:00:00.9999 has 9999 milliseconds.
int p = 0;
String secFraction = "";
if (pp.getIndex() < s.length()) {
// Check to see if rest is decimal portion
if (s.charAt(pp.getIndex()) != '.') {
return null;
}
// Skip decimal sign
pp.setIndex(pp.getIndex() + 1);
// Parse decimal portion
if (pp.getIndex() < s.length()) {
secFraction = s.substring(pp.getIndex());
if (!secFraction.matches("\\d+")) {
return null;
}
NumberFormat nf = NumberFormat.getIntegerInstance(Locale.ROOT);
Number num = nf.parse(s, pp);
if (num == null || pp.getIndex() != s.length()) {
// Invalid decimal portion
return null;
}
// Determine precision - only support prec 3 or lower
// (milliseconds) Higher precisions are quietly rounded away
p = secFraction.length();
if (maxPrecision >= 0) {
// If there is a maximum precision, ignore subsequent digits
p = Math.min(maxPrecision, p);
secFraction = secFraction.substring(0, p);
}
// Calculate milliseconds
String millis = secFraction;
if (millis.length() > 3) {
millis = secFraction.substring(0, 3);
}
while (millis.length() < 3) {
millis = millis + "0";
}
int ms = Integer.parseInt(millis);
cal.add(Calendar.MILLISECOND, ms);
}
}
assert pp.getIndex() == s.length();
return new PrecisionTime(cal, secFraction, p);
}
/**
* Gets the active time zone based on a Calendar argument
*/
public static TimeZone getTimeZone(Calendar cal) {
if (cal == null) {
return DEFAULT_ZONE;
}
return cal.getTimeZone();
}
/**
* Checks if the date/time format is valid
*
* @param pattern {@link SimpleDateFormat} pattern
* @throws IllegalArgumentException if the given pattern is invalid
*/
public static void checkDateFormat(String pattern) {
new SimpleDateFormat(pattern, Locale.ROOT);
}
/**
* Creates a new date formatter with Farrago specific options. Farrago
* parsing is strict and does not allow values such as day 0, month 13, etc.
*
* @param format {@link SimpleDateFormat} pattern
*/
public static SimpleDateFormat newDateFormat(String format) {
SimpleDateFormat sdf = new SimpleDateFormat(format, Locale.ROOT);
sdf.setLenient(false);
return sdf;
}
/** Helper for CAST({timestamp} AS VARCHAR(n)). */
public static String unixTimestampToString(long timestamp) {
return unixTimestampToString(timestamp, 0);
}
public static String unixTimestampToString(long timestamp, int precision) {
final StringBuilder buf = new StringBuilder(17);
int date = (int) (timestamp / MILLIS_PER_DAY);
int time = (int) (timestamp % MILLIS_PER_DAY);
if (time < 0) {
--date;
time += MILLIS_PER_DAY;
}
unixDateToString(buf, date);
buf.append(' ');
unixTimeToString(buf, time, precision);
return buf.toString();
}
/** Helper for CAST({timestamp} AS VARCHAR(n)). */
public static String unixTimeToString(int time) {
return unixTimeToString(time, 0);
}
public static String unixTimeToString(int time, int precision) {
final StringBuilder buf = new StringBuilder(8);
unixTimeToString(buf, time, precision);
return buf.toString();
}
private static void unixTimeToString(StringBuilder buf, int time,
int precision) {
int h = time / 3600000;
int time2 = time % 3600000;
int m = time2 / 60000;
int time3 = time2 % 60000;
int s = time3 / 1000;
int ms = time3 % 1000;
int2(buf, h);
buf.append(':');
int2(buf, m);
buf.append(':');
int2(buf, s);
if (precision > 0) {
buf.append('.');
while (precision > 0) {
buf.append((char) ('0' + (ms / 100)));
ms = ms % 100;
ms = ms * 10;
--precision;
}
}
}
private static void int2(StringBuilder buf, int i) {
buf.append((char) ('0' + (i / 10) % 10));
buf.append((char) ('0' + i % 10));
}
private static void int4(StringBuilder buf, int i) {
buf.append((char) ('0' + (i / 1000) % 10));
buf.append((char) ('0' + (i / 100) % 10));
buf.append((char) ('0' + (i / 10) % 10));
buf.append((char) ('0' + i % 10));
}
/** Helper for CAST({date} AS VARCHAR(n)). */
public static String unixDateToString(int date) {
final StringBuilder buf = new StringBuilder(10);
unixDateToString(buf, date);
return buf.toString();
}
private static void unixDateToString(StringBuilder buf, int date) {
julianToString(buf, date + EPOCH_JULIAN);
}
private static void julianToString(StringBuilder buf, int julian) {
// this shifts the epoch back to astronomical year -4800 instead of the
// start of the Christian era in year AD 1 of the proleptic Gregorian
// calendar.
int j = julian + 32044;
int g = j / 146097;
int dg = j % 146097;
int c = (dg / 36524 + 1) * 3 / 4;
int dc = dg - c * 36524;
int b = dc / 1461;
int db = dc % 1461;
int a = (db / 365 + 1) * 3 / 4;
int da = db - a * 365;
// integer number of full years elapsed since March 1, 4801 BC
int y = g * 400 + c * 100 + b * 4 + a;
// integer number of full months elapsed since the last March 1
int m = (da * 5 + 308) / 153 - 2;
// number of days elapsed since day 1 of the month
int d = da - (m + 4) * 153 / 5 + 122;
int year = y - 4800 + (m + 2) / 12;
int month = (m + 2) % 12 + 1;
int day = d + 1;
int4(buf, year);
buf.append('-');
int2(buf, month);
buf.append('-');
int2(buf, day);
}
public static String intervalYearMonthToString(int v, TimeUnitRange range) {
final StringBuilder buf = new StringBuilder();
if (v >= 0) {
buf.append('+');
} else {
buf.append('-');
v = -v;
}
final int y;
final int m;
switch (range) {
case YEAR:
v = roundUp(v, 12);
y = v / 12;
buf.append(y);
break;
case YEAR_TO_MONTH:
y = v / 12;
buf.append(y);
buf.append('-');
m = v % 12;
number(buf, m, 2);
break;
case MONTH:
m = v;
buf.append(m);
break;
default:
throw new AssertionError(range);
}
return buf.toString();
}
public static StringBuilder number(StringBuilder buf, int v, int n) {
for (int k = digitCount(v); k < n; k++) {
buf.append('0');
}
return buf.append(v);
}
public static int digitCount(int v) {
for (int n = 1;; n++) {
v /= 10;
if (v == 0) {
return n;
}
}
}
private static int roundUp(int dividend, int divisor) {
int remainder = dividend % divisor;
dividend -= remainder;
if (remainder * 2 > divisor) {
dividend += divisor;
}
return dividend;
}
/** Cheap, unsafe, long power. power(2, 3) returns 8. */
public static long powerX(long a, long b) {
long x = 1;
while (b > 0) {
x *= a;
--b;
}
return x;
}
public static String intervalDayTimeToString(long v, TimeUnitRange range,
int scale) {
final StringBuilder buf = new StringBuilder();
if (v >= 0) {
buf.append('+');
} else {
buf.append('-');
v = -v;
}
final long ms;
final long s;
final long m;
final long h;
final long d;
switch (range) {
case DAY_TO_SECOND:
v = roundUp(v, powerX(10, 3 - scale));
ms = v % 1000;
v /= 1000;
s = v % 60;
v /= 60;
m = v % 60;
v /= 60;
h = v % 24;
v /= 24;
d = v;
buf.append((int) d);
buf.append(' ');
number(buf, (int) h, 2);
buf.append(':');
number(buf, (int) m, 2);
buf.append(':');
number(buf, (int) s, 2);
fraction(buf, scale, ms);
break;
case DAY_TO_MINUTE:
v = roundUp(v, 1000 * 60);
v /= 1000;
v /= 60;
m = v % 60;
v /= 60;
h = v % 24;
v /= 24;
d = v;
buf.append((int) d);
buf.append(' ');
number(buf, (int) h, 2);
buf.append(':');
number(buf, (int) m, 2);
break;
case DAY_TO_HOUR:
v = roundUp(v, 1000 * 60 * 60);
v /= 1000;
v /= 60;
v /= 60;
h = v % 24;
v /= 24;
d = v;
buf.append((int) d);
buf.append(' ');
number(buf, (int) h, 2);
break;
case DAY:
v = roundUp(v, 1000 * 60 * 60 * 24);
d = v / (1000 * 60 * 60 * 24);
buf.append((int) d);
break;
case HOUR:
v = roundUp(v, 1000 * 60 * 60);
v /= 1000;
v /= 60;
v /= 60;
h = v;
buf.append((int) h);
break;
case HOUR_TO_MINUTE:
v = roundUp(v, 1000 * 60);
v /= 1000;
v /= 60;
m = v % 60;
v /= 60;
h = v;
buf.append((int) h);
buf.append(':');
number(buf, (int) m, 2);
break;
case HOUR_TO_SECOND:
v = roundUp(v, powerX(10, 3 - scale));
ms = v % 1000;
v /= 1000;
s = v % 60;
v /= 60;
m = v % 60;
v /= 60;
h = v;
buf.append((int) h);
buf.append(':');
number(buf, (int) m, 2);
buf.append(':');
number(buf, (int) s, 2);
fraction(buf, scale, ms);
break;
case MINUTE_TO_SECOND:
v = roundUp(v, powerX(10, 3 - scale));
ms = v % 1000;
v /= 1000;
s = v % 60;
v /= 60;
m = v;
buf.append((int) m);
buf.append(':');
number(buf, (int) s, 2);
fraction(buf, scale, ms);
break;
case MINUTE:
v = roundUp(v, 1000 * 60);
v /= 1000;
v /= 60;
m = v;
buf.append((int) m);
break;
case SECOND:
v = roundUp(v, powerX(10, 3 - scale));
ms = v % 1000;
v /= 1000;
s = v;
buf.append((int) s);
fraction(buf, scale, ms);
break;
default:
throw new AssertionError(range);
}
return buf.toString();
}
/**
* Rounds a dividend to the nearest divisor.
* For example roundUp(31, 10) yields 30; roundUp(37, 10) yields 40.
* @param dividend Number to be divided
* @param divisor Number to divide by
* @return Rounded dividend
*/
private static long roundUp(long dividend, long divisor) {
long remainder = dividend % divisor;
dividend -= remainder;
if (remainder * 2 > divisor) {
dividend += divisor;
}
return dividend;
}
private static void fraction(StringBuilder buf, int scale, long ms) {
if (scale > 0) {
buf.append('.');
long v1 = scale == 3 ? ms
: scale == 2 ? ms / 10
: scale == 1 ? ms / 100
: 0;
number(buf, (int) v1, scale);
}
}
public static int dateStringToUnixDate(String s) {
int hyphen1 = s.indexOf('-');
int y;
int m;
int d;
if (hyphen1 < 0) {
y = Integer.parseInt(s.trim());
m = 1;
d = 1;
} else {
y = Integer.parseInt(s.substring(0, hyphen1).trim());
final int hyphen2 = s.indexOf('-', hyphen1 + 1);
if (hyphen2 < 0) {
m = Integer.parseInt(s.substring(hyphen1 + 1).trim());
d = 1;
} else {
m = Integer.parseInt(s.substring(hyphen1 + 1, hyphen2).trim());
d = Integer.parseInt(s.substring(hyphen2 + 1).trim());
}
}
return ymdToUnixDate(y, m, d);
}
public static int timeStringToUnixDate(String v) {
return timeStringToUnixDate(v, 0);
}
public static int timeStringToUnixDate(String v, int start) {
final int colon1 = v.indexOf(':', start);
int hour;
int minute;
int second;
int milli;
if (colon1 < 0) {
hour = Integer.parseInt(v.trim());
minute = 0;
second = 0;
milli = 0;
} else {
hour = Integer.parseInt(v.substring(start, colon1).trim());
final int colon2 = v.indexOf(':', colon1 + 1);
if (colon2 < 0) {
minute = Integer.parseInt(v.substring(colon1 + 1).trim());
second = 0;
milli = 0;
} else {
minute = Integer.parseInt(v.substring(colon1 + 1, colon2).trim());
int dot = v.indexOf('.', colon2);
if (dot < 0) {
second = Integer.parseInt(v.substring(colon2 + 1).trim());
milli = 0;
} else {
second = Integer.parseInt(v.substring(colon2 + 1, dot).trim());
milli = parseFraction(v.substring(dot + 1).trim(), 100);
}
}
}
return hour * (int) MILLIS_PER_HOUR
+ minute * (int) MILLIS_PER_MINUTE
+ second * (int) MILLIS_PER_SECOND
+ milli;
}
/** Parses a fraction, multiplying the first character by {@code multiplier},
* the second character by {@code multiplier / 10},
* the third character by {@code multiplier / 100}, and so forth.
*
* <p>For example, {@code parseFraction("1234", 100)} yields {@code 123}. */
private static int parseFraction(String v, int multiplier) {
int r = 0;
for (int i = 0; i < v.length(); i++) {
char c = v.charAt(i);
int x = c < '0' || c > '9' ? 0 : (c - '0');
r += multiplier * x;
if (multiplier < 10) {
// We're at the last digit. Check for rounding.
if (i + 1 < v.length()
&& v.charAt(i + 1) >= '5') {
++r;
}
break;
}
multiplier /= 10;
}
return r;
}
private static void validateDate(String s, String full) {
Matcher matcher = ISO_DATE_PATTERN.matcher(s);
if (matcher.find()) {
int year = Integer.parseInt(matcher.group(1));
int month = Integer.parseInt(matcher.group(2));
int day = Integer.parseInt(matcher.group(3));
if (day > daysInMonth(year, month)) {
throw fieldOutOfRange("DAY", full);
}
} else {
throw invalidType("DATE", full);
}
}
/** Returns the number of days in a month in the proleptic Gregorian calendar
* used by ISO-8601.
*
* <p>"Proleptic" means that we apply the calendar to dates before the
* Gregorian calendar was invented (in 1582). Thus, years 0 and 1200 are
* considered leap years, and 1500 is not. */
private static int daysInMonth(int year, int month) {
switch (month) {
case 9:
case 4:
case 6:
case 11:
// Thirty days hath September,
// April, June, and November,
return 30;
default:
// All the rest have thirty-one,
return 31;
case 2:
// Except February, twenty-eight days clear,
// And twenty-nine in each leap year.
return isLeapYear(year) ? 29 : 28;
}
}
/** Whether a year is considered a leap year in the proleptic Gregorian
* calendar. */
private static boolean isLeapYear(int year) {
return year % 4 == 0 && (year % 100 != 0 || year % 400 == 0);
}
private static void validateTime(String time, String full) {
Matcher matcher = ISO_TIME_PATTERN.matcher(time);
if (matcher.find()) {
int hour = Integer.parseInt(matcher.group(1));
if (hour > 23) {
throw fieldOutOfRange("HOUR", full);
}
} else {
throw invalidType("TIME", full);
}
}
private static IllegalArgumentException fieldOutOfRange(String field,
String full) {
return new IllegalArgumentException("Value of " + field
+ " field is out of range in '" + full + "'");
}
private static IllegalArgumentException invalidType(String type,
String full) {
return new IllegalArgumentException("Invalid " + type + " value, '"
+ full + "'");
}
public static long timestampStringToUnixDate(String s) {
try {
return timestampStringToUnixDate0(s);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(e.getMessage());
}
}
private static long timestampStringToUnixDate0(String s) {
final long d;
final long t;
s = s.trim();
int space = s.indexOf(' ');
if (space >= 0) {
String datePart = s.substring(0, space);
validateDate(datePart, s);
d = dateStringToUnixDate(datePart);
String timePart = s.substring(space + 1);
validateTime(timePart, s);
t = timeStringToUnixDate(timePart);
} else {
validateDate(s, s);
d = dateStringToUnixDate(s);
t = 0;
}
return d * MILLIS_PER_DAY + t;
}
public static long unixDateExtract(TimeUnitRange range, long date) {
switch (range) {
case EPOCH:
// no need to extract year/month/day, just multiply
return date * SECONDS_PER_DAY;
default:
return julianExtract(range, (int) date + EPOCH_JULIAN);
}
}
private static int julianExtract(TimeUnitRange range, int julian) {
// this shifts the epoch back to astronomical year -4800 instead of the
// start of the Christian era in year AD 1 of the proleptic Gregorian
// calendar.
int j = julian + 32044;
int g = j / 146097;
int dg = j % 146097;
int c = (dg / 36524 + 1) * 3 / 4;
int dc = dg - c * 36524;
int b = dc / 1461;
int db = dc % 1461;
int a = (db / 365 + 1) * 3 / 4;
int da = db - a * 365;
// integer number of full years elapsed since March 1, 4801 BC
int y = g * 400 + c * 100 + b * 4 + a;
// integer number of full months elapsed since the last March 1
int m = (da * 5 + 308) / 153 - 2;
// number of days elapsed since day 1 of the month
int d = da - (m + 4) * 153 / 5 + 122;
int year = y - 4800 + (m + 2) / 12;
int month = (m + 2) % 12 + 1;
int day = d + 1;
switch (range) {
case YEAR:
return year;
case ISOYEAR:
int weekNumber = getIso8601WeekNumber(julian, year, month, day);
if (weekNumber == 1 && month == 12) {
return year + 1;
} else if (month == 1 && weekNumber > 50) {
return year - 1;
}
return year;
case QUARTER:
return (month + 2) / 3;
case MONTH:
return month;
case DAY:
return day;
case DOW:
return Math.floorMod(julian + 1, 7) + 1; // sun=1, sat=7
case ISODOW:
return Math.floorMod(julian, 7) + 1; // mon=1, sun=7
case WEEK:
return getIso8601WeekNumber(julian, year, month, day);
case DOY:
final long janFirst = ymdToJulian(year, 1, 1);
return (int) (julian - janFirst) + 1;
case DECADE:
return year / 10;
case CENTURY:
return year > 0
? (year + 99) / 100
: (year - 99) / 100;
case MILLENNIUM:
return year > 0
? (year + 999) / 1000
: (year - 999) / 1000;
default:
throw new AssertionError(range);
}
}
/** Returns the first day of the first week of a year.
* Per ISO-8601 it is the Monday of the week that contains Jan 4,
* or equivalently, it is a Monday between Dec 29 and Jan 4.
* Sometimes it is in the year before the given year. */
private static long firstMondayOfFirstWeek(int year) {
final long janFirst = ymdToJulian(year, 1, 1);
final long janFirstDow = Math.floorMod(janFirst + 1, 7L); // sun=0, sat=6
return janFirst + (11 - janFirstDow) % 7 - 3;
}
/** Returns the ISO-8601 week number based on year, month, day.
* Per ISO-8601 it is the Monday of the week that contains Jan 4,
* or equivalently, it is a Monday between Dec 29 and Jan 4.
* Sometimes it is in the year before the given year, sometimes after. */
private static int getIso8601WeekNumber(int julian, int year, int month, int day) {
long fmofw = firstMondayOfFirstWeek(year);
if (month == 12 && day > 28) {
if (31 - day + 4 > 7 - (Math.floorMod(julian, 7) + 1)
&& 31 - day + Math.floorMod(julian, 7) + 1 >= 4) {
return (int) (julian - fmofw) / 7 + 1;
} else {
return 1;
}
} else if (month == 1 && day < 5) {
if (4 - day <= 7 - (Math.floorMod(julian, 7) + 1)
&& day - (Math.floorMod(julian, 7) + 1) >= -3) {
return 1;
} else {
return (int) (julian - firstMondayOfFirstWeek(year - 1)) / 7 + 1;
}
}
return (int) (julian - fmofw) / 7 + 1;
}
/** Extracts a time unit from a UNIX date (milliseconds since epoch). */
public static int unixTimestampExtract(TimeUnitRange range,
long timestamp) {
return unixTimeExtract(range,
(int) Math.floorMod(timestamp, MILLIS_PER_DAY));
}
/** Extracts a time unit from a time value (milliseconds since midnight). */
public static int unixTimeExtract(TimeUnitRange range, int time) {
assert time >= 0;
assert time < MILLIS_PER_DAY;
switch (range) {
case HOUR:
return time / (int) MILLIS_PER_HOUR;
case MINUTE:
final int minutes = time / (int) MILLIS_PER_MINUTE;
return minutes % 60;
case SECOND:
final int seconds = time / (int) MILLIS_PER_SECOND;
return seconds % 60;
default:
throw new AssertionError(range);
}
}
/** Resets to zero the "time" part of a timestamp. */
public static long resetTime(long timestamp) {
int date = (int) (timestamp / MILLIS_PER_DAY);
return (long) date * MILLIS_PER_DAY;
}
/** Resets to epoch (1970-01-01) the "date" part of a timestamp. */
public static long resetDate(long timestamp) {
return Math.floorMod(timestamp, MILLIS_PER_DAY);
}
public static long unixTimestampFloor(TimeUnitRange range, long timestamp) {
int date = (int) (timestamp / MILLIS_PER_DAY);
final int f = julianDateFloor(range, date + EPOCH_JULIAN, true);
return (long) f * MILLIS_PER_DAY;
}
public static long unixDateFloor(TimeUnitRange range, long date) {
return julianDateFloor(range, (int) date + EPOCH_JULIAN, true);
}
public static long unixTimestampCeil(TimeUnitRange range, long timestamp) {
int date = (int) (timestamp / MILLIS_PER_DAY);
final int f = julianDateFloor(range, date + EPOCH_JULIAN, false);
return (long) f * MILLIS_PER_DAY;
}
public static long unixDateCeil(TimeUnitRange range, long date) {
return julianDateFloor(range, (int) date + EPOCH_JULIAN, false);
}
private static int julianDateFloor(TimeUnitRange range, int julian,
boolean floor) {
// this shifts the epoch back to astronomical year -4800 instead of the
// start of the Christian era in year AD 1 of the proleptic Gregorian
// calendar.
int j = julian + 32044;
int g = j / 146097;
int dg = j % 146097;
int c = (dg / 36524 + 1) * 3 / 4;
int dc = dg - c * 36524;
int b = dc / 1461;
int db = dc % 1461;
int a = (db / 365 + 1) * 3 / 4;
int da = db - a * 365;
// integer number of full years elapsed since March 1, 4801 BC
int y = g * 400 + c * 100 + b * 4 + a;
// integer number of full months elapsed since the last March 1
int m = (da * 5 + 308) / 153 - 2;
// number of days elapsed since day 1 of the month
int d = da - (m + 4) * 153 / 5 + 122;
int year = y - 4800 + (m + 2) / 12;
int month = (m + 2) % 12 + 1;
int day = d + 1;
switch (range) {
case MILLENNIUM:
return floor
? ymdToUnixDate(1000 * ((year + 999) / 1000) - 999, 1, 1)
: ymdToUnixDate(1000 * ((year + 999) / 1000) + 1, 1, 1);
case CENTURY:
return floor
? ymdToUnixDate(100 * ((year + 99) / 100) - 99, 1, 1)
: ymdToUnixDate(100 * ((year + 99) / 100) + 1, 1, 1);
case DECADE:
return floor
? ymdToUnixDate(10 * (year / 10), 1, 1)
: ymdToUnixDate(10 * (1 + year / 10), 1, 1);
case YEAR:
if (!floor && (month > 1 || day > 1)) {
++year;
}
return ymdToUnixDate(year, 1, 1);
case ISOYEAR:
final int isoWeek = getIso8601WeekNumber(julian, year, month, day);
final int dowMon = Math.floorMod(julian, 7); // mon=0, sun=6
final int isoYearFloor = julian - 7 * (isoWeek - 1) - dowMon;
if (floor || isoYearFloor == julian) {
return isoYearFloor - EPOCH_JULIAN;
} else {
// CEIL of this date is the FLOOR of the date 53 weeks later.
// (Usually 52 weeks later, sometimes 53 weeks later.)
return julianDateFloor(range, isoYearFloor + 7 * 53, true);
}
case QUARTER:
final int q = (month - 1) / 3;
if (!floor) {
if (month - 1 > q * 3 || day > 1) {
if (q == 3) {
++year;
month = 1;
} else {
month = q * 3 + 4;
}
}
} else {
month = q * 3 + 1;
}
return ymdToUnixDate(year, month, 1);
case MONTH:
if (!floor && day > 1) {
++month;
}
return ymdToUnixDate(year, month, 1);
case WEEK:
final int dow = Math.floorMod(julian + 1, 7); // sun=0, sat=6
int offset = dow;
if (!floor && offset > 0) {
offset -= 7;
}
return ymdToUnixDate(year, month, day) - offset;
case DAY:
return ymdToUnixDate(year, month, day);
default:
throw new AssertionError(range);
}
}
public static int ymdToUnixDate(int year, int month, int day) {
final int julian = ymdToJulian(year, month, day);
return julian - EPOCH_JULIAN;
}
/** Calculates the Julian Day Number for any valid date in the Gregorian
* calendar.
*
* <p>If date is invalid, result is unspecified.
*
* <p>See an
* <a href="http://www.cs.utsa.edu/~cs1063/projects/Spring2011/Project1/jdn-explanation.html">
* explanation</a> of this algorithm.
*
* @param year Year (e.g. 2020 means 2020 CE, 1 means 1 CE, 0 means 1 BCE
* because there is no 0 CE, -1 means 2 BCE, etc.)
* @param month Month (between 1 and 12 inclusive, 1 meaning January)
* @param day Day of month (between 1 and 31 inclusive) */
public static int ymdToJulian(int year, int month, int day) {
int a = (14 - month) / 12;
int y = year + 4800 - a;
int m = month + 12 * a - 3;
return day + (153 * m + 2) / 5
+ 365 * y
+ y / 4
- y / 100
+ y / 400
- 32045;
}
public static long unixTimestamp(int year, int month, int day, int hour,
int minute, int second) {
final int date = ymdToUnixDate(year, month, day);
return (long) date * MILLIS_PER_DAY
+ (long) hour * MILLIS_PER_HOUR
+ (long) minute * MILLIS_PER_MINUTE
+ (long) second * MILLIS_PER_SECOND;
}
/** Adds a given number of months to a timestamp, represented as the number
* of milliseconds since the epoch. */
public static long addMonths(long timestamp, int m) {
final long millis =
Math.floorMod(timestamp, DateTimeUtils.MILLIS_PER_DAY);
timestamp -= millis;
final long x =
addMonths((int) (timestamp / DateTimeUtils.MILLIS_PER_DAY), m);
return x * DateTimeUtils.MILLIS_PER_DAY + millis;
}
/** Adds a given number of months to a date, represented as the number of
* days since the epoch. */
public static int addMonths(int date, int m) {
int y0 = (int) DateTimeUtils.unixDateExtract(TimeUnitRange.YEAR, date);
int m0 = (int) DateTimeUtils.unixDateExtract(TimeUnitRange.MONTH, date);
int d0 = (int) DateTimeUtils.unixDateExtract(TimeUnitRange.DAY, date);
m0 += m;
int deltaYear = Math.floorDiv(m0, 12);
y0 += deltaYear;
m0 = Math.floorMod(m0, 12);
if (m0 == 0) {
y0 -= 1;
m0 += 12;
}
int last = lastDay(y0, m0);
if (d0 > last) {
d0 = last;
}
return DateTimeUtils.ymdToUnixDate(y0, m0, d0);
}
/**
* SQL {@code LAST_DAY} function.
*
* @param date days since epoch
* @return days of the last day of the month since epoch
*/
public static int lastDay(int date) {
int y0 = (int) DateTimeUtils.unixDateExtract(TimeUnitRange.YEAR, date);
int m0 = (int) DateTimeUtils.unixDateExtract(TimeUnitRange.MONTH, date);
int last = lastDay(y0, m0);
return DateTimeUtils.ymdToUnixDate(y0, m0, last);
}
private static int lastDay(int y, int m) {
switch (m) {
case 2:
return y % 4 == 0
&& (y % 100 != 0
|| y % 400 == 0)
? 29 : 28;
case 4:
case 6:
case 9:
case 11:
return 30;
default:
return 31;
}
}
/** Finds the number of months between two dates, each represented as the
* number of days since the epoch. */
public static int subtractMonths(int date0, int date1) {
if (date0 < date1) {
return -subtractMonths(date1, date0);
}
// Start with an estimate.
// Since no month has more than 31 days, the estimate is <= the true value.
int m = (date0 - date1) / 31;
for (;;) {
int date2 = addMonths(date1, m);
if (date2 >= date0) {
return m;
}
int date3 = addMonths(date1, m + 1);
if (date3 > date0) {
return m;
}
++m;
}
}
public static int subtractMonths(long t0, long t1) {
final long millis0 = Math.floorMod(t0, DateTimeUtils.MILLIS_PER_DAY);
final int d0 =
(int) Math.floorDiv(t0 - millis0, DateTimeUtils.MILLIS_PER_DAY);
final long millis1 = Math.floorMod(t1, DateTimeUtils.MILLIS_PER_DAY);
final int d1 =
(int) Math.floorDiv(t1 - millis1, DateTimeUtils.MILLIS_PER_DAY);
int x = subtractMonths(d0, d1);
final long d2 = addMonths(d1, x);
if (d2 == d0 && millis0 < millis1) {
--x;
}
return x;
}
/** Divide, rounding towards negative infinity.
*
* @deprecated Use {@link Math#floorDiv(long, long)} */
@Deprecated // to be removed before 2.0
public static long floorDiv(long x, long y) {
long r = x / y;
// if the signs are different and modulo not zero, round down
if ((x ^ y) < 0 && r * y != x) {
r--;
}
return r;
}
/** Modulo, always returning a non-negative result.
*
* @deprecated Use {@link Math#floorMod(long, long)} */
@Deprecated // to be removed before 2.0
public static long floorMod(long x, long y) {
return x - floorDiv(x, y) * y;
}
/** Creates an instance of {@link Calendar} in the root locale and UTC time
* zone. */
public static Calendar calendar() {
return Calendar.getInstance(UTC_ZONE, Locale.ROOT);
}
/** Returns whether a value is an {@code OffsetDateTime}. */
public static boolean isOffsetDateTime(Object o) {
return OFFSET_DATE_TIME_HANDLER.isOffsetDateTime(o);
}
/** Returns the value of a {@code OffsetDateTime} as a string. */
public static String offsetDateTimeValue(Object o) {
return OFFSET_DATE_TIME_HANDLER.stringValue(o);
}
/**
* Calculates the unix date as the number of days since January 1st, 1970 UTC for the given SQL
* date.
*
* @see #sqlDateToUnixDate(java.sql.Date, TimeZone)
*/
public static int sqlDateToUnixDate(java.sql.Date date, Calendar calendar) {
return sqlDateToUnixDate(date, calendar != null ? calendar.getTimeZone() : null);
}
/**
* Calculates the unix date as the number of days since January 1st, 1970 UTC for the given SQL
* date.
*
* <p>The {@link java.sql.Date} class uses the standard Gregorian calendar which switches from
* the Julian calendar to the Gregorian calendar in October 1582. For compatibility with
* ISO-8601, the value is converted to a {@link LocalDate} which uses the proleptic Gregorian
* calendar.
*
* <p>For backwards compatibility, timezone offsets are calculated in relation to the local
* timezone instead of UTC. Providing the default timezone or {@code null} will return the date
* unmodified.
*
* <p>If the date contains a partial day, it will be rounded to a full day depending on the
* milliseconds value. If the milliseconds value is positive, it will be rounded down to the
* closest full day. If the milliseconds value is negative, it will be rounded up to the closest
* full day.
*/
public static int sqlDateToUnixDate(java.sql.Date date, TimeZone timeZone) {
final long time = date.getTime();
// Convert from standard Gregorian calendar to ISO calendar system
// Use a SQL timestamp to include the time offset from UTC in the unix timestamp
final LocalDateTime dateTime = new Timestamp(time).toLocalDateTime();
long unixTimestamp = dateTime.toEpochSecond(ZoneOffset.UTC)
* DateTimeUtils.MILLIS_PER_SECOND
+ dateTime.get(ChronoField.MILLI_OF_SECOND);
// Calculate timezone offset in relation to local time
if (timeZone != null) {
unixTimestamp += timeZone.getOffset(time);
}
unixTimestamp -= DEFAULT_ZONE.getOffset(time);
return (int) (unixTimestamp / DateTimeUtils.MILLIS_PER_DAY);
}
/**
* Converts the given unix date to a SQL date.
*
* <p>The unix date should be the number of days since January 1st, 1970 UTC using the proleptic
* Gregorian calendar as defined by ISO-8601. The returned {@link java.sql.Date} object will use
* the standard Gregorian calendar which switches from the Julian calendar to the Gregorian
* calendar in October 1582. This conversion is handled by the {@link java.sql.Date} class when
* converting from a {@link LocalDate} object.
*
* <p>For backwards compatibility, timezone offsets are calculated in relation to the local
* timezone instead of UTC. Providing the default timezone or {@code null} will return the date
* unmodified.
*/
public static java.sql.Date unixDateToSqlDate(int date, Calendar calendar) {
// Convert unix date from the ISO calendar system to the standard Gregorian calendar
final LocalDate localDate = LocalDate.ofEpochDay(date);
final java.sql.Date sqlDate = java.sql.Date.valueOf(localDate);
// Calculate timezone offset in relation to local time
final long time = sqlDate.getTime();
final int offset = calendar != null ? calendar.getTimeZone().getOffset(time) : 0;
sqlDate.setTime(time + DEFAULT_ZONE.getOffset(time) - offset);
return sqlDate;
}
/**
* Calculates the unix date as the number of milliseconds since January 1st, 1970 UTC for the
* given date.
*
* @see #utilDateToUnixTimestamp(Date, TimeZone)
*/
public static long utilDateToUnixTimestamp(Date date, Calendar calendar) {
return utilDateToUnixTimestamp(date, calendar != null ? calendar.getTimeZone() : null);
}
/**
* Calculates the unix date as the number of milliseconds since January 1st, 1970 UTC for the
* given date.
*
* <p>The {@link Date} class uses the standard Gregorian calendar which switches from the Julian
* calendar to the Gregorian calendar in October 1582. For compatibility with ISO-8601, the value
* is converted to a {@code java.time} object which uses the proleptic Gregorian calendar.
*
* <p>For backwards compatibility, timezone offsets are calculated in relation to the local
* timezone instead of UTC. Providing the default timezone or {@code null} will return the date
* unmodified.
*/
public static long utilDateToUnixTimestamp(Date date, TimeZone timeZone) {
final Timestamp timestamp = new Timestamp(date.getTime());
return sqlTimestampToUnixTimestamp(timestamp, timeZone);
}
/**
* Converts the given unix timestamp to a Java date.
*
* <p>The unix timestamp should be the number of milliseconds since January 1st, 1970 UTC using
* the proleptic Gregorian calendar as defined by ISO-8601. The returned {@link Date} object will
* use the standard Gregorian calendar which switches from the Julian calendar to the Gregorian
* calendar in October 1582. This conversion is handled by the {@code java.sql} package when
* converting from a {@code java.time} object.
*
* <p>For backwards compatibility, timezone offsets are calculated in relation to the local
* timezone instead of UTC. Providing the default timezone or {@code null} will return the date
* unmodified.
*/
public static Date unixTimestampToUtilDate(long timestamp, Calendar calendar) {
final Timestamp sqlTimestamp = unixTimestampToSqlTimestamp(timestamp, calendar);
return new Date(sqlTimestamp.getTime());
}
/**
* Calculates the unix time as the number of milliseconds since the previous day in UTC for the
* given SQL time.
*
* @see #sqlTimeToUnixTime(Time, TimeZone)
*/
public static int sqlTimeToUnixTime(Time time, Calendar calendar) {
return sqlTimeToUnixTime(time, calendar != null ? calendar.getTimeZone() : null);
}
/**
* Calculates the unix time as the number of milliseconds since the previous day in UTC for the
* given SQL time.
*
* <p>For backwards compatibility, timezone offsets are calculated in relation to the local
* timezone instead of UTC. Providing the default timezone or {@code null} will return the time
* unmodified.
*/
public static int sqlTimeToUnixTime(Time time, TimeZone timeZone) {
long unixTime = time.getTime();
if (timeZone != null) {
unixTime += timeZone.getOffset(unixTime);
}
return (int) Math.floorMod(unixTime, MILLIS_PER_DAY);
}
/**
* Converts the given unix time to a SQL time.
*
* <p>The unix time should be the number of milliseconds since the previous day in UTC.
*
* <p>For backwards compatibility, timezone offsets are calculated in relation to the local
* timezone instead of UTC. Providing the default timezone or {@code null} will return the time
* unmodified.
*/
public static Time unixTimeToSqlTime(int time, Calendar calendar) {
if (calendar != null) {
time -= calendar.getTimeZone().getOffset(time);
}
return new Time(time);
}
/**
* Calculates the unix date as the number of milliseconds since January 1st, 1970 UTC for the
* given SQL timestamp.
*
* @see #sqlTimestampToUnixTimestamp(Timestamp, TimeZone)
*/
public static long sqlTimestampToUnixTimestamp(Timestamp timestamp, Calendar calendar) {
return sqlTimestampToUnixTimestamp(timestamp, calendar != null ? calendar.getTimeZone() : null);
}
/**
* Calculates the unix date as the number of milliseconds since January 1st, 1970 UTC for the
* given SQL timestamp.
*
* <p>The {@link Timestamp} class uses the standard Gregorian calendar which switches from the
* Julian calendar to the Gregorian calendar in October 1582. For compatibility with ISO-8601,
* the value is converted to a {@link LocalDateTime} which uses the proleptic Gregorian calendar.
*
* <p>For backwards compatibility, timezone offsets are calculated in relation to the local
* timezone instead of UTC. Providing the default timezone or {@code null} will return the
* timestamp unmodified.
*/
public static long sqlTimestampToUnixTimestamp(Timestamp timestamp, TimeZone timeZone) {
final long time = timestamp.getTime();
// Convert SQL timestamp from standard Gregorian calendar to ISO calendar system
final LocalDateTime dateTime = timestamp.toLocalDateTime();
long unixTimestamp = dateTime.toEpochSecond(ZoneOffset.UTC)
* DateTimeUtils.MILLIS_PER_SECOND
+ dateTime.get(ChronoField.MILLI_OF_SECOND);
// Calculate timezone offset in relation to local time
if (timeZone != null) {
unixTimestamp += timeZone.getOffset(time);
}
unixTimestamp -= DEFAULT_ZONE.getOffset(time);
return unixTimestamp;
}
/**
* Converts the given unix timestamp to a SQL timestamp.
*
* <p>The unix timestamp should be the number of milliseconds since January 1st, 1970 UTC using
* the proleptic Gregorian calendar as defined by ISO-8601. The returned {@link Timestamp} object
* will use the standard Gregorian calendar which switches from the Julian calendar to the
* Gregorian calendar in October 1582. This conversion is handled by the {@link Timestamp} class
* when converting from a {@link LocalDateTime} object.
*
* <p>For backwards compatibility, timezone offsets are calculated in relation to the local
* timezone instead of UTC. Providing the default timezone or {@code null} will return the
* timestamp unmodified.
*/
public static Timestamp unixTimestampToSqlTimestamp(long timestamp, Calendar calendar) {
// Convert unix timestamp from the ISO calendar system to the standard Gregorian calendar
final LocalDateTime localDateTime = LocalDateTime.ofEpochSecond(
Math.floorDiv(timestamp, DateTimeUtils.MILLIS_PER_SECOND),
(int) (Math.floorMod(timestamp, DateTimeUtils.MILLIS_PER_SECOND) * NANOS_PER_MILLI),
ZoneOffset.UTC);
final Timestamp sqlTimestamp = Timestamp.valueOf(localDateTime);
// Calculate timezone offset in relation to local time
final long time = sqlTimestamp.getTime();
final int offset = calendar != null ? calendar.getTimeZone().getOffset(time) : 0;
sqlTimestamp.setTime(time + DEFAULT_ZONE.getOffset(time) - offset);
return sqlTimestamp;
}
//~ Inner Classes ----------------------------------------------------------
/**
* Helper class for {@link DateTimeUtils#parsePrecisionDateTimeLiteral}
*/
public static class PrecisionTime {
private final Calendar cal;
private final String fraction;
private final int precision;
public PrecisionTime(Calendar cal, String fraction, int precision) {
this.cal = cal;
this.fraction = fraction;
this.precision = precision;
}
public Calendar getCalendar() {
return cal;
}
public int getPrecision() {
return precision;
}
public String getFraction() {
return fraction;
}
}
/** Deals with values of {@code java.time.OffsetDateTime} without introducing
* a compile-time dependency (because {@code OffsetDateTime} is only JDK 8 and
* higher). */
private interface OffsetDateTimeHandler {
boolean isOffsetDateTime(Object o);
String stringValue(Object o);
}
/** Implementation of {@code OffsetDateTimeHandler} for environments where
* no instances are possible. */
private static class NoopOffsetDateTimeHandler
implements OffsetDateTimeHandler {
public boolean isOffsetDateTime(Object o) {
return false;
}
public String stringValue(Object o) {
throw new UnsupportedOperationException();
}
}
/** Implementation of {@code OffsetDateTimeHandler} for environments where
* no instances are possible. */
private static class ReflectiveOffsetDateTimeHandler
implements OffsetDateTimeHandler {
final Class offsetDateTimeClass;
private ReflectiveOffsetDateTimeHandler() throws ClassNotFoundException {
offsetDateTimeClass = Class.forName("java.time.OffsetDateTime");
}
public boolean isOffsetDateTime(Object o) {
return o != null && o.getClass() == offsetDateTimeClass;
}
public String stringValue(Object o) {
return o.toString();
}
}
}
// End DateTimeUtils.java