org.postgresql.jdbc.TimestampUtils Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of jdbc-yugabytedb Show documentation
Show all versions of jdbc-yugabytedb Show documentation
Java JDBC 4.2 (JRE 8+) driver for YugaByte SQL database
The newest version!
/*
* Copyright (c) 2003, PostgreSQL Global Development Group
* See the LICENSE file in the project root for more information.
*/
package org.postgresql.jdbc;
import static org.postgresql.util.internal.Nullness.castNonNull;
import org.postgresql.PGStatement;
import org.postgresql.core.JavaVersion;
import org.postgresql.core.Oid;
import org.postgresql.core.Provider;
import org.postgresql.util.ByteConverter;
import org.postgresql.util.GT;
import org.postgresql.util.PSQLException;
import org.postgresql.util.PSQLState;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.checkerframework.checker.nullness.qual.PolyNull;
import java.lang.reflect.Field;
import java.sql.Date;
import java.sql.SQLException;
import java.sql.Time;
import java.sql.Timestamp;
import java.time.Duration;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.OffsetTime;
import java.time.ZoneOffset;
import java.time.chrono.IsoEra;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoField;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.Objects;
import java.util.SimpleTimeZone;
import java.util.TimeZone;
/**
* Misc utils for handling time and date values.
*/
public class TimestampUtils {
/**
* Number of milliseconds in one day.
*/
private static final int ONEDAY = 24 * 3600 * 1000;
private static final char[] ZEROS = {'0', '0', '0', '0', '0', '0', '0', '0', '0'};
private static final char[][] NUMBERS;
private static final HashMap GMT_ZONES = new HashMap<>();
private static final int MAX_NANOS_BEFORE_WRAP_ON_ROUND = 999999500;
private static final Duration ONE_MICROSECOND = Duration.ofNanos(1000);
// LocalTime.MAX is 23:59:59.999_999_999, and it wraps to 24:00:00 when nanos exceed 999_999_499
// since PostgreSQL has microsecond resolution only
private static final LocalTime MAX_TIME = LocalTime.MAX.minus(Duration.ofNanos(500));
private static final OffsetDateTime MAX_OFFSET_DATETIME = OffsetDateTime.MAX.minus(Duration.ofMillis(500));
private static final LocalDateTime MAX_LOCAL_DATETIME = LocalDateTime.MAX.minus(Duration.ofMillis(500));
// low value for dates is 4713 BC
private static final LocalDate MIN_LOCAL_DATE = LocalDate.of(4713, 1, 1).with(ChronoField.ERA, IsoEra.BCE.getValue());
private static final LocalDateTime MIN_LOCAL_DATETIME = MIN_LOCAL_DATE.atStartOfDay();
private static final OffsetDateTime MIN_OFFSET_DATETIME = MIN_LOCAL_DATETIME.atOffset(ZoneOffset.UTC);
private static final Duration PG_EPOCH_DIFF =
Duration.between(Instant.EPOCH, LocalDate.of(2000, 1, 1).atStartOfDay().toInstant(ZoneOffset.UTC));
private static final @Nullable Field DEFAULT_TIME_ZONE_FIELD;
private static final TimeZone UTC_TIMEZONE = TimeZone.getTimeZone(ZoneOffset.UTC);
private @Nullable TimeZone prevDefaultZoneFieldValue;
private @Nullable TimeZone defaultTimeZoneCache;
static {
// The expected maximum value is 60 (seconds), so 64 is used "just in case"
NUMBERS = new char[64][];
for (int i = 0; i < NUMBERS.length; i++) {
NUMBERS[i] = ((i < 10 ? "0" : "") + Integer.toString(i)).toCharArray();
}
// Backend's gmt-3 means GMT+03 in Java. Here a map is created so gmt-3 can be converted to
// java TimeZone
for (int i = -12; i <= 14; i++) {
TimeZone timeZone;
String pgZoneName;
if (i == 0) {
timeZone = TimeZone.getTimeZone("GMT");
pgZoneName = "GMT";
} else {
timeZone = TimeZone.getTimeZone("GMT" + (i <= 0 ? "+" : "-") + Math.abs(i));
pgZoneName = "GMT" + (i >= 0 ? "+" : "-");
}
if (i == 0) {
GMT_ZONES.put(pgZoneName, timeZone);
continue;
}
GMT_ZONES.put(pgZoneName + Math.abs(i), timeZone);
GMT_ZONES.put(pgZoneName + new String(NUMBERS[Math.abs(i)]), timeZone);
}
// Fast path to getting the default timezone.
// Accessing the default timezone over and over creates a clone with regular API.
// Because we don't mutate that object in our use of it, we can access the field directly.
// This saves the creation of a clone everytime, and the memory associated to all these clones.
Field tzField;
try {
tzField = null;
// Avoid reflective access in Java 9+
if (JavaVersion.getRuntimeVersion().compareTo(JavaVersion.v1_8) <= 0) {
tzField = TimeZone.class.getDeclaredField("defaultTimeZone");
tzField.setAccessible(true);
TimeZone defaultTz = TimeZone.getDefault();
Object tzFromField = tzField.get(null);
if (defaultTz == null || !defaultTz.equals(tzFromField)) {
tzField = null;
}
}
} catch (Exception e) {
tzField = null;
}
DEFAULT_TIME_ZONE_FIELD = tzField;
}
private final StringBuilder sbuf = new StringBuilder();
// This calendar is used when user provides calendar in setX(, Calendar) method.
// It ensures calendar is Gregorian.
private final Calendar calendarWithUserTz = new GregorianCalendar();
private @Nullable Calendar calCache;
private @Nullable ZoneOffset calCacheZone;
/**
* True if the backend uses doubles for time values. False if long is used.
*/
private final boolean usesDouble;
private final Provider timeZoneProvider;
private final ResourceLock lock = new ResourceLock();
public TimestampUtils(boolean usesDouble, Provider timeZoneProvider) {
this.usesDouble = usesDouble;
this.timeZoneProvider = timeZoneProvider;
}
private Calendar getCalendar(ZoneOffset offset) {
if (calCache != null && Objects.equals(offset, calCacheZone)) {
return calCache;
}
// normally we would use:
// calCache = new GregorianCalendar(TimeZone.getTimeZone(offset));
// But this seems to cause issues for some crazy offsets as returned by server for BC dates!
final String tzid = offset.getTotalSeconds() == 0 ? "UTC" : "GMT".concat(offset.getId());
final TimeZone syntheticTZ = new SimpleTimeZone(offset.getTotalSeconds() * 1000, tzid);
calCache = new GregorianCalendar(syntheticTZ);
calCacheZone = offset;
return calCache;
}
private static class ParsedTimestamp {
boolean hasDate;
int era = GregorianCalendar.AD;
int year = 1970;
int month = 1;
boolean hasTime;
int day = 1;
int hour;
int minute;
int second;
int nanos;
boolean hasOffset;
ZoneOffset offset = ZoneOffset.UTC;
}
private static class ParsedBinaryTimestamp {
@Nullable Infinity infinity;
long millis;
int nanos;
}
enum Infinity {
POSITIVE,
NEGATIVE
}
/**
* Load date/time information into the provided calendar returning the fractional seconds.
*/
private ParsedTimestamp parseBackendTimestamp(String str) throws SQLException {
char[] s = str.toCharArray();
int slen = s.length;
// This is pretty gross..
ParsedTimestamp result = new ParsedTimestamp();
// We try to parse these fields in order; all are optional
// (but some combinations don't make sense, e.g. if you have
// both date and time then they must be whitespace-separated).
// At least one of date and time must be present.
// leading whitespace
// yyyy-mm-dd
// whitespace
// hh:mm:ss
// whitespace
// timezone in one of the formats: +hh, -hh, +hh:mm, -hh:mm
// whitespace
// if date is present, an era specifier: AD or BC
// trailing whitespace
try {
int start = skipWhitespace(s, 0); // Skip leading whitespace
int end = firstNonDigit(s, start);
int num;
char sep;
// Possibly read date.
if (charAt(s, end) == '-') {
//
// Date
//
result.hasDate = true;
// year
result.year = number(s, start, end);
start = end + 1; // Skip '-'
// month
end = firstNonDigit(s, start);
result.month = number(s, start, end);
sep = charAt(s, end);
if (sep != '-') {
throw new NumberFormatException("Expected date to be dash-separated, got '" + sep + "'");
}
start = end + 1; // Skip '-'
// day of month
end = firstNonDigit(s, start);
result.day = number(s, start, end);
start = skipWhitespace(s, end); // Skip trailing whitespace
}
// Possibly read time.
if (Character.isDigit(charAt(s, start))) {
//
// Time.
//
result.hasTime = true;
// Hours
end = firstNonDigit(s, start);
result.hour = number(s, start, end);
sep = charAt(s, end);
if (sep != ':') {
throw new NumberFormatException("Expected time to be colon-separated, got '" + sep + "'");
}
start = end + 1; // Skip ':'
// minutes
end = firstNonDigit(s, start);
result.minute = number(s, start, end);
sep = charAt(s, end);
if (sep != ':') {
throw new NumberFormatException("Expected time to be colon-separated, got '" + sep + "'");
}
start = end + 1; // Skip ':'
// seconds
end = firstNonDigit(s, start);
result.second = number(s, start, end);
start = end;
// Fractional seconds.
if (charAt(s, start) == '.') {
end = firstNonDigit(s, start + 1); // Skip '.'
num = number(s, start + 1, end);
for (int numlength = end - (start + 1); numlength < 9; numlength++) {
num *= 10;
}
result.nanos = num;
start = end;
}
start = skipWhitespace(s, start); // Skip trailing whitespace
}
// Possibly read timezone.
sep = charAt(s, start);
if (sep == '-' || sep == '+') {
result.hasOffset = true;
int tzsign = sep == '-' ? -1 : 1;
int tzhr;
int tzmin;
int tzsec;
end = firstNonDigit(s, start + 1); // Skip +/-
tzhr = number(s, start + 1, end);
start = end;
sep = charAt(s, start);
if (sep == ':') {
end = firstNonDigit(s, start + 1); // Skip ':'
tzmin = number(s, start + 1, end);
start = end;
} else {
tzmin = 0;
}
tzsec = 0;
sep = charAt(s, start);
if (sep == ':') {
end = firstNonDigit(s, start + 1); // Skip ':'
tzsec = number(s, start + 1, end);
start = end;
}
result.offset = ZoneOffset.ofHoursMinutesSeconds(tzsign * tzhr, tzsign * tzmin, tzsign * tzsec);
start = skipWhitespace(s, start); // Skip trailing whitespace
}
if (result.hasDate && start < slen) {
String eraString = new String(s, start, slen - start);
if (eraString.startsWith("AD")) {
result.era = GregorianCalendar.AD;
start += 2;
} else if (eraString.startsWith("BC")) {
result.era = GregorianCalendar.BC;
start += 2;
}
}
if (start < slen) {
throw new NumberFormatException(
"Trailing junk on timestamp: '" + new String(s, start, slen - start) + "'");
}
if (!result.hasTime && !result.hasDate) {
throw new NumberFormatException("Timestamp has neither date nor time");
}
} catch (NumberFormatException nfe) {
throw new PSQLException(
GT.tr("Bad value for type timestamp/date/time: {0}", str),
PSQLState.BAD_DATETIME_FORMAT, nfe);
}
return result;
}
/**
* Parse a string and return a timestamp representing its value.
*
* @param cal calendar to be used to parse the input string
* @param s The ISO formated date string to parse.
* @return null if s is null or a timestamp of the parsed string s.
* @throws SQLException if there is a problem parsing s.
*/
public @PolyNull Timestamp toTimestamp(@Nullable Calendar cal,
@PolyNull String s) throws SQLException {
try (ResourceLock ignore = lock.obtain()) {
if (s == null) {
return null;
}
int slen = s.length();
// convert postgres's infinity values to internal infinity magic value
if (slen == 8 && "infinity".equals(s)) {
return new Timestamp(PGStatement.DATE_POSITIVE_INFINITY);
}
if (slen == 9 && "-infinity".equals(s)) {
return new Timestamp(PGStatement.DATE_NEGATIVE_INFINITY);
}
ParsedTimestamp ts = parseBackendTimestamp(s);
Calendar useCal = ts.hasOffset ? getCalendar(ts.offset) : setupCalendar(cal);
useCal.set(Calendar.ERA, ts.era);
useCal.set(Calendar.YEAR, ts.year);
useCal.set(Calendar.MONTH, ts.month - 1);
useCal.set(Calendar.DAY_OF_MONTH, ts.day);
useCal.set(Calendar.HOUR_OF_DAY, ts.hour);
useCal.set(Calendar.MINUTE, ts.minute);
useCal.set(Calendar.SECOND, ts.second);
useCal.set(Calendar.MILLISECOND, 0);
Timestamp result = new Timestamp(useCal.getTimeInMillis());
result.setNanos(ts.nanos);
return result;
}
}
/**
* Parse a string and return a LocalTime representing its value.
*
* @param s The ISO formated time string to parse.
* @return null if s is null or a LocalTime of the parsed string s.
* @throws SQLException if there is a problem parsing s.
*/
public @PolyNull LocalTime toLocalTime(@PolyNull String s) throws SQLException {
if (s == null) {
return null;
}
if ("24:00:00".equals(s)) {
return LocalTime.MAX;
}
try {
return LocalTime.parse(s);
} catch (DateTimeParseException nfe) {
throw new PSQLException(
GT.tr("Bad value for type timestamp/date/time: {0}", s),
PSQLState.BAD_DATETIME_FORMAT, nfe);
}
}
/**
* Returns the offset time object matching the given bytes with Oid#TIMETZ or Oid#TIME.
*
* @param bytes The binary encoded TIMETZ/TIME value.
* @return The parsed offset time object.
* @throws PSQLException If binary format could not be parsed.
*/
public OffsetTime toOffsetTimeBin(byte[] bytes) throws PSQLException {
if (bytes.length != 12) {
throw new PSQLException(GT.tr("Unsupported binary encoding of {0}.", "time"),
PSQLState.BAD_DATETIME_FORMAT);
}
final long micros;
if (usesDouble) {
double seconds = ByteConverter.float8(bytes, 0);
micros = (long) (seconds * 1_000_000d);
} else {
micros = ByteConverter.int8(bytes, 0);
}
// postgres offset is negative, so we have to flip sign:
final ZoneOffset timeOffset = ZoneOffset.ofTotalSeconds(-ByteConverter.int4(bytes, 8));
return OffsetTime.of(LocalTime.ofNanoOfDay(Math.multiplyExact(micros, 1000L)), timeOffset);
}
/**
* Parse a string and return a OffsetTime representing its value.
*
* @param s The ISO formated time string to parse.
* @return null if s is null or a OffsetTime of the parsed string s.
* @throws SQLException if there is a problem parsing s.
*/
public @PolyNull OffsetTime toOffsetTime(@PolyNull String s) throws SQLException {
if (s == null) {
return null;
}
if (s.startsWith("24:00:00")) {
return OffsetTime.MAX;
}
final ParsedTimestamp ts = parseBackendTimestamp(s);
return OffsetTime.of(ts.hour, ts.minute, ts.second, ts.nanos, ts.offset);
}
/**
* Parse a string and return a LocalDateTime representing its value.
*
* @param s The ISO formated date string to parse.
* @return null if s is null or a LocalDateTime of the parsed string s.
* @throws SQLException if there is a problem parsing s.
*/
public @PolyNull LocalDateTime toLocalDateTime(@PolyNull String s) throws SQLException {
if (s == null) {
return null;
}
int slen = s.length();
// convert postgres's infinity values to internal infinity magic value
if (slen == 8 && "infinity".equals(s)) {
return LocalDateTime.MAX;
}
if (slen == 9 && "-infinity".equals(s)) {
return LocalDateTime.MIN;
}
ParsedTimestamp ts = parseBackendTimestamp(s);
// intentionally ignore time zone
// 2004-10-19 10:23:54+03:00 is 2004-10-19 10:23:54 locally
LocalDateTime result = LocalDateTime.of(ts.year, ts.month, ts.day, ts.hour, ts.minute, ts.second, ts.nanos);
if (ts.era == GregorianCalendar.BC) {
return result.with(ChronoField.ERA, IsoEra.BCE.getValue());
} else {
return result;
}
}
/**
* Returns the offset date time object matching the given bytes with Oid#TIMETZ.
* Not used internally anymore, function is here to retain compatibility with previous versions
*
* @param t the time value
* @return the matching offset date time
* @deprecated was used internally, and not used anymore
*/
@Deprecated
public OffsetDateTime toOffsetDateTime(Time t) {
// hardcode utc because the backend does not provide us the timezone
// hardcode UNIX epoch, JDBC requires OffsetDateTime but doesn't describe what date should be used
return t.toLocalTime().atDate(LocalDate.of(1970, 1, 1)).atOffset(ZoneOffset.UTC);
}
/**
* Parse a string and return a OffsetDateTime representing its value.
*
* @param s The ISO formatted date string to parse.
* @return null if s is null or a OffsetDateTime of the parsed string s.
* @throws SQLException if there is a problem parsing s.
*/
public @PolyNull OffsetDateTime toOffsetDateTime(
@PolyNull String s) throws SQLException {
if (s == null) {
return null;
}
int slen = s.length();
// convert postgres's infinity values to internal infinity magic value
if (slen == 8 && "infinity".equals(s)) {
return OffsetDateTime.MAX;
}
if (slen == 9 && "-infinity".equals(s)) {
return OffsetDateTime.MIN;
}
final ParsedTimestamp ts = parseBackendTimestamp(s);
OffsetDateTime result =
OffsetDateTime.of(ts.year, ts.month, ts.day, ts.hour, ts.minute, ts.second, ts.nanos, ts.offset);
if (ts.era == GregorianCalendar.BC) {
return result.with(ChronoField.ERA, IsoEra.BCE.getValue());
} else {
return result;
}
}
/**
* Returns the offset date time object matching the given bytes with Oid#TIMESTAMPTZ.
*
* @param bytes The binary encoded local date time value.
* @return The parsed local date time object.
* @throws PSQLException If binary format could not be parsed.
*/
public OffsetDateTime toOffsetDateTimeBin(byte[] bytes) throws PSQLException {
ParsedBinaryTimestamp parsedTimestamp = this.toProlepticParsedTimestampBin(bytes);
if (parsedTimestamp.infinity == Infinity.POSITIVE) {
return OffsetDateTime.MAX;
} else if (parsedTimestamp.infinity == Infinity.NEGATIVE) {
return OffsetDateTime.MIN;
}
// hardcode utc because the backend does not provide us the timezone
// Postgres is always UTC
Instant instant = Instant.ofEpochSecond(parsedTimestamp.millis / 1000L, parsedTimestamp.nanos);
return OffsetDateTime.ofInstant(instant, ZoneOffset.UTC);
}
public @PolyNull Time toTime(
@Nullable Calendar cal, @PolyNull String s) throws SQLException {
try (ResourceLock ignore = lock.obtain()) {
// 1) Parse backend string
if (s == null) {
return null;
}
ParsedTimestamp ts = parseBackendTimestamp(s);
Calendar useCal = ts.hasOffset ? getCalendar(ts.offset) : setupCalendar(cal);
if (!ts.hasOffset) {
// When no time zone provided (e.g. time or timestamp)
// We get the year-month-day from the string, then truncate the day to 1970-01-01
// This is used for timestamp -> time conversion
// Note: this cannot be merged with "else" branch since
// timestamps at which the time flips to/from DST depend on the date
// For instance, 2000-03-26 02:00:00 is invalid timestamp in Europe/Moscow time zone
// and the valid one is 2000-03-26 03:00:00. That is why we parse full timestamp
// then set year to 1970 later
useCal.set(Calendar.ERA, ts.era);
useCal.set(Calendar.YEAR, ts.year);
useCal.set(Calendar.MONTH, ts.month - 1);
useCal.set(Calendar.DAY_OF_MONTH, ts.day);
} else {
// When time zone is given, we just pick the time part and assume date to be 1970-01-01
// this is used for time, timez, and timestamptz parsing
useCal.set(Calendar.ERA, GregorianCalendar.AD);
useCal.set(Calendar.YEAR, 1970);
useCal.set(Calendar.MONTH, Calendar.JANUARY);
useCal.set(Calendar.DAY_OF_MONTH, 1);
}
useCal.set(Calendar.HOUR_OF_DAY, ts.hour);
useCal.set(Calendar.MINUTE, ts.minute);
useCal.set(Calendar.SECOND, ts.second);
useCal.set(Calendar.MILLISECOND, 0);
long timeMillis = useCal.getTimeInMillis() + ts.nanos / 1000000;
if (ts.hasOffset || (ts.year == 1970 && ts.era == GregorianCalendar.AD)) {
// time with time zone has proper time zone, so the value can be returned as is
return new Time(timeMillis);
}
// 2) Truncate date part so in given time zone the date would be formatted as 01/01/1970
return convertToTime(timeMillis, useCal.getTimeZone());
}
}
public @PolyNull Date toDate(@Nullable Calendar cal,
@PolyNull String s) throws SQLException {
try (ResourceLock ignore = lock.obtain()) {
// 1) Parse backend string
Timestamp timestamp = toTimestamp(cal, s);
if (timestamp == null) {
return null;
}
// Note: infinite dates are handled in convertToDate
// 2) Truncate date part so in given time zone the date would be formatted as 00:00
return convertToDate(timestamp.getTime(), cal == null ? null : cal.getTimeZone());
}
}
private Calendar setupCalendar(@Nullable Calendar cal) {
TimeZone timeZone = cal == null ? null : cal.getTimeZone();
return getSharedCalendar(timeZone);
}
/**
* Get a shared calendar, applying the supplied time zone or the default time zone if null.
*
* @param timeZone time zone to be set for the calendar
* @return The shared calendar.
*/
public Calendar getSharedCalendar(@Nullable TimeZone timeZone) {
if (timeZone == null) {
timeZone = getDefaultTz();
}
Calendar tmp = calendarWithUserTz;
tmp.setTimeZone(timeZone);
return tmp;
}
/**
* Returns true when microsecond part of the time should be increased
* when rounding to microseconds
* @param nanos nanosecond part of the time
* @return true when microsecond part of the time should be increased when rounding to microseconds
*/
private static boolean nanosExceed499(int nanos) {
return nanos % 1000 > 499;
}
public String toString(@Nullable Calendar cal, Timestamp x) {
return toString(cal, x, true);
}
public String toString(@Nullable Calendar cal, Timestamp x,
boolean withTimeZone) {
try (ResourceLock ignore = lock.obtain()) {
if (x.getTime() == PGStatement.DATE_POSITIVE_INFINITY) {
return "infinity";
} else if (x.getTime() == PGStatement.DATE_NEGATIVE_INFINITY) {
return "-infinity";
}
cal = setupCalendar(cal);
long timeMillis = x.getTime();
// Round to microseconds
int nanos = x.getNanos();
if (nanos >= MAX_NANOS_BEFORE_WRAP_ON_ROUND) {
nanos = 0;
timeMillis++;
} else if (nanosExceed499(nanos)) {
// PostgreSQL does not support nanosecond resolution yet, and appendTime will just ignore
// 0..999 part of the nanoseconds, however we subtract nanos % 1000 to make the value
// a little bit saner for debugging reasons
nanos += 1000 - nanos % 1000;
}
cal.setTimeInMillis(timeMillis);
sbuf.setLength(0);
appendDate(sbuf, cal);
sbuf.append(' ');
appendTime(sbuf, cal, nanos);
if (withTimeZone) {
appendTimeZone(sbuf, cal);
}
appendEra(sbuf, cal);
return sbuf.toString();
}
}
public String toString(@Nullable Calendar cal, Date x) {
return toString(cal, x, true);
}
public String toString(@Nullable Calendar cal, Date x,
boolean withTimeZone) {
try (ResourceLock ignore = lock.obtain()) {
if (x.getTime() == PGStatement.DATE_POSITIVE_INFINITY) {
return "infinity";
} else if (x.getTime() == PGStatement.DATE_NEGATIVE_INFINITY) {
return "-infinity";
}
cal = setupCalendar(cal);
cal.setTime(x);
sbuf.setLength(0);
appendDate(sbuf, cal);
appendEra(sbuf, cal);
if (withTimeZone) {
sbuf.append(' ');
appendTimeZone(sbuf, cal);
}
return sbuf.toString();
}
}
public String toString(@Nullable Calendar cal, Time x) {
return toString(cal, x, true);
}
public String toString(@Nullable Calendar cal, Time x,
boolean withTimeZone) {
try (ResourceLock ignore = lock.obtain()) {
cal = setupCalendar(cal);
cal.setTime(x);
sbuf.setLength(0);
appendTime(sbuf, cal, cal.get(Calendar.MILLISECOND) * 1000000);
// The 'time' parser for <= 7.3 doesn't like timezones.
if (withTimeZone) {
appendTimeZone(sbuf, cal);
}
return sbuf.toString();
}
}
private static void appendDate(StringBuilder sb, Calendar cal) {
int year = cal.get(Calendar.YEAR);
int month = cal.get(Calendar.MONTH) + 1;
int day = cal.get(Calendar.DAY_OF_MONTH);
appendDate(sb, year, month, day);
}
private static void appendDate(StringBuilder sb, int year, int month, int day) {
// always use at least four digits for the year so very
// early years, like 2, don't get misinterpreted
//
int prevLength = sb.length();
sb.append(year);
int leadingZerosForYear = 4 - (sb.length() - prevLength);
if (leadingZerosForYear > 0) {
sb.insert(prevLength, ZEROS, 0, leadingZerosForYear);
}
sb.append('-');
sb.append(NUMBERS[month]);
sb.append('-');
sb.append(NUMBERS[day]);
}
private static void appendTime(StringBuilder sb, Calendar cal, int nanos) {
int hours = cal.get(Calendar.HOUR_OF_DAY);
int minutes = cal.get(Calendar.MINUTE);
int seconds = cal.get(Calendar.SECOND);
appendTime(sb, hours, minutes, seconds, nanos);
}
/**
* Appends time part to the {@code StringBuilder} in PostgreSQL-compatible format.
* The function truncates {@param nanos} to microseconds. The value is expected to be rounded
* beforehand.
* @param sb destination
* @param hours hours
* @param minutes minutes
* @param seconds seconds
* @param nanos nanoseconds
*/
private static void appendTime(StringBuilder sb, int hours, int minutes, int seconds, int nanos) {
sb.append(NUMBERS[hours]);
sb.append(':');
sb.append(NUMBERS[minutes]);
sb.append(':');
sb.append(NUMBERS[seconds]);
// Add nanoseconds.
// This won't work for server versions < 7.2 which only want
// a two digit fractional second, but we don't need to support 7.1
// anymore and getting the version number here is difficult.
//
if (nanos < 1000) {
return;
}
sb.append('.');
int len = sb.length();
sb.append(nanos / 1000); // append microseconds
int needZeros = 6 - (sb.length() - len);
if (needZeros > 0) {
sb.insert(len, ZEROS, 0, needZeros);
}
int end = sb.length() - 1;
while (sb.charAt(end) == '0') {
sb.deleteCharAt(end);
end--;
}
}
private void appendTimeZone(StringBuilder sb, Calendar cal) {
int offset = (cal.get(Calendar.ZONE_OFFSET) + cal.get(Calendar.DST_OFFSET)) / 1000;
appendTimeZone(sb, offset);
}
private void appendTimeZone(StringBuilder sb, int offset) {
int absoff = Math.abs(offset);
int hours = absoff / 60 / 60;
int mins = (absoff - hours * 60 * 60) / 60;
int secs = absoff - hours * 60 * 60 - mins * 60;
sb.append(offset >= 0 ? "+" : "-");
sb.append(NUMBERS[hours]);
if (mins == 0 && secs == 0) {
return;
}
sb.append(':');
sb.append(NUMBERS[mins]);
if (secs != 0) {
sb.append(':');
sb.append(NUMBERS[secs]);
}
}
private static void appendEra(StringBuilder sb, Calendar cal) {
if (cal.get(Calendar.ERA) == GregorianCalendar.BC) {
sb.append(" BC");
}
}
public String toString(LocalDate localDate) {
try (ResourceLock ignore = lock.obtain()) {
if (LocalDate.MAX.equals(localDate)) {
return "infinity";
} else if (localDate.isBefore(MIN_LOCAL_DATE)) {
return "-infinity";
}
sbuf.setLength(0);
appendDate(sbuf, localDate);
appendEra(sbuf, localDate);
return sbuf.toString();
}
}
public String toString(LocalTime localTime) {
try (ResourceLock ignore = lock.obtain()) {
sbuf.setLength(0);
if (localTime.isAfter(MAX_TIME)) {
return "24:00:00";
}
int nano = localTime.getNano();
if (nanosExceed499(nano)) {
// Technically speaking this is not a proper rounding, however
// it relies on the fact that appendTime just truncates 000..999 nanosecond part
localTime = localTime.plus(ONE_MICROSECOND);
}
appendTime(sbuf, localTime);
return sbuf.toString();
}
}
public String toString(OffsetTime offsetTime) {
try (ResourceLock ignore = lock.obtain()) {
sbuf.setLength(0);
final LocalTime localTime = offsetTime.toLocalTime();
if (localTime.isAfter(MAX_TIME)) {
sbuf.append("24:00:00");
appendTimeZone(sbuf, offsetTime.getOffset());
return sbuf.toString();
}
int nano = offsetTime.getNano();
if (nanosExceed499(nano)) {
// Technically speaking this is not a proper rounding, however
// it relies on the fact that appendTime just truncates 000..999 nanosecond part
offsetTime = offsetTime.plus(ONE_MICROSECOND);
}
appendTime(sbuf, localTime);
appendTimeZone(sbuf, offsetTime.getOffset());
return sbuf.toString();
}
}
/**
* Converts {@code timetz} to string taking client time zone ({@link #timeZoneProvider})
* into account.
* @param value binary representation of {@code timetz}
* @return string representation of {@code timetz}
*/
public String toStringOffsetTimeBin(byte[] value) throws PSQLException {
OffsetTime offsetTimeBin = toOffsetTimeBin(value);
return toString(withClientOffsetSameInstant(offsetTimeBin));
}
/**
* PostgreSQL does not store the time zone in the binary representation of timetz.
* However, we want to preserve the output of {@code getString()} in both binary and text formats
* So we try a client time zone when serializing {@link OffsetTime} to string.
* @param input input offset time
* @return adjusted offset time (it represents the same instant as the input one)
*/
public OffsetTime withClientOffsetSameInstant(OffsetTime input) {
if (input == OffsetTime.MAX || input == OffsetTime.MIN) {
return input;
}
TimeZone timeZone = timeZoneProvider.get();
int offsetMillis = timeZone.getRawOffset();
return input.withOffsetSameInstant(
offsetMillis == 0
? ZoneOffset.UTC
: ZoneOffset.ofTotalSeconds(offsetMillis / 1000));
}
public String toString(OffsetDateTime offsetDateTime) {
try (ResourceLock ignore = lock.obtain()) {
if (offsetDateTime.isAfter(MAX_OFFSET_DATETIME)) {
return "infinity";
} else if (offsetDateTime.isBefore(MIN_OFFSET_DATETIME)) {
return "-infinity";
}
sbuf.setLength(0);
int nano = offsetDateTime.getNano();
if (nanosExceed499(nano)) {
// Technically speaking this is not a proper rounding, however
// it relies on the fact that appendTime just truncates 000..999 nanosecond part
offsetDateTime = offsetDateTime.plus(ONE_MICROSECOND);
}
LocalDateTime localDateTime = offsetDateTime.toLocalDateTime();
LocalDate localDate = localDateTime.toLocalDate();
appendDate(sbuf, localDate);
sbuf.append(' ');
appendTime(sbuf, localDateTime.toLocalTime());
appendTimeZone(sbuf, offsetDateTime.getOffset());
appendEra(sbuf, localDate);
return sbuf.toString();
}
}
/**
* Converts {@code timestamptz} to string taking client time zone ({@link #timeZoneProvider})
* into account.
* @param value binary representation of {@code timestamptz}
* @return string representation of {@code timestamptz}
*/
public String toStringOffsetDateTime(byte[] value) throws PSQLException {
OffsetDateTime offsetDateTime = toOffsetDateTimeBin(value);
return toString(withClientOffsetSameInstant(offsetDateTime));
}
/**
* PostgreSQL does not store the time zone in the binary representation of timestamptz.
* However, we want to preserve the output of {@code getString()} in both binary and text formats
* So we try a client time zone when serializing {@link OffsetDateTime} to string.
* @param input input offset date time
* @return adjusted offset date time (it represents the same instant as the input one)
*/
public OffsetDateTime withClientOffsetSameInstant(OffsetDateTime input) {
if (input == OffsetDateTime.MAX || input == OffsetDateTime.MIN) {
return input;
}
int offsetMillis;
TimeZone timeZone = timeZoneProvider.get();
if (isSimpleTimeZone(timeZone.getID())) {
offsetMillis = timeZone.getRawOffset();
} else {
offsetMillis = timeZone.getOffset(input.toEpochSecond() * 1000L);
}
return input.withOffsetSameInstant(
offsetMillis == 0
? ZoneOffset.UTC
: ZoneOffset.ofTotalSeconds(offsetMillis / 1000));
}
/**
* Formats {@link LocalDateTime} to be sent to the backend, thus it adds time zone.
* Do not use this method in {@link java.sql.ResultSet#getString(int)}
* @param localDateTime The local date to format as a String
* @return The formatted local date
*/
public String toString(LocalDateTime localDateTime) {
try (ResourceLock ignore = lock.obtain()) {
if (localDateTime.isAfter(MAX_LOCAL_DATETIME)) {
return "infinity";
} else if (localDateTime.isBefore(MIN_LOCAL_DATETIME)) {
return "-infinity";
}
sbuf.setLength(0);
if (nanosExceed499(localDateTime.getNano())) {
localDateTime = localDateTime.plus(ONE_MICROSECOND);
}
LocalDate localDate = localDateTime.toLocalDate();
appendDate(sbuf, localDate);
sbuf.append(' ');
appendTime(sbuf, localDateTime.toLocalTime());
appendEra(sbuf, localDate);
return sbuf.toString();
}
}
private static void appendDate(StringBuilder sb, LocalDate localDate) {
int year = localDate.get(ChronoField.YEAR_OF_ERA);
int month = localDate.getMonthValue();
int day = localDate.getDayOfMonth();
appendDate(sb, year, month, day);
}
private static void appendTime(StringBuilder sb, LocalTime localTime) {
int hours = localTime.getHour();
int minutes = localTime.getMinute();
int seconds = localTime.getSecond();
int nanos = localTime.getNano();
appendTime(sb, hours, minutes, seconds, nanos);
}
private void appendTimeZone(StringBuilder sb, ZoneOffset offset) {
int offsetSeconds = offset.getTotalSeconds();
appendTimeZone(sb, offsetSeconds);
}
private static void appendEra(StringBuilder sb, LocalDate localDate) {
if (localDate.get(ChronoField.ERA) == IsoEra.BCE.getValue()) {
sb.append(" BC");
}
}
private static int skipWhitespace(char[] s, int start) {
int slen = s.length;
for (int i = start; i < slen; i++) {
if (!Character.isSpace(s[i])) {
return i;
}
}
return slen;
}
private static int firstNonDigit(char[] s, int start) {
int slen = s.length;
for (int i = start; i < slen; i++) {
if (!Character.isDigit(s[i])) {
return i;
}
}
return slen;
}
private static int number(char[] s, int start, int end) {
if (start >= end) {
throw new NumberFormatException();
}
int n = 0;
for (int i = start; i < end; i++) {
n = 10 * n + (s[i] - '0');
}
return n;
}
private static char charAt(char[] s, int pos) {
if (pos >= 0 && pos < s.length) {
return s[pos];
}
return '\0';
}
/**
* Returns the SQL Date object matching the given bytes with {@link Oid#DATE}.
*
* @param tz The timezone used.
* @param bytes The binary encoded date value.
* @return The parsed date object.
* @throws PSQLException If binary format could not be parsed.
*/
public Date toDateBin(@Nullable TimeZone tz, byte[] bytes) throws PSQLException {
if (bytes.length != 4) {
throw new PSQLException(GT.tr("Unsupported binary encoding of {0}.", "date"),
PSQLState.BAD_DATETIME_FORMAT);
}
int days = ByteConverter.int4(bytes, 0);
if (tz == null) {
tz = getDefaultTz();
}
long secs = toJavaSecs(days * 86400L);
long millis = secs * 1000L;
if (millis <= PGStatement.DATE_NEGATIVE_SMALLER_INFINITY) {
millis = PGStatement.DATE_NEGATIVE_INFINITY;
} else if (millis >= PGStatement.DATE_POSITIVE_SMALLER_INFINITY) {
millis = PGStatement.DATE_POSITIVE_INFINITY;
} else {
// Here be dragons: backend did not provide us the timezone, so we guess the actual point in
// time
millis = guessTimestamp(millis, tz);
}
return new Date(millis);
}
private TimeZone getDefaultTz() {
// Fast path to getting the default timezone.
if (DEFAULT_TIME_ZONE_FIELD != null) {
try {
TimeZone defaultTimeZone = (TimeZone) DEFAULT_TIME_ZONE_FIELD.get(null);
if (defaultTimeZone == prevDefaultZoneFieldValue) {
return castNonNull(defaultTimeZoneCache);
}
prevDefaultZoneFieldValue = defaultTimeZone;
} catch (Exception e) {
// If this were to fail, fallback on slow method.
}
}
TimeZone tz = TimeZone.getDefault();
defaultTimeZoneCache = tz;
return tz;
}
public boolean hasFastDefaultTimeZone() {
return DEFAULT_TIME_ZONE_FIELD != null;
}
/**
* Returns the SQL Time object matching the given bytes with {@link Oid#TIME} or
* {@link Oid#TIMETZ}.
*
* @param tz The timezone used when received data is {@link Oid#TIME}, ignored if data already
* contains {@link Oid#TIMETZ}.
* @param bytes The binary encoded time value.
* @return The parsed time object.
* @throws PSQLException If binary format could not be parsed.
*/
public Time toTimeBin(@Nullable TimeZone tz, byte[] bytes) throws PSQLException {
if (bytes.length != 8 && bytes.length != 12) {
throw new PSQLException(GT.tr("Unsupported binary encoding of {0}.", "time"),
PSQLState.BAD_DATETIME_FORMAT);
}
long millis;
int timeOffset;
if (usesDouble) {
double time = ByteConverter.float8(bytes, 0);
millis = (long) (time * 1000);
} else {
long time = ByteConverter.int8(bytes, 0);
millis = time / 1000;
}
if (bytes.length == 12) {
timeOffset = ByteConverter.int4(bytes, 8);
timeOffset *= -1000;
millis -= timeOffset;
return new Time(millis);
}
if (tz == null) {
tz = getDefaultTz();
}
// Here be dragons: backend did not provide us the timezone, so we guess the actual point in
// time
millis = guessTimestamp(millis, tz);
return convertToTime(millis, tz); // Ensure date part is 1970-01-01
}
/**
* Returns the SQL Time object matching the given bytes with {@link Oid#TIME}.
*
* @param bytes The binary encoded time value.
* @return The parsed time object.
* @throws PSQLException If binary format could not be parsed.
*/
public LocalTime toLocalTimeBin(byte[] bytes) throws PSQLException {
if (bytes.length != 8) {
throw new PSQLException(GT.tr("Unsupported binary encoding of {0}.", "time"),
PSQLState.BAD_DATETIME_FORMAT);
}
long micros;
if (usesDouble) {
double seconds = ByteConverter.float8(bytes, 0);
micros = (long) (seconds * 1000000d);
} else {
micros = ByteConverter.int8(bytes, 0);
}
return LocalTime.ofNanoOfDay(Math.multiplyExact(micros, 1000L));
}
/**
* Returns the SQL Timestamp object matching the given bytes with {@link Oid#TIMESTAMP} or
* {@link Oid#TIMESTAMPTZ}.
*
* @param tz The timezone used when received data is {@link Oid#TIMESTAMP}, ignored if data
* already contains {@link Oid#TIMESTAMPTZ}.
* @param bytes The binary encoded timestamp value.
* @param timestamptz True if the binary is in GMT.
* @return The parsed timestamp object.
* @throws PSQLException If binary format could not be parsed.
*/
public Timestamp toTimestampBin(@Nullable TimeZone tz, byte[] bytes, boolean timestamptz)
throws PSQLException {
ParsedBinaryTimestamp parsedTimestamp = this.toParsedTimestampBin(tz, bytes, timestamptz);
if (parsedTimestamp.infinity == Infinity.POSITIVE) {
return new Timestamp(PGStatement.DATE_POSITIVE_INFINITY);
} else if (parsedTimestamp.infinity == Infinity.NEGATIVE) {
return new Timestamp(PGStatement.DATE_NEGATIVE_INFINITY);
}
Timestamp ts = new Timestamp(parsedTimestamp.millis);
ts.setNanos(parsedTimestamp.nanos);
return ts;
}
private ParsedBinaryTimestamp toParsedTimestampBinPlain(byte[] bytes)
throws PSQLException {
if (bytes.length != 8) {
throw new PSQLException(GT.tr("Unsupported binary encoding of {0}.", "timestamp"),
PSQLState.BAD_DATETIME_FORMAT);
}
long secs;
int nanos;
if (usesDouble) {
double time = ByteConverter.float8(bytes, 0);
if (time == Double.POSITIVE_INFINITY) {
ParsedBinaryTimestamp ts = new ParsedBinaryTimestamp();
ts.infinity = Infinity.POSITIVE;
return ts;
} else if (time == Double.NEGATIVE_INFINITY) {
ParsedBinaryTimestamp ts = new ParsedBinaryTimestamp();
ts.infinity = Infinity.NEGATIVE;
return ts;
}
secs = (long) time;
nanos = (int) ((time - secs) * 1000000);
} else {
long time = ByteConverter.int8(bytes, 0);
// compatibility with text based receiving, not strictly necessary
// and can actually be confusing because there are timestamps
// that are larger than infinite
if (time == Long.MAX_VALUE) {
ParsedBinaryTimestamp ts = new ParsedBinaryTimestamp();
ts.infinity = Infinity.POSITIVE;
return ts;
} else if (time == Long.MIN_VALUE) {
ParsedBinaryTimestamp ts = new ParsedBinaryTimestamp();
ts.infinity = Infinity.NEGATIVE;
return ts;
}
secs = time / 1000000;
nanos = (int) (time - secs * 1000000);
}
if (nanos < 0) {
secs--;
nanos += 1000000;
}
nanos *= 1000;
long millis = secs * 1000L;
ParsedBinaryTimestamp ts = new ParsedBinaryTimestamp();
ts.millis = millis;
ts.nanos = nanos;
return ts;
}
private ParsedBinaryTimestamp toParsedTimestampBin(@Nullable TimeZone tz, byte[] bytes,
boolean timestamptz)
throws PSQLException {
ParsedBinaryTimestamp ts = toParsedTimestampBinPlain(bytes);
if (ts.infinity != null) {
return ts;
}
long secs = ts.millis / 1000L;
secs = toJavaSecs(secs);
long millis = secs * 1000L;
if (!timestamptz) {
// Here be dragons: backend did not provide us the timezone, so we guess the actual point in
// time
millis = guessTimestamp(millis, tz);
}
ts.millis = millis;
return ts;
}
private ParsedBinaryTimestamp toProlepticParsedTimestampBin(byte[] bytes)
throws PSQLException {
ParsedBinaryTimestamp ts = toParsedTimestampBinPlain(bytes);
if (ts.infinity != null) {
return ts;
}
long secs = ts.millis / 1000L;
// postgres epoc to java epoc
secs += PG_EPOCH_DIFF.getSeconds();
long millis = secs * 1000L;
ts.millis = millis;
return ts;
}
/**
* Returns the local date time object matching the given bytes with {@link Oid#TIMESTAMP} or
* {@link Oid#TIMESTAMPTZ}.
* @param bytes The binary encoded local date time value.
*
* @return The parsed local date time object.
* @throws PSQLException If binary format could not be parsed.
*/
public LocalDateTime toLocalDateTimeBin(byte[] bytes) throws PSQLException {
ParsedBinaryTimestamp parsedTimestamp = this.toProlepticParsedTimestampBin(bytes);
if (parsedTimestamp.infinity == Infinity.POSITIVE) {
return LocalDateTime.MAX;
} else if (parsedTimestamp.infinity == Infinity.NEGATIVE) {
return LocalDateTime.MIN;
}
// hardcode utc because the backend does not provide us the timezone
// Postgres is always UTC
return LocalDateTime.ofEpochSecond(parsedTimestamp.millis / 1000L, parsedTimestamp.nanos, ZoneOffset.UTC);
}
/**
* Returns the local date time object matching the given bytes with {@link Oid#DATE} or
* {@link Oid#TIMESTAMP}.
* @param bytes The binary encoded local date value.
*
* @return The parsed local date object.
* @throws PSQLException If binary format could not be parsed.
*/
public LocalDate toLocalDateBin(byte[] bytes) throws PSQLException {
if (bytes.length != 4) {
throw new PSQLException(GT.tr("Unsupported binary encoding of {0}.", "date"),
PSQLState.BAD_DATETIME_FORMAT);
}
int days = ByteConverter.int4(bytes, 0);
if (days == Integer.MAX_VALUE) {
return LocalDate.MAX;
} else if (days == Integer.MIN_VALUE) {
return LocalDate.MIN;
}
// adapt from different Postgres Epoch and convert to LocalDate:
return LocalDate.ofEpochDay(PG_EPOCH_DIFF.toDays() + days);
}
/**
* Given a UTC timestamp {@code millis} finds another point in time that is rendered in given time
* zone {@code tz} exactly as "millis in UTC".
*
* For instance, given 7 Jan 16:00 UTC and tz=GMT+02:00 it returns 7 Jan 14:00 UTC == 7 Jan 16:00
* GMT+02:00 Note that is not trivial for timestamps near DST change. For such cases, we rely on
* {@link Calendar} to figure out the proper timestamp.
*
* @param millis source timestamp
* @param tz desired time zone
* @return timestamp that would be rendered in {@code tz} like {@code millis} in UTC
*/
private long guessTimestamp(long millis, @Nullable TimeZone tz) {
if (tz == null) {
// If client did not provide us with time zone, we use system default time zone
tz = getDefaultTz();
}
// The story here:
// Backend provided us with something like '2015-10-04 13:40' and it did NOT provide us with a
// time zone.
// On top of that, user asked us to treat the timestamp as if it were in GMT+02:00.
//
// The code below creates such a timestamp that is rendered as '2015-10-04 13:40 GMT+02:00'
// In other words, its UTC value should be 11:40 UTC == 13:40 GMT+02:00.
// It is not sufficient to just subtract offset as you might cross DST change as you subtract.
//
// For instance, on 2000-03-26 02:00:00 Moscow went to DST, thus local time became 03:00:00
// Suppose we deal with 2000-03-26 02:00:01
// If you subtract offset from the timestamp, the time will be "a hour behind" since
// "just a couple of hours ago the OFFSET was different"
//
// To make a long story short: we have UTC timestamp that looks like "2000-03-26 02:00:01" when
// rendered in UTC tz.
// We want to know another timestamp that will look like "2000-03-26 02:00:01" in Europe/Moscow
// time zone.
if (isSimpleTimeZone(tz.getID())) {
// For well-known non-DST time zones, just subtract offset
return millis - tz.getRawOffset();
}
// For all the other time zones, enjoy debugging Calendar API
// Here we do a straight-forward implementation that splits original timestamp into pieces and
// composes it back.
// Note: cal.setTimeZone alone is not sufficient as it would alter hour (it will try to keep the
// same time instant value)
Calendar cal = calendarWithUserTz;
cal.setTimeZone(UTC_TIMEZONE);
cal.setTimeInMillis(millis);
int era = cal.get(Calendar.ERA);
int year = cal.get(Calendar.YEAR);
int month = cal.get(Calendar.MONTH);
int day = cal.get(Calendar.DAY_OF_MONTH);
int hour = cal.get(Calendar.HOUR_OF_DAY);
int min = cal.get(Calendar.MINUTE);
int sec = cal.get(Calendar.SECOND);
int ms = cal.get(Calendar.MILLISECOND);
cal.setTimeZone(tz);
cal.set(Calendar.ERA, era);
cal.set(Calendar.YEAR, year);
cal.set(Calendar.MONTH, month);
cal.set(Calendar.DAY_OF_MONTH, day);
cal.set(Calendar.HOUR_OF_DAY, hour);
cal.set(Calendar.MINUTE, min);
cal.set(Calendar.SECOND, sec);
cal.set(Calendar.MILLISECOND, ms);
return cal.getTimeInMillis();
}
private static boolean isSimpleTimeZone(String id) {
return id.startsWith("GMT") || id.startsWith("UTC");
}
/**
* Extracts the date part from a timestamp.
*
* @param millis The timestamp from which to extract the date.
* @param tz The time zone of the date.
* @return The extracted date.
*/
public Date convertToDate(long millis, @Nullable TimeZone tz) {
// no adjustments for the infinity hack values
if (millis <= PGStatement.DATE_NEGATIVE_INFINITY
|| millis >= PGStatement.DATE_POSITIVE_INFINITY) {
return new Date(millis);
}
if (tz == null) {
tz = getDefaultTz();
}
if (isSimpleTimeZone(tz.getID())) {
// Truncate to 00:00 of the day.
// Suppose the input date is 7 Jan 15:40 GMT+02:00 (that is 13:40 UTC)
// We want it to become 7 Jan 00:00 GMT+02:00
// 1) Make sure millis becomes 15:40 in UTC, so add offset
int offset = tz.getRawOffset();
millis += offset;
// 2) Truncate hours, minutes, etc. Day is always 86400 seconds, no matter what leap seconds
// are
millis = floorDiv(millis, ONEDAY) * ONEDAY;
// 2) Now millis is 7 Jan 00:00 UTC, however we need that in GMT+02:00, so subtract some
// offset
millis -= offset;
// Now we have brand-new 7 Jan 00:00 GMT+02:00
return new Date(millis);
}
Calendar cal = calendarWithUserTz;
cal.setTimeZone(tz);
cal.setTimeInMillis(millis);
cal.set(Calendar.HOUR_OF_DAY, 0);
cal.set(Calendar.MINUTE, 0);
cal.set(Calendar.SECOND, 0);
cal.set(Calendar.MILLISECOND, 0);
return new Date(cal.getTimeInMillis());
}
/**
* Extracts the time part from a timestamp. This method ensures the date part of output timestamp
* looks like 1970-01-01 in given timezone.
*
* @param millis The timestamp from which to extract the time.
* @param tz timezone to use.
* @return The extracted time.
*/
public Time convertToTime(long millis, TimeZone tz) {
if (tz == null) {
tz = getDefaultTz();
}
if (isSimpleTimeZone(tz.getID())) {
// Leave just time part of the day.
// Suppose the input date is 2015 7 Jan 15:40 GMT+02:00 (that is 13:40 UTC)
// We want it to become 1970 1 Jan 15:40 GMT+02:00
// 1) Make sure millis becomes 15:40 in UTC, so add offset
int offset = tz.getRawOffset();
millis += offset;
// 2) Truncate year, month, day. Day is always 86400 seconds, no matter what leap seconds are
millis = floorMod(millis, ONEDAY);
// 2) Now millis is 1970 1 Jan 15:40 UTC, however we need that in GMT+02:00, so subtract some
// offset
millis -= offset;
// Now we have brand-new 1970 1 Jan 15:40 GMT+02:00
return new Time(millis);
}
Calendar cal = calendarWithUserTz;
cal.setTimeZone(tz);
cal.setTimeInMillis(millis);
cal.set(Calendar.ERA, GregorianCalendar.AD);
cal.set(Calendar.YEAR, 1970);
cal.set(Calendar.MONTH, 0);
cal.set(Calendar.DAY_OF_MONTH, 1);
return new Time(cal.getTimeInMillis());
}
/**
* Returns the given time value as String matching what the current postgresql server would send
* in text mode.
*
* @param time time value
* @param withTimeZone whether timezone should be added
* @return given time value as String
*/
public String timeToString(java.util.Date time, boolean withTimeZone) {
Calendar cal = null;
if (withTimeZone) {
cal = calendarWithUserTz;
cal.setTimeZone(timeZoneProvider.get());
}
if (time instanceof Timestamp) {
return toString(cal, (Timestamp) time, withTimeZone);
}
if (time instanceof Time) {
return toString(cal, (Time) time, withTimeZone);
}
return toString(cal, (Date) time, withTimeZone);
}
/**
* Converts the given postgresql seconds to java seconds. Reverse engineered by inserting varying
* dates to postgresql and tuning the formula until the java dates matched. See {@link #toPgSecs}
* for the reverse operation.
*
* @param secs Postgresql seconds.
* @return Java seconds.
*/
private static long toJavaSecs(long secs) {
// postgres epoc to java epoc
secs += PG_EPOCH_DIFF.getSeconds();
// Julian/Gregorian calendar cutoff point
if (secs < -12219292800L) { // October 4, 1582 -> October 15, 1582
secs += 86400 * 10;
if (secs < -14825808000L) { // 1500-02-28 -> 1500-03-01
int extraLeaps = (int) ((secs + 14825808000L) / 3155760000L);
extraLeaps--;
extraLeaps -= extraLeaps / 4;
secs += extraLeaps * 86400L;
}
}
return secs;
}
/**
* Converts the given java seconds to postgresql seconds. See {@link #toJavaSecs} for the reverse
* operation. The conversion is valid for any year 100 BC onwards.
*
* @param secs Postgresql seconds.
* @return Java seconds.
*/
private static long toPgSecs(long secs) {
// java epoc to postgres epoc
secs -= PG_EPOCH_DIFF.getSeconds();
// Julian/Gregorian calendar cutoff point
if (secs < -13165977600L) { // October 15, 1582 -> October 4, 1582
secs -= 86400 * 10;
if (secs < -15773356800L) { // 1500-03-01 -> 1500-02-28
int years = (int) ((secs + 15773356800L) / -3155823050L);
years++;
years -= years / 4;
secs += years * 86400L;
}
}
return secs;
}
/**
* Converts the SQL Date to binary representation for {@link Oid#DATE}.
*
* @param tz The timezone used.
* @param bytes The binary encoded date value.
* @param value value
* @throws PSQLException If binary format could not be parsed.
*/
public void toBinDate(@Nullable TimeZone tz, byte[] bytes, Date value) throws PSQLException {
long millis = value.getTime();
if (tz == null) {
tz = getDefaultTz();
}
// It "getOffset" is UNTESTED
// See org.postgresql.jdbc.AbstractJdbc2Statement.setDate(int, java.sql.Date,
// java.util.Calendar)
// The problem is we typically do not know for sure what is the exact required date/timestamp
// type
// Thus pgjdbc sticks to text transfer.
millis += tz.getOffset(millis);
long secs = toPgSecs(millis / 1000);
ByteConverter.int4(bytes, 0, (int) (secs / 86400));
}
/**
* Converts backend's TimeZone parameter to java format.
* Notable difference: backend's gmt-3 is GMT+03 in Java.
*
* @param timeZone time zone to use
* @return java TimeZone
*/
public static TimeZone parseBackendTimeZone(String timeZone) {
if (timeZone.startsWith("GMT")) {
TimeZone tz = GMT_ZONES.get(timeZone);
if (tz != null) {
return tz;
}
}
return TimeZone.getTimeZone(timeZone);
}
private 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;
}
private static long floorMod(long x, long y) {
return x - floorDiv(x, y) * y;
}
}
© 2015 - 2024 Weber Informatics LLC | Privacy Policy