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

net.snowflake.common.util.TimeUtil Maven / Gradle / Ivy

There is a newer version: 5.1.4
Show newest version
package net.snowflake.common.util;

import net.snowflake.common.core.CalendarCache;
import net.snowflake.common.core.SFTime;
import net.snowflake.common.core.SFTimestamp;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.sql.Timestamp;
import java.util.Calendar;
import java.util.Date;
import java.util.TimeZone;


/**
 * Time-related utilities
 * @author mzukowski
 */
public class TimeUtil
{
  private static final BigDecimal LONG_MIN_VALUE_BIGD =
      BigDecimal.valueOf(Long.MIN_VALUE);
  private static final BigDecimal LONG_MAX_VALUE_BIGD =
      BigDecimal.valueOf(Long.MAX_VALUE);

  public enum TimestampType
  {
    TIMESTAMP_NTZ,
    TIMESTAMP_TZ,
    TIMESTAMP_LTZ
  };

  public enum TimestampUnit
  {
    IN_NANOSECONDS,
    IN_SECONDS
  }

  private static TimeZone utcTZ = TimeZone.getTimeZone("UTC");

  /**
   * Converts the number of nanoseconds into a java.sql.Timestamp, if that is
   * possible. (java.sql.Timestamp can only accommodate timestamps where the
   * number of millis since epoch can be represented by a long).
   *
   * Note: this method will adjust timestamp before 1582-10-05 to be
   * Gregorian Calendar (by default java use Julian Calendar)
   *
   * @param ns a BigDecimal value
   * @return timestamp, or null if ns is outside the range of Java timestamps
   */
  static public Timestamp timestampFromNs(BigDecimal ns)
  {
    // Get the integral-seconds part in nanoseconds
    // We round down, for negative times it's critical
    BigDecimal nsIntegral = ns.scaleByPowerOfTen(-9)
            .setScale(0, RoundingMode.FLOOR).scaleByPowerOfTen(9);
    // Get the full-seconds part in ms
    BigDecimal msIntegral = nsIntegral.scaleByPowerOfTen(-6);
    // Get the fractional-seconds-part (in nanoseconds)
    // Note - if ns was negative, this will be positive
    BigDecimal nsFractional = ns.subtract(nsIntegral);

    // Create the timestamp
    if (msIntegral.compareTo(LONG_MIN_VALUE_BIGD) < 0 ||
        msIntegral.compareTo(LONG_MAX_VALUE_BIGD) > 0)
    {
      // not representable by java.sql.Timestamp
      return null;
    }

    Timestamp timestamp = new Timestamp(msIntegral.longValueExact());
    timestamp.setNanos(nsFractional.intValueExact());

    return timestamp;
  }


  /**
   * Convert a timestamp internal value (scaled number of seconds + fractional
   * seconds) into a SFTimestamp.
   *
   * @param timestampStr timestamp object
   * @param scale timestamp scale
   * @param internalColumnType snowflake timestamp type
   * @param resultVersion For new result version, timestamp with timezone is formatted as
   *                      the seconds since epoch with fractional part in the decimal followed
   *                      by time zone index. E.g.: "123.456 1440". Here 123.456 is the * number
   *                      of seconds since epoch and 1440 is the timezone index.
   * @param sessionTZ session timezone
   * @return converted snowflake timestamp object
   * @throws IllegalArgumentException if timestampStr is an invalid timestamp
   */
  static public SFTimestamp getSFTimestamp(String timestampStr, int scale,
                                           TimestampType internalColumnType,
                                           long resultVersion,
                                           TimeZone sessionTZ)
      throws IllegalArgumentException
  {
    return getSFTimestamp(timestampStr, TimestampUnit.IN_SECONDS,
        scale, internalColumnType, resultVersion, sessionTZ);
  }

  /**
   * Convert a timestamp internal value (scaled number of seconds + fractional
   * seconds or nanoseconds) into a SFTimestamp.
   *
   * @param timestampStr timestamp object
   * @param unit IN_SECONDS or IN_NANOSECONDS, a unit of epoch time in timestampStr
   * @param scale timestamp scale for IN_SECONDS. Must be 0 for IN_NANOSECONDS.
   * @param internalColumnType snowflake timestamp type
   * @param resultVersion For new result version, timestamp with timezone is formatted as
   *                      the seconds since epoch with fractional part in the decimal followed
   *                      by time zone index. E.g.: "123.456 1440". Here 123.456 is the * number
   *                      of seconds since epoch and 1440 is the timezone index.
   * @param sessionTZ session timezone
   * @return converted snowflake timestamp object
   * @throws IllegalArgumentException if timestampStr is an invalid timestamp
   */
  static public SFTimestamp getSFTimestamp(String timestampStr,
                                           TimestampUnit unit,
                                           int scale,
                                           TimestampType internalColumnType,
                                           long resultVersion,
                                           TimeZone sessionTZ)
      throws IllegalArgumentException
  {
    assert unit == TimestampUnit.IN_NANOSECONDS && scale == 0 ||
        unit == TimestampUnit.IN_SECONDS && scale >= 0;
    try
    {
      BigDecimal fractionsSinceEpoch;

      // Derive the used timezone - if NULL, will be extracted from the number.
      TimeZone tz;
      switch (internalColumnType)
      {
        case TIMESTAMP_NTZ:
          fractionsSinceEpoch = parseSecondsSinceEpoch(timestampStr, scale);
          // Always in UTC
          tz = utcTZ;
          break;
        case TIMESTAMP_TZ:

          /*
           * For new result version, timestamp with timezone is formatted as
           * the seconds since epoch with fractional part in the decimal followed
           * by time zone index. E.g.: "123.456 1440". Here 123.456 is the
           * number of seconds since epoch and 1440 is the timezone index.
           */
          if (resultVersion > 0)
          {
            int indexForSeparator = timestampStr.indexOf(' ');
            String secondsSinceEpochStr =
                timestampStr.substring(0, indexForSeparator);
            String timezoneIndexStr = timestampStr.substring(indexForSeparator+1);

            fractionsSinceEpoch = parseSecondsSinceEpoch(secondsSinceEpochStr,
                scale);

            tz = SFTimestamp.convertTimezoneIndexToTimeZone(Integer.parseInt(
                timezoneIndexStr));
          }
          else
          {
            fractionsSinceEpoch = parseSecondsSinceEpoch(timestampStr, scale);

            // Timezone needs to be derived from the binary value for old
            // result version
            tz = null;
          }
          break;
        default:
          // Timezone from the environment
          assert internalColumnType
              == TimestampType.TIMESTAMP_LTZ;
          fractionsSinceEpoch = parseSecondsSinceEpoch(timestampStr, scale);
          tz = sessionTZ;
          break;
      }

      // Construct a timestamp in the proper timezone
      if (unit == TimestampUnit.IN_SECONDS)
      {
        return SFTimestamp.fromBinary(fractionsSinceEpoch, scale, tz);
      }
      else
      {
        return SFTimestamp.fromNanoseconds(fractionsSinceEpoch, tz);
      }
    }
    catch(NumberFormatException ex)
    {
      throw new IllegalArgumentException(ex.getMessage());
    }
  }

  /**
   * Convert a time internal value (scaled number of seconds + fractional
   * seconds) into an SFTime.
   *
   * Example: getSFTime("123.456", 5) returns an SFTime for 00:02:03.45600.
   *
   * @param obj time object
   * @param scale time scale
   * @return snowflake time object
   * @throws IllegalArgumentException if time is invalid
   */
  static public SFTime getSFTime(String obj, int scale)
      throws IllegalArgumentException
  {
    try
    {
      long fractionsSinceMidnight =
          parseSecondsSinceEpoch(obj, scale).longValue();
      return SFTime.fromFractionalSeconds(fractionsSinceMidnight, scale);
    }
    catch(NumberFormatException ex)
    {
      throw new IllegalArgumentException(ex.getMessage());
    }
  }

  /**
   * Parse seconds since epoch with both seconds and fractional seconds after
   * decimal point (e.g 123.456 with a scale of 3) to a representation with
   * fractions normalized to an integer (e.g. 123456)
   * @param secondsSinceEpochStr
   * @param scale
   * @return a BigDecimal containing the number of fractional seconds since
   * epoch.
   */
  static private BigDecimal parseSecondsSinceEpoch(
      String secondsSinceEpochStr, int scale)
  {
    // seconds since epoch has both seconds and fractional seconds after decimal
    // point. Ex: 134567890.12345678
    // Note: can actually contain timezone in the lowest part
    // Example: obj is e.g. "123.456" (scale=3)
    //          Then, secondsSinceEpoch is 123.456
    BigDecimal secondsSinceEpoch = new BigDecimal(secondsSinceEpochStr);

    // Representation with fractions normalized to an integer
    // Note: can actually contain timezone in the lowest part
    // Example: fractionsSinceEpoch is 123456
    return secondsSinceEpoch.scaleByPowerOfTen(scale);
  }

  /**
   * Returns true if the specified instant corresponds to a wallclock time
   * that is "ambiguous" in the given timezone because of daylight saving
   * time rules. For example, '2016-11-06 01:30' is ambiguous in time zone
   * 'America/Los_Angeles', because the clock goes from 2:00 back to 1:00; so
   * '2016-11-06 01:30 PT' can mean either '2016-11-06 08:30 GMT' or
   * '2016-01-30 09:30 GMT'.
   *
   * @param dateInMs
   * @param tz
   * @return true if the given timestamp is ambiguous due to DST rules.
   */
  public static boolean isDSTAmbiguous(long dateInMs, TimeZone tz)
  {
    int dstSavings = tz.getDSTSavings();

    if (dstSavings == 0)
    {
      // No DST in this time zone
      return false;
    }

    long datePrevHour = dateInMs - dstSavings;

    int tzOffset = tz.getOffset(dateInMs);
    int tzOffsetPrevHour = tz.getOffset(datePrevHour);

    return (tzOffsetPrevHour - tzOffset == dstSavings);
  }

  /**
   * Returns true if the specified instant is illegal in the current timezone
   * because of daylight saving time rules. For example, '2016-03-13 02:30'
   * is illegal in time zone 'America/Los_Angeles', because the clock goes
   * from 2:00 straight to 3:00.
   *
   * (Note that illegal timestamps can't be detected from millis since epoch,
   *  since an illegal timestamp will be mapped to a legal timestamp as it is
   *  converted to millis since epoch. We need the timestamp to be broken
   *  into its components. Hence the asymmetry in the arguments compared to
   *  isDSTAmbiguous()).
   *
   * @param era
   * @param year
   * @param month
   * @param dayOfMonth
   * @param dayOfWeek
   * @param millisecondWithinDay
   * @param tz
   * @return true if the given timestamp is illegal due to DST rules.
   */
  public static boolean isDSTIllegal(
      int era,
      int year,
      int month,
      int dayOfMonth,
      int dayOfWeek,
      int millisecondWithinDay,
      TimeZone tz
  )
  {
    int dstSavings = tz.getDSTSavings();

    if (dstSavings == 0)
    {
      // No DST in this time zone
      return false;
    }

    // dstSavings is usually 1 hour, hence the naming
    int millisecondWithinDayPrevHour = millisecondWithinDay - dstSavings;

    if (millisecondWithinDayPrevHour < 0)
    {
      // Handling this would be complicated.
      // Luckily, no DST I've ever heard of changes on a day boundary.
      return false;
    }

    int tzOffset = tz.getOffset(
        era,
        year,
        month,
        dayOfMonth,
        dayOfWeek,
        millisecondWithinDay);

    int tzOffsetPrevHour = tz.getOffset(
        era,
        year,
        month,
        dayOfMonth,
        dayOfWeek,
        millisecondWithinDayPrevHour);

    return (tzOffset - tzOffsetPrevHour == dstSavings);
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy