All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.starschema.clouddb.jdbc.DateTimeUtils Maven / Gradle / Ivy

package net.starschema.clouddb.jdbc;

import java.math.BigDecimal;
import java.sql.Date;
import java.sql.SQLException;
import java.sql.Time;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoField;
import java.util.Calendar;
import java.util.GregorianCalendar;
import java.util.Locale;
import java.util.TimeZone;

class DateTimeUtils {

  private static final ZoneId UTC_ZONE = ZoneId.of("UTC");
  private static final Calendar DEFAULT_CALENDAR = new GregorianCalendar(TimeZone.getDefault());

  /** Formatter used to parse DATETIME literals. */
  private static final DateTimeFormatter DATETIME_FORMATTER =
      new DateTimeFormatterBuilder()
          // The date part is always YYYY-MM-DD.
          .appendValue(ChronoField.YEAR, 4)
          .appendLiteral('-')
          .appendValue(ChronoField.MONTH_OF_YEAR, 2)
          .appendLiteral('-')
          .appendValue(ChronoField.DAY_OF_MONTH, 2)
          // Accept either a literal 'T' or a space to separate the date from the time.
          // Make the space optional but pad with 'T' if it's omitted, so that parsing accepts both,
          // but formatting defaults to using the space.
          .padNext(1, 'T')
          .optionalStart()
          .appendLiteral(' ')
          .optionalEnd()
          // The whole-second time part is always HH:MM:SS.
          .appendValue(ChronoField.HOUR_OF_DAY, 2)
          .appendLiteral(':')
          .appendValue(ChronoField.MINUTE_OF_HOUR, 2)
          .appendLiteral(':')
          .appendValue(ChronoField.SECOND_OF_MINUTE, 2)
          // BigQuery has optional microsecond precision.
          .optionalStart()
          .appendFraction(ChronoField.NANO_OF_SECOND, 0, 6, true)
          .optionalEnd()
          .toFormatter(Locale.ROOT);

  /** Formatter used to parse TIME literals. */
  private static final DateTimeFormatter TIME_FORMATTER =
      new DateTimeFormatterBuilder()
          // The whole-second time part is always HH:MM:SS.
          .appendValue(ChronoField.HOUR_OF_DAY, 2)
          .appendLiteral(':')
          .appendValue(ChronoField.MINUTE_OF_HOUR, 2)
          .appendLiteral(':')
          .appendValue(ChronoField.SECOND_OF_MINUTE, 2)
          // BigQuery only has microsecond precision, but this parser supports up to nanosecond
          // precision after the decimal point.
          .optionalStart()
          .appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true)
          .optionalEnd()
          .toFormatter(Locale.ROOT);

  private static final LocalDate TIME_EPOCH = LocalDate.of(1970, 1, 1);

  /** Parse a BigQuery DATETIME represented as a String. */
  static Timestamp parseDateTime(String value, Calendar cal) throws SQLException {
    if (cal == null) {
      cal = DEFAULT_CALENDAR;
    }
    try {
      // BigQuery DATETIME has a string representation that looks like e.g. "2010-10-26 02:49:35".
      LocalDateTime localDateTime = LocalDateTime.parse(value, DATETIME_FORMATTER);
      // In order to represent it as a [java.sql.Timestamp], which is defined by an instant,
      // we must subtract the offset from the calendar, so that the [toString] method of the
      // resulting [Timestamp] has the correct result. Since time zones can have different offsets
      // at different times of year (e.g. due to Daylight Saving), first calculate the instant
      // assuming no offset, and use that to calculate the offset.
      long utcInstant = localDateTime.toInstant(ZoneOffset.UTC).toEpochMilli();
      ZoneOffset offset = ZoneOffset.ofTotalSeconds(cal.getTimeZone().getOffset(utcInstant) / 1000);
      Instant instant = localDateTime.atOffset(offset).toInstant();
      Timestamp timestamp = new Timestamp(instant.toEpochMilli());
      timestamp.setNanos(instant.getNano());
      return timestamp;
    } catch (DateTimeParseException e) {
      throw new BQSQLException(e);
    }
  }

  /** Parse a BigQuery TIMESTAMP literal, represented as the number of seconds since epoch. */
  static Timestamp parseTimestamp(String value) throws SQLException {
    if (value == null) {
      return null;
    }
    try {
      // BigQuery TIMESTAMP has a string representation that looks like e.g. "1.288061375E9"
      // for 2010-10-26 02:49:35 UTC.
      // It is the (possibly fractional) number of seconds since the epoch.
      BigDecimal secondsSinceEpoch = new BigDecimal(value);
      // https://stackoverflow.com/questions/5839411/java-sql-timestamp-way-of-storing-nanoseconds
      // In order to support sub-millisecond precision, we need to first initialize the timestamp
      // with the correct number of whole seconds (expressed in milliseconds),
      // then set the nanosecond value, which overrides the initial milliseconds.
      long wholeSeconds = secondsSinceEpoch.longValue();
      Timestamp timestamp = new Timestamp(wholeSeconds * 1000L);
      int nanoSeconds = secondsSinceEpoch.remainder(BigDecimal.ONE).movePointRight(9).intValue();
      timestamp.setNanos(nanoSeconds);
      return timestamp;
    } catch (NumberFormatException e) {
      throw new BQSQLException(e);
    }
  }

  static String formatTimestamp(String rawString) throws SQLException {
    if (rawString == null) {
      return null;
    }
    Timestamp timestamp = parseTimestamp(rawString);
    return DATETIME_FORMATTER.format(OffsetDateTime.ofInstant(timestamp.toInstant(), UTC_ZONE))
        + " UTC";
  }

  static Date parseDate(String value, Calendar cal) throws SQLException {
    // Dates in BigQuery come back in the YYYY-MM-DD format
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
    try {
      java.util.Date date = sdf.parse(value);
      return new java.sql.Date(date.getTime());
    } catch (java.text.ParseException e) {
      throw new BQSQLException(e);
    }
  }

  static Time parseTime(String value, Calendar cal) throws SQLException {
    if (cal == null) {
      cal = DEFAULT_CALENDAR;
    }
    try {
      // BigQuery DATETIME has a string representation that looks like e.g. "2010-10-26 02:49:35".
      LocalTime localTime = LocalTime.parse(value, TIME_FORMATTER);
      // In order to represent it as a [java.sql.Time], which is defined by an instant,
      // we must subtract the offset from the calendar, so that the [toString] method of the
      // resulting [Time] has the correct result. Since time values do not have date components,
      // assume the standard offset should be used.
      ZoneOffset offset = ZoneOffset.ofTotalSeconds(cal.getTimeZone().getRawOffset() / 1000);
      Instant instant = localTime.atDate(TIME_EPOCH).atOffset(offset).toInstant();
      return new Time(instant.toEpochMilli());
    } catch (DateTimeParseException e) {
      throw new BQSQLException(e);
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy