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

net.snowflake.common.core.SFTimestamp Maven / Gradle / Ivy

There is a newer version: 5.1.4
Show newest version
/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package net.snowflake.common.core;

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.*;
import net.snowflake.common.util.TimeUtil;

/**
 * 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 implements Comparable { 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); public static final SFTimestamp MIN_VALID_VALUE, MAX_VALID_VALUE; static { final GregorianCalendar cal = new GregorianCalendar(SFInstant.GMT); cal.setGregorianChange(new Date(Long.MIN_VALUE)); // clears HOUR, MINUTE, SECOND, etc. cal.setTimeInMillis(0); // min supported timestamp is -1000000-12-31 00:00:00 UTC cal.set(Calendar.ERA, GregorianCalendar.BC); cal.set(1_000_001, Calendar.DECEMBER, 31); MIN_VALID_VALUE = SFTimestamp.fromMilliseconds(cal.getTimeInMillis(), SFInstant.GMT); // max supported timestamp is 1000000-01-02 00:00:00 UTC cal.set(Calendar.ERA, GregorianCalendar.AD); cal.set(1_000_000, Calendar.JANUARY, 2); MAX_VALID_VALUE = SFTimestamp.fromMilliseconds(cal.getTimeInMillis(), SFInstant.GMT); } /** * 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 */ public static 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 */ public static 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 */ public static 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 */ public static 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 */ public static 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 */ public static 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 */ public static 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. */ public static 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)); calLocal.set(Calendar.ERA, calUTC.get(Calendar.ERA)); // 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 && !(newTimeZone instanceof TimeUtil.CCompatibleTimeZone)) { 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-03-13 01:30 -0800 or 2016-03-13 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 Java timestamp. * * @return the time zone offset in millis */ public int getTimeZoneOffsetMillis() { BigInteger timeMs = getTimeInMsBigInt(); // We only support timestamps within a two-million-years range. To deal with // timestamps out of that range, we assume that there is no timezone offset // change beyond the range. timeMs = timeMs.max(MIN_VALID_VALUE.getTimeInMsBigInt()); timeMs = timeMs.min(MAX_VALID_VALUE.getTimeInMsBigInt()); 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 - 2024 Weber Informatics LLC | Privacy Policy