org.h2.util.DateTimeUtils Maven / Gradle / Ivy
/*
* Copyright 2004-2022 H2 Group. Multiple-Licensed under the MPL 2.0, and the
* EPL 1.0 (https://h2database.com/html/license.html).
* Initial Developer: H2 Group
* Iso8601: Initial Developer: Robert Rathsack (firstName dot lastName at gmx
* dot de)
*/
package org.h2.util;
import java.time.Instant;
import org.h2.api.ErrorCode;
import org.h2.engine.CastDataProvider;
import org.h2.message.DbException;
import org.h2.value.TypeInfo;
import org.h2.value.Value;
import org.h2.value.ValueDate;
import org.h2.value.ValueTime;
import org.h2.value.ValueTimeTimeZone;
import org.h2.value.ValueTimestamp;
import org.h2.value.ValueTimestampTimeZone;
/**
* This utility class contains time conversion functions.
*
* Date value: a bit field with bits for the year, month, and day. Absolute day:
* the day number (0 means 1970-01-01).
*/
public class DateTimeUtils {
/**
* The number of milliseconds per day.
*/
public static final long MILLIS_PER_DAY = 24 * 60 * 60 * 1000L;
/**
* The number of seconds per day.
*/
public static final long SECONDS_PER_DAY = 24 * 60 * 60;
/**
* The number of nanoseconds per second.
*/
public static final long NANOS_PER_SECOND = 1_000_000_000;
/**
* The number of nanoseconds per minute.
*/
public static final long NANOS_PER_MINUTE = 60 * NANOS_PER_SECOND;
/**
* The number of nanoseconds per hour.
*/
public static final long NANOS_PER_HOUR = 60 * NANOS_PER_MINUTE;
/**
* The number of nanoseconds per day.
*/
public static final long NANOS_PER_DAY = MILLIS_PER_DAY * 1_000_000;
/**
* The offset of year bits in date values.
*/
public static final int SHIFT_YEAR = 9;
/**
* The offset of month bits in date values.
*/
public static final int SHIFT_MONTH = 5;
/**
* Date value for 1970-01-01.
*/
public static final int EPOCH_DATE_VALUE = (1970 << SHIFT_YEAR) + (1 << SHIFT_MONTH) + 1;
/**
* Minimum possible date value.
*/
public static final long MIN_DATE_VALUE = (-1_000_000_000L << SHIFT_YEAR) + (1 << SHIFT_MONTH) + 1;
/**
* Maximum possible date value.
*/
public static final long MAX_DATE_VALUE = (1_000_000_000L << SHIFT_YEAR) + (12 << SHIFT_MONTH) + 31;
private static final int[] NORMAL_DAYS_PER_MONTH = { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
/**
* Multipliers for {@link #convertScale(long, int, long)} and
* {@link #appendNanos(StringBuilder, int)}.
*/
private static final int[] FRACTIONAL_SECONDS_TABLE = { 1_000_000_000, 100_000_000,
10_000_000, 1_000_000, 100_000, 10_000, 1_000, 100, 10, 1 };
private static volatile TimeZoneProvider LOCAL;
private DateTimeUtils() {
// utility class
}
/**
* Reset the cached calendar for default timezone, for example after
* changing the default timezone.
*/
public static void resetCalendar() {
LOCAL = null;
}
/**
* Get the time zone provider for the default time zone.
*
* @return the time zone provider for the default time zone
*/
public static TimeZoneProvider getTimeZone() {
TimeZoneProvider local = LOCAL;
if (local == null) {
LOCAL = local = TimeZoneProvider.getDefault();
}
return local;
}
/**
* Returns current timestamp.
*
* @param timeZone
* the time zone
* @return current timestamp
*/
public static ValueTimestampTimeZone currentTimestamp(TimeZoneProvider timeZone) {
return currentTimestamp(timeZone, Instant.now());
}
/**
* Returns current timestamp using the specified instant for its value.
*
* @param timeZone
* the time zone
* @param now
* timestamp source, must be greater than or equal to
* 1970-01-01T00:00:00Z
* @return current timestamp
*/
public static ValueTimestampTimeZone currentTimestamp(TimeZoneProvider timeZone, Instant now) {
/*
* This code intentionally does not support properly dates before UNIX
* epoch because such support is not required for current dates.
*/
long second = now.getEpochSecond();
int offset = timeZone.getTimeZoneOffsetUTC(second);
second += offset;
return ValueTimestampTimeZone.fromDateValueAndNanos(dateValueFromAbsoluteDay(second / SECONDS_PER_DAY),
second % SECONDS_PER_DAY * 1_000_000_000 + now.getNano(), offset);
}
/**
* Parse a date string. The format is: [+|-]year-month-day
* or [+|-]yyyyMMdd.
*
* @param s the string to parse
* @param start the parse index start
* @param end the parse index end
* @return the date value
* @throws IllegalArgumentException if there is a problem
*/
public static long parseDateValue(String s, int start, int end) {
if (s.charAt(start) == '+') {
// +year
start++;
}
// start at position 1 to support "-year"
int yEnd = s.indexOf('-', start + 1);
int mStart, mEnd, dStart;
if (yEnd > 0) {
// Standard [+|-]year-month-day format
mStart = yEnd + 1;
mEnd = s.indexOf('-', mStart);
if (mEnd <= mStart) {
throw new IllegalArgumentException(s);
}
dStart = mEnd + 1;
} else {
// Additional [+|-]yyyyMMdd format for compatibility
mEnd = dStart = end - 2;
yEnd = mStart = mEnd - 2;
// Accept only 3 or more digits in year for now
if (yEnd < start + 3) {
throw new IllegalArgumentException(s);
}
}
int year = Integer.parseInt(s.substring(start, yEnd));
int month = StringUtils.parseUInt31(s, mStart, mEnd);
int day = StringUtils.parseUInt31(s, dStart, end);
if (!isValidDate(year, month, day)) {
throw new IllegalArgumentException(year + "-" + month + "-" + day);
}
return dateValue(year, month, day);
}
/**
* Parse a time string. The format is: hour:minute[:second[.nanos]],
* hhmm[ss[.nanos]], or hour.minute.second[.nanos].
*
* @param s the string to parse
* @param start the parse index start
* @param end the parse index end
* @return the time in nanoseconds
* @throws IllegalArgumentException if there is a problem
*/
public static long parseTimeNanos(String s, int start, int end) {
int hour, minute, second, nanos;
int hEnd = s.indexOf(':', start);
int mStart, mEnd, sStart, sEnd;
if (hEnd > 0) {
mStart = hEnd + 1;
mEnd = s.indexOf(':', mStart);
if (mEnd >= mStart) {
// Standard hour:minute:second[.nanos] format
sStart = mEnd + 1;
sEnd = s.indexOf('.', sStart);
} else {
// Additional hour:minute format for compatibility
mEnd = end;
sStart = sEnd = -1;
}
} else {
int t = s.indexOf('.', start);
if (t < 0) {
// Additional hhmm[ss] format for compatibility
hEnd = mStart = start + 2;
mEnd = mStart + 2;
int len = end - start;
if (len == 6) {
sStart = mEnd;
sEnd = -1;
} else if (len == 4) {
sStart = sEnd = -1;
} else {
throw new IllegalArgumentException(s);
}
} else if (t >= start + 6) {
// Additional hhmmss.nanos format for compatibility
if (t - start != 6) {
throw new IllegalArgumentException(s);
}
hEnd = mStart = start + 2;
mEnd = sStart = mStart + 2;
sEnd = t;
} else {
// Additional hour.minute.second[.nanos] IBM DB2 time format
hEnd = t;
mStart = hEnd + 1;
mEnd = s.indexOf('.', mStart);
if (mEnd <= mStart) {
throw new IllegalArgumentException(s);
}
sStart = mEnd + 1;
sEnd = s.indexOf('.', sStart);
}
}
hour = StringUtils.parseUInt31(s, start, hEnd);
if (hour >= 24) {
throw new IllegalArgumentException(s);
}
minute = StringUtils.parseUInt31(s, mStart, mEnd);
if (sStart > 0) {
if (sEnd < 0) {
second = StringUtils.parseUInt31(s, sStart, end);
nanos = 0;
} else {
second = StringUtils.parseUInt31(s, sStart, sEnd);
nanos = parseNanos(s, sEnd + 1, end);
}
} else {
second = nanos = 0;
}
if (minute >= 60 || second >= 60) {
throw new IllegalArgumentException(s);
}
return ((((hour * 60L) + minute) * 60) + second) * NANOS_PER_SECOND + nanos;
}
/**
* Parse nanoseconds.
*
* @param s String to parse.
* @param start Begin position at the string to read.
* @param end End position at the string to read.
* @return Parsed nanoseconds.
*/
static int parseNanos(String s, int start, int end) {
if (start >= end) {
throw new IllegalArgumentException(s);
}
int nanos = 0, mul = 100_000_000;
do {
char c = s.charAt(start);
if (c < '0' || c > '9') {
throw new IllegalArgumentException(s);
}
nanos += mul * (c - '0');
// mul can become 0, but continue loop anyway to ensure that all
// remaining digits are valid
mul /= 10;
} while (++start < end);
return nanos;
}
/**
* Parses timestamp value from the specified string.
*
* @param s
* string to parse
* @param provider
* the cast information provider, may be {@code null} for
* Standard-compliant literals
* @param withTimeZone
* if {@code true} return {@link ValueTimestampTimeZone} instead of
* {@link ValueTimestamp}
* @return parsed timestamp
*/
public static Value parseTimestamp(String s, CastDataProvider provider, boolean withTimeZone) {
int dateEnd = s.indexOf(' ');
if (dateEnd < 0) {
// ISO 8601 compatibility
dateEnd = s.indexOf('T');
if (dateEnd < 0 && provider != null && provider.getMode().allowDB2TimestampFormat) {
// DB2 also allows dash between date and time
dateEnd = s.indexOf('-', s.indexOf('-', s.indexOf('-') + 1) + 1);
}
}
int timeStart;
if (dateEnd < 0) {
dateEnd = s.length();
timeStart = -1;
} else {
timeStart = dateEnd + 1;
}
long dateValue = parseDateValue(s, 0, dateEnd);
long nanos;
TimeZoneProvider tz = null;
if (timeStart < 0) {
nanos = 0;
} else {
dateEnd++;
int timeEnd;
if (s.endsWith("Z")) {
tz = TimeZoneProvider.UTC;
timeEnd = s.length() - 1;
} else {
int timeZoneStart = s.indexOf('+', dateEnd);
if (timeZoneStart < 0) {
timeZoneStart = s.indexOf('-', dateEnd);
}
if (timeZoneStart >= 0) {
// Allow [timeZoneName] part after time zone offset
int offsetEnd = s.indexOf('[', timeZoneStart + 1);
if (offsetEnd < 0) {
offsetEnd = s.length();
}
tz = TimeZoneProvider.ofId(s.substring(timeZoneStart, offsetEnd));
if (s.charAt(timeZoneStart - 1) == ' ') {
timeZoneStart--;
}
timeEnd = timeZoneStart;
} else {
timeZoneStart = s.indexOf(' ', dateEnd);
if (timeZoneStart > 0) {
tz = TimeZoneProvider.ofId(s.substring(timeZoneStart + 1));
timeEnd = timeZoneStart;
} else {
timeEnd = s.length();
}
}
}
nanos = parseTimeNanos(s, dateEnd, timeEnd);
}
if (withTimeZone) {
int tzSeconds;
if (tz == null) {
tz = provider != null ? provider.currentTimeZone() : DateTimeUtils.getTimeZone();
}
if (tz != TimeZoneProvider.UTC) {
tzSeconds = tz.getTimeZoneOffsetUTC(tz.getEpochSecondsFromLocal(dateValue, nanos));
} else {
tzSeconds = 0;
}
return ValueTimestampTimeZone.fromDateValueAndNanos(dateValue, nanos, tzSeconds);
} else if (tz != null) {
long seconds = tz.getEpochSecondsFromLocal(dateValue, nanos);
seconds += (provider != null ? provider.currentTimeZone() : DateTimeUtils.getTimeZone())
.getTimeZoneOffsetUTC(seconds);
dateValue = dateValueFromLocalSeconds(seconds);
nanos = nanos % 1_000_000_000 + nanosFromLocalSeconds(seconds);
}
return ValueTimestamp.fromDateValueAndNanos(dateValue, nanos);
}
/**
* Parses TIME WITH TIME ZONE value from the specified string.
*
* @param s
* string to parse
* @param provider
* the cast information provider, or {@code null}
* @return parsed time with time zone
*/
public static ValueTimeTimeZone parseTimeWithTimeZone(String s, CastDataProvider provider) {
int timeEnd;
TimeZoneProvider tz;
if (s.endsWith("Z")) {
tz = TimeZoneProvider.UTC;
timeEnd = s.length() - 1;
} else {
int timeZoneStart = s.indexOf('+', 1);
if (timeZoneStart < 0) {
timeZoneStart = s.indexOf('-', 1);
}
if (timeZoneStart >= 0) {
tz = TimeZoneProvider.ofId(s.substring(timeZoneStart));
if (s.charAt(timeZoneStart - 1) == ' ') {
timeZoneStart--;
}
timeEnd = timeZoneStart;
} else {
timeZoneStart = s.indexOf(' ', 1);
if (timeZoneStart > 0) {
tz = TimeZoneProvider.ofId(s.substring(timeZoneStart + 1));
timeEnd = timeZoneStart;
} else {
throw DbException.get(ErrorCode.INVALID_DATETIME_CONSTANT_2, "TIME WITH TIME ZONE", s);
}
}
if (!tz.hasFixedOffset()) {
throw DbException.get(ErrorCode.INVALID_DATETIME_CONSTANT_2, "TIME WITH TIME ZONE", s);
}
}
return ValueTimeTimeZone.fromNanos(parseTimeNanos(s, 0, timeEnd), tz.getTimeZoneOffsetUTC(0L));
}
/**
* Calculates the seconds since epoch for the specified date value,
* nanoseconds since midnight, and time zone offset.
* @param dateValue
* date value
* @param timeNanos
* nanoseconds since midnight
* @param offsetSeconds
* time zone offset in seconds
* @return seconds since epoch in UTC
*/
public static long getEpochSeconds(long dateValue, long timeNanos, int offsetSeconds) {
return absoluteDayFromDateValue(dateValue) * SECONDS_PER_DAY + timeNanos / NANOS_PER_SECOND - offsetSeconds;
}
/**
* Extracts date value and nanos of day from the specified value.
*
* @param value
* value to extract fields from
* @param provider
* the cast information provider
* @return array with date value and nanos of day
*/
public static long[] dateAndTimeFromValue(Value value, CastDataProvider provider) {
long dateValue = EPOCH_DATE_VALUE;
long timeNanos = 0;
if (value instanceof ValueTimestamp) {
ValueTimestamp v = (ValueTimestamp) value;
dateValue = v.getDateValue();
timeNanos = v.getTimeNanos();
} else if (value instanceof ValueDate) {
dateValue = ((ValueDate) value).getDateValue();
} else if (value instanceof ValueTime) {
timeNanos = ((ValueTime) value).getNanos();
} else if (value instanceof ValueTimestampTimeZone) {
ValueTimestampTimeZone v = (ValueTimestampTimeZone) value;
dateValue = v.getDateValue();
timeNanos = v.getTimeNanos();
} else if (value instanceof ValueTimeTimeZone) {
timeNanos = ((ValueTimeTimeZone) value).getNanos();
} else {
ValueTimestamp v = (ValueTimestamp) value.convertTo(TypeInfo.TYPE_TIMESTAMP, provider);
dateValue = v.getDateValue();
timeNanos = v.getTimeNanos();
}
return new long[] {dateValue, timeNanos};
}
/**
* Creates a new date-time value with the same type as original value. If
* original value is a ValueTimestampTimeZone or ValueTimeTimeZone, returned
* value will have the same time zone offset as original value.
*
* @param original
* original value
* @param dateValue
* date value for the returned value
* @param timeNanos
* nanos of day for the returned value
* @return new value with specified date value and nanos of day
*/
public static Value dateTimeToValue(Value original, long dateValue, long timeNanos) {
switch (original.getValueType()) {
case Value.DATE:
return ValueDate.fromDateValue(dateValue);
case Value.TIME:
return ValueTime.fromNanos(timeNanos);
case Value.TIME_TZ:
return ValueTimeTimeZone.fromNanos(timeNanos, ((ValueTimeTimeZone) original).getTimeZoneOffsetSeconds());
case Value.TIMESTAMP:
default:
return ValueTimestamp.fromDateValueAndNanos(dateValue, timeNanos);
case Value.TIMESTAMP_TZ:
return ValueTimestampTimeZone.fromDateValueAndNanos(dateValue, timeNanos,
((ValueTimestampTimeZone) original).getTimeZoneOffsetSeconds());
}
}
/**
* Returns day of week.
*
* @param dateValue
* the date value
* @param firstDayOfWeek
* first day of week, Monday as 1, Sunday as 7 or 0
* @return day of week
* @see #getIsoDayOfWeek(long)
*/
public static int getDayOfWeek(long dateValue, int firstDayOfWeek) {
return getDayOfWeekFromAbsolute(absoluteDayFromDateValue(dateValue), firstDayOfWeek);
}
/**
* Get the day of the week from the absolute day value.
*
* @param absoluteValue the absolute day
* @param firstDayOfWeek the first day of the week
* @return the day of week
*/
public static int getDayOfWeekFromAbsolute(long absoluteValue, int firstDayOfWeek) {
return absoluteValue >= 0 ? (int) ((absoluteValue - firstDayOfWeek + 11) % 7) + 1
: (int) ((absoluteValue - firstDayOfWeek - 2) % 7) + 7;
}
/**
* Returns number of day in year.
*
* @param dateValue
* the date value
* @return number of day in year
*/
public static int getDayOfYear(long dateValue) {
int m = monthFromDateValue(dateValue);
int a = (367 * m - 362) / 12 + dayFromDateValue(dateValue);
if (m > 2) {
a--;
long y = yearFromDateValue(dateValue);
if ((y & 3) != 0 || (y % 100 == 0 && y % 400 != 0)) {
a--;
}
}
return a;
}
/**
* Returns ISO day of week.
*
* @param dateValue
* the date value
* @return ISO day of week, Monday as 1 to Sunday as 7
* @see #getSundayDayOfWeek(long)
*/
public static int getIsoDayOfWeek(long dateValue) {
return getDayOfWeek(dateValue, 1);
}
/**
* Returns ISO number of week in year.
*
* @param dateValue
* the date value
* @return number of week in year
* @see #getIsoWeekYear(long)
* @see #getWeekOfYear(long, int, int)
*/
public static int getIsoWeekOfYear(long dateValue) {
return getWeekOfYear(dateValue, 1, 4);
}
/**
* Returns ISO week year.
*
* @param dateValue
* the date value
* @return ISO week year
* @see #getIsoWeekOfYear(long)
* @see #getWeekYear(long, int, int)
*/
public static int getIsoWeekYear(long dateValue) {
return getWeekYear(dateValue, 1, 4);
}
/**
* Returns day of week with Sunday as 1.
*
* @param dateValue
* the date value
* @return day of week, Sunday as 1 to Monday as 7
* @see #getIsoDayOfWeek(long)
*/
public static int getSundayDayOfWeek(long dateValue) {
return getDayOfWeek(dateValue, 0);
}
/**
* Returns number of week in year.
*
* @param dateValue
* the date value
* @param firstDayOfWeek
* first day of week, Monday as 1, Sunday as 7 or 0
* @param minimalDaysInFirstWeek
* minimal days in first week of year
* @return number of week in year
* @see #getIsoWeekOfYear(long)
*/
public static int getWeekOfYear(long dateValue, int firstDayOfWeek, int minimalDaysInFirstWeek) {
long abs = absoluteDayFromDateValue(dateValue);
int year = yearFromDateValue(dateValue);
long base = getWeekYearAbsoluteStart(year, firstDayOfWeek, minimalDaysInFirstWeek);
if (abs - base < 0) {
base = getWeekYearAbsoluteStart(year - 1, firstDayOfWeek, minimalDaysInFirstWeek);
} else if (monthFromDateValue(dateValue) == 12 && 24 + minimalDaysInFirstWeek < dayFromDateValue(dateValue)) {
if (abs >= getWeekYearAbsoluteStart(year + 1, firstDayOfWeek, minimalDaysInFirstWeek)) {
return 1;
}
}
return (int) ((abs - base) / 7) + 1;
}
/**
* Get absolute day of the first day in the week year.
*
* @param weekYear
* the week year
* @param firstDayOfWeek
* first day of week, Monday as 1, Sunday as 7 or 0
* @param minimalDaysInFirstWeek
* minimal days in first week of year
* @return absolute day of the first day in the week year
*/
public static long getWeekYearAbsoluteStart(int weekYear, int firstDayOfWeek, int minimalDaysInFirstWeek) {
long first = absoluteDayFromYear(weekYear);
int daysInFirstWeek = 8 - getDayOfWeekFromAbsolute(first, firstDayOfWeek);
long base = first + daysInFirstWeek;
if (daysInFirstWeek >= minimalDaysInFirstWeek) {
base -= 7;
}
return base;
}
/**
* Returns week year.
*
* @param dateValue
* the date value
* @param firstDayOfWeek
* first day of week, Monday as 1, Sunday as 7 or 0
* @param minimalDaysInFirstWeek
* minimal days in first week of year
* @return week year
* @see #getIsoWeekYear(long)
*/
public static int getWeekYear(long dateValue, int firstDayOfWeek, int minimalDaysInFirstWeek) {
long abs = absoluteDayFromDateValue(dateValue);
int year = yearFromDateValue(dateValue);
long base = getWeekYearAbsoluteStart(year, firstDayOfWeek, minimalDaysInFirstWeek);
if (abs < base) {
return year - 1;
} else if (monthFromDateValue(dateValue) == 12 && 24 + minimalDaysInFirstWeek < dayFromDateValue(dateValue)) {
if (abs >= getWeekYearAbsoluteStart(year + 1, firstDayOfWeek, minimalDaysInFirstWeek)) {
return year + 1;
}
}
return year;
}
/**
* Returns number of days in month.
*
* @param year the year
* @param month the month
* @return number of days in the specified month
*/
public static int getDaysInMonth(int year, int month) {
if (month != 2) {
return NORMAL_DAYS_PER_MONTH[month];
}
return (year & 3) == 0 && (year % 100 != 0 || year % 400 == 0) ? 29 : 28;
}
/**
* Verify if the specified date is valid.
*
* @param year the year
* @param month the month (January is 1)
* @param day the day (1 is the first of the month)
* @return true if it is valid
*/
public static boolean isValidDate(int year, int month, int day) {
return month >= 1 && month <= 12 && day >= 1 && day <= getDaysInMonth(year, month);
}
/**
* Get the year from a date value.
*
* @param x the date value
* @return the year
*/
public static int yearFromDateValue(long x) {
return (int) (x >>> SHIFT_YEAR);
}
/**
* Get the month from a date value.
*
* @param x the date value
* @return the month (1..12)
*/
public static int monthFromDateValue(long x) {
return (int) (x >>> SHIFT_MONTH) & 15;
}
/**
* Get the day of month from a date value.
*
* @param x the date value
* @return the day (1..31)
*/
public static int dayFromDateValue(long x) {
return (int) (x & 31);
}
/**
* Get the date value from a given date.
*
* @param year the year
* @param month the month (1..12)
* @param day the day (1..31)
* @return the date value
*/
public static long dateValue(long year, int month, int day) {
return (year << SHIFT_YEAR) | (month << SHIFT_MONTH) | day;
}
/**
* Get the date value from a given denormalized date with possible out of range
* values of month and/or day. Used after addition or subtraction month or years
* to (from) it to get a valid date.
*
* @param year
* the year
* @param month
* the month, if out of range month and year will be normalized
* @param day
* the day of the month, if out of range it will be saturated
* @return the date value
*/
public static long dateValueFromDenormalizedDate(long year, long month, int day) {
long mm1 = month - 1;
long yd = mm1 / 12;
if (mm1 < 0 && yd * 12 != mm1) {
yd--;
}
int y = (int) (year + yd);
int m = (int) (month - yd * 12);
if (day < 1) {
day = 1;
} else {
int max = getDaysInMonth(y, m);
if (day > max) {
day = max;
}
}
return dateValue(y, m, day);
}
/**
* Convert a local seconds to an encoded date.
*
* @param localSeconds the seconds since 1970-01-01
* @return the date value
*/
public static long dateValueFromLocalSeconds(long localSeconds) {
long absoluteDay = localSeconds / SECONDS_PER_DAY;
// Round toward negative infinity
if (localSeconds < 0 && (absoluteDay * SECONDS_PER_DAY != localSeconds)) {
absoluteDay--;
}
return dateValueFromAbsoluteDay(absoluteDay);
}
/**
* Convert a time in seconds in local time to the nanoseconds since midnight.
*
* @param localSeconds the seconds since 1970-01-01
* @return the nanoseconds
*/
public static long nanosFromLocalSeconds(long localSeconds) {
localSeconds %= SECONDS_PER_DAY;
if (localSeconds < 0) {
localSeconds += SECONDS_PER_DAY;
}
return localSeconds * NANOS_PER_SECOND;
}
/**
* Calculate the normalized nanos of day.
*
* @param nanos the nanoseconds (may be negative or larger than one day)
* @return the nanos of day within a day
*/
public static long normalizeNanosOfDay(long nanos) {
nanos %= NANOS_PER_DAY;
if (nanos < 0) {
nanos += NANOS_PER_DAY;
}
return nanos;
}
/**
* Calculate the absolute day for a January, 1 of the specified year.
*
* @param year
* the year
* @return the absolute day
*/
public static long absoluteDayFromYear(long year) {
long a = 365 * year - 719_528;
if (year >= 0) {
a += (year + 3) / 4 - (year + 99) / 100 + (year + 399) / 400;
} else {
a -= year / -4 - year / -100 + year / -400;
}
return a;
}
/**
* Calculate the absolute day from an encoded date value.
*
* @param dateValue the date value
* @return the absolute day
*/
public static long absoluteDayFromDateValue(long dateValue) {
return absoluteDay(yearFromDateValue(dateValue), monthFromDateValue(dateValue), dayFromDateValue(dateValue));
}
/**
* Calculate the absolute day.
*
* @param y year
* @param m month
* @param d day
* @return the absolute day
*/
static long absoluteDay(long y, int m, int d) {
long a = absoluteDayFromYear(y) + (367 * m - 362) / 12 + d - 1;
if (m > 2) {
a--;
if ((y & 3) != 0 || (y % 100 == 0 && y % 400 != 0)) {
a--;
}
}
return a;
}
/**
* Calculate the encoded date value from an absolute day.
*
* @param absoluteDay the absolute day
* @return the date value
*/
public static long dateValueFromAbsoluteDay(long absoluteDay) {
long d = absoluteDay + 719_468;
long a = 0;
if (d < 0) {
a = (d + 1) / 146_097 - 1;
d -= a * 146_097;
a *= 400;
}
long y = (400 * d + 591) / 146_097;
int day = (int) (d - (365 * y + y / 4 - y / 100 + y / 400));
if (day < 0) {
y--;
day = (int) (d - (365 * y + y / 4 - y / 100 + y / 400));
}
y += a;
int m = (day * 5 + 2) / 153;
day -= (m * 306 + 5) / 10 - 1;
if (m >= 10) {
y++;
m -= 12;
}
return dateValue(y, m + 3, day);
}
/**
* Return the next date value.
*
* @param dateValue
* the date value
* @return the next date value
*/
public static long incrementDateValue(long dateValue) {
int day = dayFromDateValue(dateValue);
if (day < 28) {
return dateValue + 1;
}
int year = yearFromDateValue(dateValue);
int month = monthFromDateValue(dateValue);
if (day < getDaysInMonth(year, month)) {
return dateValue + 1;
}
if (month < 12) {
month++;
} else {
month = 1;
year++;
}
return dateValue(year, month, 1);
}
/**
* Return the previous date value.
*
* @param dateValue
* the date value
* @return the previous date value
*/
public static long decrementDateValue(long dateValue) {
if (dayFromDateValue(dateValue) > 1) {
return dateValue - 1;
}
int year = yearFromDateValue(dateValue);
int month = monthFromDateValue(dateValue);
if (month > 1) {
month--;
} else {
month = 12;
year--;
}
return dateValue(year, month, getDaysInMonth(year, month));
}
/**
* Append a date to the string builder.
*
* @param builder the target string builder
* @param dateValue the date value
* @return the specified string builder
*/
public static StringBuilder appendDate(StringBuilder builder, long dateValue) {
int y = yearFromDateValue(dateValue);
if (y < 1_000 && y > -1_000) {
if (y < 0) {
builder.append('-');
y = -y;
}
StringUtils.appendZeroPadded(builder, 4, y);
} else {
builder.append(y);
}
StringUtils.appendTwoDigits(builder.append('-'), monthFromDateValue(dateValue)).append('-');
return StringUtils.appendTwoDigits(builder, dayFromDateValue(dateValue));
}
/**
* Append a time to the string builder.
*
* @param builder the target string builder
* @param nanos the time in nanoseconds
* @return the specified string builder
*/
public static StringBuilder appendTime(StringBuilder builder, long nanos) {
if (nanos < 0) {
builder.append('-');
nanos = -nanos;
}
/*
* nanos now either in range from 0 to Long.MAX_VALUE or equals to
* Long.MIN_VALUE. We need to divide nanos by 1,000,000,000 with
* unsigned division to get correct result. The simplest way to do this
* with such constraints is to divide -nanos by -1,000,000,000.
*/
long s = -nanos / -1_000_000_000;
nanos -= s * 1_000_000_000;
int m = (int) (s / 60);
s -= m * 60;
int h = m / 60;
m -= h * 60;
StringUtils.appendTwoDigits(builder, h).append(':');
StringUtils.appendTwoDigits(builder, m).append(':');
StringUtils.appendTwoDigits(builder, (int) s);
return appendNanos(builder, (int) nanos);
}
/**
* Append nanoseconds of time, if any.
*
* @param builder string builder to append to
* @param nanos nanoseconds of second
* @return the specified string builder
*/
static StringBuilder appendNanos(StringBuilder builder, int nanos) {
if (nanos > 0) {
builder.append('.');
for (int i = 1; nanos < FRACTIONAL_SECONDS_TABLE[i]; i++) {
builder.append('0');
}
if (nanos % 1_000 == 0) {
nanos /= 1_000;
if (nanos % 1_000 == 0) {
nanos /= 1_000;
}
}
if (nanos % 10 == 0) {
nanos /= 10;
if (nanos % 10 == 0) {
nanos /= 10;
}
}
builder.append(nanos);
}
return builder;
}
/**
* Append a time zone to the string builder.
*
* @param builder the target string builder
* @param tz the time zone offset in seconds
* @return the specified string builder
*/
public static StringBuilder appendTimeZone(StringBuilder builder, int tz) {
if (tz < 0) {
builder.append('-');
tz = -tz;
} else {
builder.append('+');
}
int rem = tz / 3_600;
StringUtils.appendTwoDigits(builder, rem);
tz -= rem * 3_600;
if (tz != 0) {
rem = tz / 60;
StringUtils.appendTwoDigits(builder.append(':'), rem);
tz -= rem * 60;
if (tz != 0) {
StringUtils.appendTwoDigits(builder.append(':'), tz);
}
}
return builder;
}
/**
* Generates time zone name for the specified offset in seconds.
*
* @param offsetSeconds
* time zone offset in seconds
* @return time zone name
*/
public static String timeZoneNameFromOffsetSeconds(int offsetSeconds) {
if (offsetSeconds == 0) {
return "UTC";
}
StringBuilder b = new StringBuilder(12);
b.append("GMT");
if (offsetSeconds < 0) {
b.append('-');
offsetSeconds = -offsetSeconds;
} else {
b.append('+');
}
StringUtils.appendTwoDigits(b, offsetSeconds / 3_600).append(':');
offsetSeconds %= 3_600;
StringUtils.appendTwoDigits(b, offsetSeconds / 60);
offsetSeconds %= 60;
if (offsetSeconds != 0) {
b.append(':');
StringUtils.appendTwoDigits(b, offsetSeconds);
}
return b.toString();
}
/**
* Converts scale of nanoseconds.
*
* @param nanosOfDay nanoseconds of day
* @param scale fractional seconds precision
* @param range the allowed range of values (0..range-1)
* @return scaled value
*/
public static long convertScale(long nanosOfDay, int scale, long range) {
if (scale >= 9) {
return nanosOfDay;
}
int m = FRACTIONAL_SECONDS_TABLE[scale];
long mod = nanosOfDay % m;
if (mod >= m >>> 1) {
nanosOfDay += m;
}
long r = nanosOfDay - mod;
if (r >= range) {
r = range - m;
}
return r;
}
/**
* Moves timestamp with time zone to a new time zone.
*
* @param dateValue the date value
* @param timeNanos the nanoseconds since midnight
* @param oldOffset old offset
* @param newOffset new offset
* @return timestamp with time zone with new offset
*/
public static ValueTimestampTimeZone timestampTimeZoneAtOffset(long dateValue, long timeNanos, int oldOffset,
int newOffset) {
timeNanos += (newOffset - oldOffset) * DateTimeUtils.NANOS_PER_SECOND;
// Value can be 18+18 hours before or after the limit
if (timeNanos < 0) {
timeNanos += DateTimeUtils.NANOS_PER_DAY;
dateValue = DateTimeUtils.decrementDateValue(dateValue);
if (timeNanos < 0) {
timeNanos += DateTimeUtils.NANOS_PER_DAY;
dateValue = DateTimeUtils.decrementDateValue(dateValue);
}
} else if (timeNanos >= DateTimeUtils.NANOS_PER_DAY) {
timeNanos -= DateTimeUtils.NANOS_PER_DAY;
dateValue = DateTimeUtils.incrementDateValue(dateValue);
if (timeNanos >= DateTimeUtils.NANOS_PER_DAY) {
timeNanos -= DateTimeUtils.NANOS_PER_DAY;
dateValue = DateTimeUtils.incrementDateValue(dateValue);
}
}
return ValueTimestampTimeZone.fromDateValueAndNanos(dateValue, timeNanos, newOffset);
}
}