
net.snowflake.common.core.SFTimestamp Maven / Gradle / Ivy
/*
* To change this template, choose Tools | Templates
* and open the template in the editor.
*/
package net.snowflake.common.core;
import net.snowflake.common.util.TimeUtil;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.math.RoundingMode;
import java.sql.Timestamp;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* Represents timestamps, including their originating time zone.
*
* Stores data in UTC, with nanosecond precision.
*
* Instances of this class are immutable.
*
* @author mzukowski
*/
public class SFTimestamp extends SFInstant
{
private static final BigInteger LONG_MIN_VALUE_BIGINT =
BigInteger.valueOf(Long.MIN_VALUE);
private static final BigInteger LONG_MAX_VALUE_BIGINT =
BigInteger.valueOf(Long.MAX_VALUE);
private static final BigDecimal NANOS_IN_SECOND =
BigDecimal.valueOf(1).scaleByPowerOfTen(9);
/**
* Thrown when a Snowflake timestamp cannot be manipulated in Java due to
* size limitations. Snowflake can use up to a full SB16 to represent a
* timestamp. Java, on the other hand, requires that the number of millis
* since epoch fit into a long. For timestamps whose millis since epoch
* don't fit into a long, certain operations, such as conversion to java
* .sql.Timestamp, are not available.
*
*/
public static class TimestampOperationNotAvailableException
extends RuntimeException
{
TimestampOperationNotAvailableException(SFTimestamp timestamp)
{
// Don't call SFTimestamp.toString() here, because SFTimestamp.toString
// () uses getTimestamp(), which may also throw this exception and thus
// create an infinite loop.
super("nanos=" + timestamp.nanosSinceEpoch);
}
}
// this could be a BigInteger but it's more convenient to represent it as a
// BigDecimal because we need to scale it by powers of 10 a lot.
private final BigDecimal nanosSinceEpoch;
private final TimeZone timeZone;
/****** WARNING: need to match values in Timestamp.hpp in XP *****/
// See https://snowflakecomputing.atlassian.net/wiki/display/EN/Timestamp+Data+Type
private static final int BITS_FOR_TIMEZONE = 14;
private static final int MASK_OF_TIMEZONE = (1 << BITS_FOR_TIMEZONE) - 1;
/**
* Constructs an SF timestamp from a given number of UTC milliseconds
* and an originating timezone.
* @param ms milliseconds
* @param tz timezone
* @return SFTimestamp instance
*/
static public SFTimestamp fromMilliseconds(long ms, TimeZone tz)
{
return fromNanoseconds(new BigDecimal(ms).scaleByPowerOfTen(6), tz);
}
/**
* Constructs an SF timestamp from a given number of UTC nanoseconds
* and an originating timezone.
* @param ns nanoseconds
* @param tz timezone
* @return SFTimestamp instance
*/
static public SFTimestamp fromNanoseconds(long ns, TimeZone tz)
{
return fromNanoseconds(new BigDecimal(ns), tz);
}
/**
* Constructs an SF timestamp from a given number of UTC nanoseconds,
* using the specified timezone
* @param ns nanoseconds in BigDecimal
* @param tz timezone
* @return SFTimestamp instance
*/
static public SFTimestamp fromNanoseconds(BigDecimal ns, TimeZone tz)
{
return new SFTimestamp(ns, tz);
}
/**
* Constructs an SF timestamp from a given number of UTC nanoseconds,
* using the GMT timezone.
* @param ns nanoseconds in BigDecimal
* @return SFTimestamp instance
*/
static public SFTimestamp fromNanoseconds(BigDecimal ns)
{
return new SFTimestamp(ns, SFInstant.GMT);
}
/**
* Convert a timezone index to timezone.
*
* @param timezoneIndex timezone index where 1440 is UTC
* @return Timezone instance
*/
static public TimeZone convertTimezoneIndexToTimeZone(int timezoneIndex)
{
assert timezoneIndex >= 0 && timezoneIndex <= 2880; // API
timezoneIndex -= 1440;
boolean negate = (timezoneIndex < 0);
timezoneIndex = Math.abs(timezoneIndex);
int hour = timezoneIndex / 60;
assert hour >= 0 && hour <= 24;
int min = timezoneIndex % 60;
assert min >= 0 && min <= 59;
String tzName = String.format("GMT%s%02d:%02d", negate ? "-" : "+",
hour, min);
return TimeZone.getTimeZone(tzName);
}
/**
* Constructs an SFTimestamp from a binary representation
* @param binary Physical representation, consists of UTC fractions
* (scaled decimals) and optional timezone index
* @param scale Scale of fractional seconds
* @param tz If specified, TimeZone to use.
* If null, we'll assume the timezone is embedded in the number
* @return SFTimestamp instance
*/
static public SFTimestamp fromBinary(BigDecimal binary,
int scale,
TimeZone tz)
{
BigDecimal nanoseconds;
if (tz != null)
{
// Just scale the decimal number, e.g. for actual value "123.456" with
// scale=3, which is represented as 123456 (fractions),
// we'll get 123456000000 nanoseconds
nanoseconds = binary.scaleByPowerOfTen(9 - scale);
}
else
{
// We will extract the time zone, and adapt the input on the way.
// The input here is the integer respresentation in XP, formatted
// as a decimal using the scale.
// For example, (assuming everything is decimal) a time moment
// 123.456 (scale = 3) in timezone 78 is represented in XP as 12345678.
// (On binary level: 123456 << 14 + 78)
// We want to convert it to 123456, and extract 78 on the way.
// Extract timezone offset
// Grab integer, e.g. 12345678
BigInteger secsWithTzOff = binary.toBigIntegerExact();
// Extract 78
BigInteger tzOff = secsWithTzOff.and(BigInteger.valueOf(MASK_OF_TIMEZONE));
// Get rid of the timezone, e.g. 123456
BigInteger secsWoTzOff = secsWithTzOff.shiftRight(BITS_FOR_TIMEZONE);
// Scale to nanoseconds, e.g. 123456000000
nanoseconds = new BigDecimal(secsWoTzOff).scaleByPowerOfTen(9 - scale);
// Finally, construct a timezone from timezone offset
// @todo This is very ugly - maybe we should consider not using TimeZone
// to represent this.
tz = convertTimezoneIndexToTimeZone(tzOff.intValue());
}
return fromNanoseconds(nanoseconds, tz);
}
/**
* Constructs an SF timestamp from a Date and a number of nanoseconds.
* @param date date instance
* @param nanos nanoseconds
* @param tz timezone
* @return SFTImestamp instance
*/
static public SFTimestamp fromDate(Date date, int nanos, TimeZone tz)
{
Timestamp t = new Timestamp(date.getTime());
t.setNanos(nanos);
SFTimestamp res = new SFTimestamp(t, tz);
return res;
}
/**
* Constructs an SF timestamp from an SF date,
* @param sfd SFDate representing a date.
* Note - it's always UTC milliseconds.
* @param tz timezone
* @return SFTimestamp representing a midnight in a specified timezone.
*/
static public SFTimestamp fromSFDate(SFDate sfd, TimeZone tz)
{
// Create a Calendar representing midnight in UTC
Calendar calUTC = CalendarCache.get(SFInstant.GMT, "GMT-source");
calUTC.setTime(sfd.getDate());
// Create a Calendar with midnight in the defined timezone
Calendar calLocal = CalendarCache.get(tz);
calLocal.set(calUTC.get(Calendar.YEAR),
calUTC.get(Calendar.MONTH),
calUTC.get(Calendar.DAY_OF_MONTH));
// Build the result timestamp
return fromMilliseconds(calLocal.getTimeInMillis(), tz);
}
private SFTimestamp(BigDecimal nanosSinceEpoch, TimeZone tz)
{
this.nanosSinceEpoch = nanosSinceEpoch;
if (tz != null)
{
this.timeZone = tz;
}
else
{
this.timeZone = SFInstant.GMT;
}
}
/**
* Constructs an SFTimestamp from a java Timestamp and java TimeZone.
* @param ts timestamp
* @param tz timezone
*/
public SFTimestamp(Timestamp ts, TimeZone tz)
{
if (ts == null)
{
ts = new Timestamp(new Date().getTime());
}
this.nanosSinceEpoch =
new BigDecimal(ts.getTime())
// Zero out the milliseconds part.
// java.sql.Timestamp is supposed to contain only integral
// seconds in its millis timestamp, and then the fractional
// seconds in the nanos field.
// But, depending on how it is constructed, the millis after
// second part could be present in both the millis and the nanos
// variables... go figure.
.scaleByPowerOfTen(-3)
.setScale(0, RoundingMode.FLOOR)
// seconds to nanos
.scaleByPowerOfTen(9)
// add the fractional seconds from the nanos part
.add(BigDecimal.valueOf(ts.getNanos()));
if (tz != null)
{
timeZone = tz;
}
else
{
timeZone = SFInstant.GMT;
}
}
/**
* Constructs an SFTimestamp from a java Timestamp.
* The result timestamp has no timezone.
* @param ts Java timestamp
*/
public SFTimestamp(Timestamp ts)
{
this(ts, SFInstant.GMT);
}
/**
* Constructs an SFTimestamp from SFDate.
* The result timestamp has no timezone
* @param date SFDate instance
*/
public SFTimestamp(SFDate date)
{
this(new Timestamp(date.getTime()), SFInstant.GMT);
}
/**
* Empty constructor, will create a timestamp at this instant
*/
public SFTimestamp()
{
this((Timestamp) null, null);
}
/**
* Copy constructor.
* Unless an explicit copy of {@code original} is needed, use of this
* constructor is unnecessary since SFTimestamps are immutable.
* @param sft source SFTimestamp instance
*/
public SFTimestamp(SFTimestamp sft)
{
// It's safe to copy BigInteger by reference; it's immutable.
this.nanosSinceEpoch = sft.nanosSinceEpoch;
this.timeZone = (TimeZone) sft.timeZone.clone();
}
public BigDecimal getNanosSinceEpoch()
{
return nanosSinceEpoch;
}
/**
* Convert this to a Java timestamp.
*
* @return Java timestamp or null
* @throws TimestampOperationNotAvailableException if this timestamp doesn't fit
* in a Java timestamp
*/
public Timestamp getTimestamp()
throws TimestampOperationNotAvailableException
{
Timestamp ts = TimeUtil.timestampFromNs(nanosSinceEpoch);
if (ts == null)
{
throw new TimestampOperationNotAvailableException(this);
}
return ts;
}
/**
* Returns Timezone
* @return Timezone
*/
public TimeZone getTimeZone()
{
return timeZone;
}
/**
* Returns a new SFTimestamp object, with specified timeZone set in it.
* @param timeZone the timezone for this timestamp
* @return the new SFTimestamp in the new timezone
*/
public SFTimestamp changeTimeZone(TimeZone timeZone)
{
return new SFTimestamp(this.nanosSinceEpoch, timeZone);
}
/**
* Moves the timestamp to another time zone without changing its displayed value.
* For example, 18:53:21 PDT will become 18:53:21 EDT.
* This is NOT the same as changeTimestamp(); that will change 18:53:21 PDT into 21:53:21 EDT.
*
* @param newTimeZone a target Timezone
* @return SFTimestamp instance
* @throws TimestampOperationNotAvailableException if this timestamp doesn't fit
* into a Java timestamp
*/
public SFTimestamp moveToTimeZone(TimeZone newTimeZone)
throws TimestampOperationNotAvailableException
{
return moveToTimeZone(newTimeZone, false /*cCompatibility*/);
}
/**
* Moves the timestamp to another time zone without changing its displayed value.
* For example, 18:53:21 PDT will become 18:53:21 EDT.
* This is NOT the same as changeTimestamp(); that will change 18:53:21 PDT into 21:53:21 EDT.
*
* @param newTimeZone a target Timezone
* @param cCompatibility
* corrects incompatibilities between Java and C libraries for timestamps
* that are ambiguous (e.g. '2016-11-06 01:30') or illegal (e.g. '2016-03-13
* 02:30') due to DST rules.
*
* @return SFTimestamp instance
* @throws TimestampOperationNotAvailableException if this timestamp doesn't fit
* into a Java timestamp
*/
public SFTimestamp moveToTimeZone(TimeZone newTimeZone,
boolean cCompatibility)
throws TimestampOperationNotAvailableException
{
int offsetMillisInOldTZ = getTimeZone().getOffset(getTime());
Calendar calendar = CalendarCache.get(getTimeZone());
calendar.setTimeInMillis(getTime());
int millisecondWithinDay = ((calendar.get(Calendar.HOUR_OF_DAY) * 60 +
calendar.get(Calendar.MINUTE))*60 +
calendar.get(Calendar.SECOND))*1000+
calendar.get(Calendar.MILLISECOND);
int era = calendar.get(Calendar.ERA);
int year = calendar.get(Calendar.YEAR);
int month = calendar.get(Calendar.MONTH);
int dayOfMonth = calendar.get(Calendar.DAY_OF_MONTH);
int dayOfWeek = calendar.get(Calendar.DAY_OF_WEEK);
int offsetMillisInNewTZ = newTimeZone.getOffset(
era,
year,
month,
dayOfMonth,
dayOfWeek,
millisecondWithinDay);
if (cCompatibility)
{
if (TimeUtil.isDSTAmbiguous(
getTime() + offsetMillisInOldTZ - offsetMillisInNewTZ,
newTimeZone))
{
// For an ambiguous timestamp, such as '2016-11-06 01:30' in
// America/Los_Angeles:
// - The C library chooses the earlier instant, i.e.
// 2016-11-06 01:30 -0700 or 2016-11-06 08:30 Z
// - Java chooses the later instant, i.e.
// 2016-11-06 01:30 -0800 or 2016-11-06 09:30 Z
// For compatibility, we need to bring back the Java timestamp by an
// hour.
offsetMillisInNewTZ += newTimeZone.getDSTSavings();
}
else if (TimeUtil.isDSTIllegal(era, year, month, dayOfMonth, dayOfWeek,
millisecondWithinDay, newTimeZone))
{
// For an illegal timestamp, such as '2016-03-13 02:30' in
// America/Los_Angeles:
// - The C library corrects this to
// 2016-03-13 03:30 -0700 or 2016-03-13 10:30 Z
// - Java corrects this to
// 2016-11-06 01:30 -0800 or 2016-11-06 09:30 Z
// (which doesn't really make sense).
// For compatibility, we need to spring forward the Java timestamp by
// an hour.
offsetMillisInNewTZ -= newTimeZone.getDSTSavings();
}
}
int offsetMillis = offsetMillisInOldTZ - offsetMillisInNewTZ;
long newMillis = getTime() + offsetMillis;
Timestamp newSqlTs = new Timestamp(newMillis);
newSqlTs.setNanos(getNanos());
return new SFTimestamp(newSqlTs, newTimeZone);
}
/**
* Adjusts fractional second accuracy if necessary.
*
* @param scale scale
* @return SFTimestamp instance
*/
public SFTimestamp adjustScale(int scale)
{
if (scale < 0 || scale > 9)
{
throw new IllegalArgumentException("Invalid timestamp scale " + scale);
}
int zeroesAtEnd = 9 - scale;
BigDecimal powerOfTen = BigDecimal.valueOf(
SFInstant.POWERS_OF_TEN[zeroesAtEnd]);
BigDecimal extraDigits = nanosSinceEpoch
.remainder(powerOfTen);
if (extraDigits.equals(BigDecimal.ZERO))
{
return this;
}
// Wrap negative remainders around, otherwise timestamps before epoch
// will round up instead of down.
if (extraDigits.compareTo(BigDecimal.ZERO) < 0)
{
extraDigits = powerOfTen.add(extraDigits);
}
BigDecimal newNanosSinceEpoch = nanosSinceEpoch
.subtract(extraDigits);
return new SFTimestamp(newNanosSinceEpoch, getTimeZone());
}
/**
* Returns UTC string
* @return UTC string
*/
public String toUTCString()
{
Timestamp timestamp = TimeUtil.timestampFromNs(nanosSinceEpoch);
if (timestamp == null)
{
// This can't be represented as a Java timestamp.
// Just print the seconds since epoch.
return "(seconds_since_epoch=" +
nanosSinceEpoch.scaleByPowerOfTen(-9).toPlainString() +
")";
}
String nanoStr = String.format(".%1$09d", timestamp.getNanos());
Calendar calendar = CalendarCache.get("UTC");
DateFormat tsf = new SimpleDateFormat("yyyy-MM-dd' 'HH:mm:ss"+nanoStr+"XXX");
tsf.setCalendar(calendar);
String res = tsf.format(timestamp.getTime());
return res;
}
/**
* Compares with other SFTimestamp
* @param other target SFTimestamp
* @return 1 if larger, 0 if equals otherwise -1
*/
public int compareTo(SFTimestamp other)
{
return nanosSinceEpoch.compareTo(other.nanosSinceEpoch);
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(Object o)
{
if (o == null || !(o instanceof SFTimestamp))
{
return false;
}
return equals((SFTimestamp) o);
}
/**
* Checks if equals with other SFTimestamp
* @param other target SFTimestamp
* @return true if equal otherwise false
*/
public boolean equals(SFTimestamp other)
{
return nanosSinceEpoch.equals(other.nanosSinceEpoch);
}
/**
* Returns epoch time in milliseconds as a long.
* CAUTION: If this timestamp's millis since epoch can't fit into a long,
* an exception will be thrown!
*
* @return milliseconds since epoch as a long
* @throws TimestampOperationNotAvailableException if this timestamp doesn't fit
* into a Java timestamp
*/
public long getTime()
throws TimestampOperationNotAvailableException
{
// IntelliJ doesn't like BigInteger.longValueExact() for some reason
BigInteger timeBigInt = getTimeInMsBigInt();
if (timeBigInt.compareTo(LONG_MIN_VALUE_BIGINT) < 0 ||
timeBigInt.compareTo(LONG_MAX_VALUE_BIGINT) > 0)
{
throw new TimestampOperationNotAvailableException(this);
}
return timeBigInt.longValue();
}
/**
* Returns the epoch time in milliseconds.
*
* @return milliseconds since epoch
*/
public BigInteger getTimeInMsBigInt()
{
return nanosSinceEpoch.scaleByPowerOfTen(-6)
// always round down; this is important for timestamps before the epoch.
// by default, Java will round towards 0.
.setScale(0, RoundingMode.FLOOR)
.toBigInteger();
}
/**
* Returns the integral seconds component of this timestamp.
*
* @return
*/
public BigInteger getSeconds()
{
return nanosSinceEpoch.scaleByPowerOfTen(-9)
// always round down; this is important for timestamps before the epoch.
// by default, Java will round towards 0.
.setScale(0, RoundingMode.FLOOR)
.toBigInteger();
}
/**
* Returns the fractional seconds component of this timestamp, i.e. the
* number of nanos from the nearest integral second (rounded down).
*
* @return nanoseconds
*/
public int getNanos()
{
BigDecimal nsFractional = nanosSinceEpoch.remainder(NANOS_IN_SECOND);
// Remainder can return negative numbers; wrap it around.
if (nsFractional.compareTo(BigDecimal.ZERO) < 0)
{
nsFractional = nsFractional.add(NANOS_IN_SECOND);
}
return nsFractional.intValue();
}
/**
* {@inheritDoc}
*/
public int hashCode()
{
return this.toBinary(9, false).hashCode();
}
/**
* Calculates the offset of this timestamp's time zone at this timestamp's
* time instant. This is equivalent to
* getTimeZone().getOffset(getTime())
* except it won't throw TimestampOperationNotAvailableException for
* timestamps that don't fit in a Java timestamp.
*
* @return the time zone offset in millis
*/
public int getTimeZoneOffsetMillis()
{
// Java will only give us an offset for timestamps whose millis since
// epoch fit into a long.
// Otherwise, get the raw offset.
// That should be OK. User-defined timestamps will usually be within
// limits, and timestamps from FDN file data will have fixed time zones
// (i.e. GMT+10 or GMT+9 instead of America/Los_Angeles).
BigInteger timeMs = getTimeInMsBigInt();
if (timeMs.compareTo(LONG_MIN_VALUE_BIGINT) < 0 ||
timeMs.compareTo(LONG_MAX_VALUE_BIGINT) > 0)
{
return timeZone.getRawOffset();
}
else
{
return timeZone.getOffset(timeMs.longValue());
}
}
/**
* Constructs a binary representation of this timestamp.
* This is the opposite of fromBinary().
*
* @param scale scale of fractional seconds
* @param includeTimeZone encode the time zone in the lower-order bits
* @return the binary representation of this timestamp
*/
public BigInteger toBinary(int scale, boolean includeTimeZone)
{
if (scale < 0 || scale > 9)
{
throw new IllegalArgumentException("Scale must be between 0 and 9");
}
BigDecimal timeInNs = this.nanosSinceEpoch;
// If the scale is 9 (maximum), then we return the number of nanos.
// If it is 8, we return the number of 10's of nanos. etc.
// slide the decimal point
BigDecimal scaledTime = timeInNs.scaleByPowerOfTen(scale-9);
// round to an integer
// We have to use RoundingMode.DOWN instead of RoundingMode.FLOOR because we
// want to truncate the extra digits; for negative timestamps, FLOOR will do
// the wrong thing.
scaledTime = scaledTime.setScale(0, RoundingMode.DOWN);
BigInteger fcpInt = scaledTime.unscaledValue();
if (includeTimeZone)
{
// now add the time zone
int offsetMillis = getTimeZoneOffsetMillis();
// our offset is in minutes
int offsetMin = offsetMillis / 60000;
// this code mimics fromBinary; see above
assert offsetMin >= -1440 && offsetMin <= 1440;
offsetMin += 1440;
fcpInt = fcpInt.shiftLeft(14);
fcpInt = fcpInt.add(BigInteger.valueOf(offsetMin & MASK_OF_TIMEZONE));
}
return fcpInt;
}
/**
* Extract a particular component of a date.
* @param field field id as specified in the Calendar class.
* TODO: we don't support e.g. nanosecond nor timezones for now
* @return The value of the extracted field.
* @throws TimestampOperationNotAvailableException if this timestamp doesn't fit
* into a Java timestamp
*/
@Override
public int extract(int field, Integer optWeekStart, Integer optWoyPolicy)
throws TimestampOperationNotAvailableException
{
TimeZone tz = timeZone == null ? SFInstant.GMT : timeZone;
return extract(field, tz, getTime(), optWeekStart, optWoyPolicy);
}
/**
* {@inheritDoc}
*/
@Override
public String toString()
{
String tsStr;
try
{
tsStr = "timestamp='" + getTimestamp().toString() + "'";
}
catch (TimestampOperationNotAvailableException e)
{
tsStr = "--nanos=" + nanosSinceEpoch;
}
return "SFTimestamp(" + tsStr + " timeZone='" + timeZone +"')";
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy