
net.snowflake.common.util.TimeUtil Maven / Gradle / Ivy
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