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

io.deephaven.time.DateTimeUtils Maven / Gradle / Ivy

There is a newer version: 0.37.1
Show newest version
//
// Copyright (c) 2016-2024 Deephaven Data Labs and Patent Pending
//
package io.deephaven.time;

import io.deephaven.base.clock.Clock;
import io.deephaven.base.clock.TimeConstants;
import io.deephaven.function.Numeric;
import io.deephaven.hash.KeyedObjectHashMap;
import io.deephaven.hash.KeyedObjectKey;
import io.deephaven.util.QueryConstants;
import io.deephaven.util.annotations.ScriptApi;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.SignStyle;
import java.time.temporal.ChronoField;
import java.time.zone.ZoneRulesException;
import java.util.Date;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static io.deephaven.util.QueryConstants.*;
import static java.time.format.DateTimeFormatter.*;

/**
 * Functions for working with time.
 */
@SuppressWarnings({"RegExpRedundantEscape"})
public class DateTimeUtils {

    // region Format Patterns

    /**
     * Very permissive formatter / parser for local dates.
     */
    private static final DateTimeFormatter FORMATTER_ISO_LOCAL_DATE = new DateTimeFormatterBuilder()
            .appendValue(ChronoField.YEAR, 4, 10, SignStyle.EXCEEDS_PAD)
            .appendLiteral('-')
            .appendValue(ChronoField.MONTH_OF_YEAR, 1, 2, SignStyle.NORMAL)
            .appendLiteral('-')
            .appendValue(ChronoField.DAY_OF_MONTH, 1, 2, SignStyle.NORMAL)
            .toFormatter();

    /**
     * Very permissive formatter / parser for local times.
     */
    private static final DateTimeFormatter FORMATTER_ISO_LOCAL_TIME = new DateTimeFormatterBuilder()
            .appendValue(ChronoField.HOUR_OF_DAY, 1, 2, SignStyle.NORMAL)
            .appendLiteral(':')
            .appendValue(ChronoField.MINUTE_OF_HOUR, 1, 2, SignStyle.NORMAL)
            .optionalStart()
            .appendLiteral(':')
            .appendValue(ChronoField.SECOND_OF_MINUTE, 1, 2, SignStyle.NORMAL)
            .optionalStart()
            .appendFraction(ChronoField.NANO_OF_SECOND, 0, 9, true)
            .toFormatter();

    /**
     * Very permissive formatter / parser for local date times.
     */
    private static final DateTimeFormatter FORMATTER_ISO_LOCAL_DATE_TIME = new DateTimeFormatterBuilder()
            .parseCaseInsensitive()
            .append(FORMATTER_ISO_LOCAL_DATE)
            .appendLiteral('T')
            .append(FORMATTER_ISO_LOCAL_TIME)
            .toFormatter();

    /**
     * Matches long values.
     */
    private static final Pattern LONG_PATTERN = Pattern.compile("^-?\\d{1,19}$");

    /**
     * Matches dates with time zones.
     */
    private static final Pattern DATE_TZ_PATTERN = Pattern.compile(
            "(?[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9])(?[tT]?) (?[a-zA-Z_/]+)");

    /**
     * Matches dates without time zones.
     */
    private static final Pattern LOCAL_DATE_PATTERN = Pattern.compile(
            "(?[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9])(?[tT]?)");


    /**
     * Matches time durations.
     */
    private static final Pattern TIME_DURATION_PATTERN = Pattern.compile(
            "(?[-]?)[pP][tT](?[-]?)(?[0-9]+):(?[0-9]+)(?:[0-9]+)?(?\\.[0-9][0-9]?[0-9]?[0-9]?[0-9]?[0-9]?[0-9]?[0-9]?[0-9]?)?");

    /**
     * Matches date times.
     */
    private static final Pattern CAPTURING_DATETIME_PATTERN = Pattern.compile(
            "(([0-9][0-9][0-9][0-9])-([0-9][0-9])-([0-9][0-9])T?)?(([0-9][0-9]?)(?::([0-9][0-9])(?::([0-9][0-9]))?(?:\\.([0-9][0-9]?[0-9]?[0-9]?[0-9]?[0-9]?[0-9]?[0-9]?[0-9]?))?)?)?( [a-zA-Z]+)?");

    // endregion

    // region Constants

    /**
     * A zero length array of {@link Instant instants}.
     */
    public static final Instant[] ZERO_LENGTH_INSTANT_ARRAY = new Instant[0];

    /**
     * One microsecond in nanoseconds.
     */
    public static final long MICRO = 1_000;

    /**
     * One millisecond in nanoseconds.
     */
    public static final long MILLI = 1_000_000;

    /**
     * One second in nanoseconds.
     */
    public static final long SECOND = 1_000_000_000;

    /**
     * One minute in nanoseconds.
     */
    public static final long MINUTE = 60 * SECOND;

    /**
     * One hour in nanoseconds.
     */
    public static final long HOUR = 60 * MINUTE;

    /**
     * One day in nanoseconds. This is one hour of wall time and does not take into account calendar adjustments.
     */
    public static final long DAY = 24 * HOUR;

    /**
     * One week in nanoseconds. This is 7 days of wall time and does not take into account calendar adjustments.
     */
    public static final long WEEK = 7 * DAY;

    /**
     * One 365 day year in nanoseconds. This is 365 days of wall time and does not take into account calendar
     * adjustments.
     */
    public static final long YEAR_365 = 365 * DAY;

    /**
     * One average year in nanoseconds. This is 365.2425 days of wall time and does not take into account calendar
     * adjustments.
     */
    public static final long YEAR_AVG = 31556952000000000L;

    /**
     * Maximum time in microseconds that can be converted to an instant without overflow.
     */
    private static final long MAX_CONVERTIBLE_MICROS = Long.MAX_VALUE / 1_000L;

    /**
     * Maximum time in milliseconds that can be converted to an instant without overflow.
     */
    private static final long MAX_CONVERTIBLE_MILLIS = Long.MAX_VALUE / 1_000_000L;

    /**
     * Maximum time in seconds that can be converted to an instant without overflow.
     */
    private static final long MAX_CONVERTIBLE_SECONDS = Long.MAX_VALUE / 1_000_000_000L;

    /**
     * Number of seconds per nanosecond.
     */
    public static final double SECONDS_PER_NANO = 1. / (double) SECOND;

    /**
     * Number of minutes per nanosecond.
     */
    public static final double MINUTES_PER_NANO = 1. / (double) MINUTE;

    /**
     * Number of hours per nanosecond.
     */
    public static final double HOURS_PER_NANO = 1. / (double) HOUR;

    /**
     * Number of days per nanosecond.
     */
    public static final double DAYS_PER_NANO = 1. / (double) DAY;

    /**
     * Number of 365 day years per nanosecond.
     */
    public static final double YEARS_PER_NANO_365 = 1. / (double) YEAR_365;

    /**
     * Number of average (365.2425 day) years per nanosecond.
     */
    public static final double YEARS_PER_NANO_AVG = 1. / (double) YEAR_AVG;

    // endregion

    // region Overflow / Underflow

    /**
     * Exception type thrown when operations on date time values would exceed the range available by max or min long
     * nanoseconds.
     */
    public static class DateTimeOverflowException extends RuntimeException {
        /**
         * Creates a new overflow exception.
         */
        @SuppressWarnings("unused")
        private DateTimeOverflowException() {
            super("Operation failed due to overflow");
        }

        /**
         * Creates a new overflow exception.
         *
         * @param cause the cause of the overflow
         */
        private DateTimeOverflowException(@NotNull final Throwable cause) {
            super("Operation failed due to overflow", cause);
        }

        /**
         * Creates a new overflow exception.
         *
         * @param message the error string
         */
        private DateTimeOverflowException(@NotNull final String message) {
            super(message);
        }

        /**
         * Creates a new overflow exception.
         *
         * @param message the error message
         * @param cause the cause of the overflow
         */
        @SuppressWarnings("unused")
        private DateTimeOverflowException(@NotNull final String message, @NotNull final Throwable cause) {
            super(message, cause);
        }
    }

    // + can only result in overflow if both positive or both negative
    @SuppressWarnings("SameParameterValue")
    private static long checkOverflowPlus(final long l1, final long l2, final boolean minusOperation) {
        if (l1 > 0 && l2 > 0 && Long.MAX_VALUE - l1 < l2) {
            final String message = minusOperation
                    ? "Subtracting " + -l2 + " nanos from " + l1 + " would overflow"
                    : "Adding " + l2 + " nanos to " + l1 + " would overflow";
            throw new DateTimeOverflowException(message);
        }

        if (l1 < 0 && l2 < 0) {
            return checkUnderflowMinus(l1, -l2, false);
        }

        return l1 + l2;
    }

    // - can only result in overflow if one is positive and one is negative
    private static long checkUnderflowMinus(final long l1, final long l2, final boolean minusOperation) {
        if (l1 < 0 && l2 > 0 && Long.MIN_VALUE + l2 > -l1) {
            final String message = minusOperation
                    ? "Subtracting " + l2 + " nanos from " + l1 + " would underflow"
                    : "Adding " + -l2 + " nanos to " + l1 + " would underflow";
            throw new DateTimeOverflowException(message);
        }

        if (l1 > 0 && l2 < 0) {
            return checkOverflowPlus(l1, -l2, true);
        }

        return l1 - l2;
    }

    private static long safeComputeNanos(final long epochSecond, final long nanoOfSecond) {
        if (epochSecond >= MAX_CONVERTIBLE_SECONDS) {
            throw new DateTimeOverflowException("Numeric overflow detected during conversion of " + epochSecond
                    + " to nanoseconds");
        }

        return epochSecond * 1_000_000_000L + nanoOfSecond;
    }

    // endregion

    // region Clock

    // TODO(deephaven-core#3044): Improve scaffolding around full system replay
    /**
     * Clock used to compute the current time. This allows a custom clock to be used instead of the current system
     * clock. This is mainly used for replay simulations.
     */
    private static Clock clock;

    /**
     * Set the clock used to compute the current time. This allows a custom clock to be used instead of the current
     * system clock. This is mainly used for replay simulations.
     *
     * @param clock the clock used to compute the current time; if {@code null}, use the system clock
     */
    @ScriptApi
    public static void setClock(@Nullable final Clock clock) {
        DateTimeUtils.clock = clock;
    }

    /**
     * Returns the clock used to compute the current time. This may be the current system clock, or it may be an
     * alternative clock used for replay simulations.
     *
     * @return the current clock
     * @see #setClock(Clock)
     */
    @ScriptApi
    @NotNull
    public static Clock currentClock() {
        return Objects.requireNonNullElse(clock, Clock.system());
    }

    /**
     * Provides the current {@link Instant instant} with nanosecond resolution according to the {@link #currentClock()
     * current clock}. Under most circumstances, this method will return the current system time, but during replay
     * simulations, this method can return the replay time.
     *
     * @return the current instant with nanosecond resolution according to the current clock
     * @see #currentClock()
     * @see #setClock(Clock)
     * @see #nowSystem()
     */
    @ScriptApi
    @NotNull
    public static Instant now() {
        return currentClock().instantNanos();
    }

    /**
     * Provides the current {@link Instant instant} with millisecond resolution according to the {@link #currentClock()
     * current clock}. Under most circumstances, this method will return the current system time, but during replay
     * simulations, this method can return the replay time.
     *
     * @return the current instant with millisecond resolution according to the current clock
     * @see #currentClock()
     * @see #setClock(Clock)
     * @see #nowSystemMillisResolution()
     */
    @ScriptApi
    @NotNull
    public static Instant nowMillisResolution() {
        return currentClock().instantMillis();
    }

    /**
     * Provides the current {@link Instant instant} with nanosecond resolution according to the {@link Clock#system()
     * system clock}. Note that the system time may not be desirable during replay simulations.
     *
     * @return the current instant with nanosecond resolution according to the system clock
     * @see #now()
     */
    @ScriptApi
    @NotNull
    public static Instant nowSystem() {
        return Clock.system().instantNanos();
    }

    /**
     * Provides the current {@link Instant instant} with millisecond resolution according to the {@link Clock#system()
     * system clock}. Note that the system time may not be desirable during replay simulations.
     *
     * @return the current instant with millisecond resolution according to the system clock
     * @see #nowMillisResolution()
     */
    @ScriptApi
    @NotNull
    public static Instant nowSystemMillisResolution() {
        return Clock.system().instantMillis();
    }

    /**
     * A cached date in a specific timezone. The cache is invalidated when the current clock indicates the next day has
     * arrived.
     */
    private abstract static class CachedDate {

        final ZoneId timeZone;
        volatile LocalDate date;
        volatile String str;
        volatile long valueExpirationTimeMillis;

        private CachedDate(@NotNull final ZoneId timeZone) {
            this.timeZone = timeZone;
        }

        @SuppressWarnings("unused")
        private ZoneId getTimeZone() {
            return timeZone;
        }

        public LocalDate getLocalDate() {
            return getLocalDate(currentClock().currentTimeMillis());
        }

        public LocalDate getLocalDate(final long currentTimeMillis) {
            if (currentTimeMillis >= valueExpirationTimeMillis) {
                update(currentTimeMillis);
            }
            return date;
        }

        public String getStr() {
            return getStr(currentClock().currentTimeMillis());
        }

        public synchronized String getStr(final long currentTimeMillis) {
            if (currentTimeMillis >= valueExpirationTimeMillis) {
                update(currentTimeMillis);
            }
            return str;
        }

        // Update methods should be synchronized!
        abstract void update(long currentTimeMillis);
    }

    private static class CachedCurrentDate extends CachedDate {

        private CachedCurrentDate(@NotNull final ZoneId timeZone) {
            super(timeZone);
        }

        @Override
        synchronized void update(final long currentTimeMillis) {
            date = toLocalDate(epochMillisToInstant(currentTimeMillis), timeZone);
            str = formatDate(date);
            valueExpirationTimeMillis = epochMillis(date.plusDays(1).atStartOfDay(timeZone));
        }
    }

    private static class CachedDateKey
            extends KeyedObjectKey.Basic {

        @Override
        public ZoneId getKey(final CACHED_DATE_TYPE cachedDate) {
            return cachedDate.timeZone;
        }
    }

    private static final KeyedObjectHashMap cachedCurrentDates =
            new KeyedObjectHashMap<>(new CachedDateKey<>());

    private static CachedCurrentDate getCachedCurrentDate(@NotNull final ZoneId timeZone) {
        return cachedCurrentDates.putIfAbsent(timeZone, CachedCurrentDate::new);
    }

    /**
     * Provides the current date string according to the {@link #currentClock() current clock}. Under most
     * circumstances, this method will return the date according to current system time, but during replay simulations,
     * this method can return the date according to replay time.
     *
     * @param timeZone the time zone
     * @return the current date according to the current clock and time zone formatted as "yyyy-MM-dd". {@code null} if
     *         the input is {@code null}.
     * @see #currentClock()
     * @see #setClock(Clock)
     */
    @ScriptApi
    @Nullable
    public static String today(@Nullable final ZoneId timeZone) {
        if (timeZone == null) {
            return null;
        }

        return getCachedCurrentDate(timeZone).getStr();
    }

    /**
     * Provides the current date string according to the {@link #currentClock() current clock} and the
     * {@link ZoneId#systemDefault() default time zone}. Under most circumstances, this method will return the date
     * according to current system time, but during replay simulations, this method can return the date according to
     * replay time.
     *
     * @return the current date according to the current clock and default time zone formatted as "yyyy-MM-dd"
     * @see #currentClock()
     * @see #setClock(Clock)
     * @see ZoneId#systemDefault()
     */
    @ScriptApi
    @NotNull
    public static String today() {
        // noinspection ConstantConditions
        return today(DateTimeUtils.timeZone());
    }

    /**
     * Provides the current date according to the {@link #currentClock() current clock}. Under most circumstances, this
     * method will return the date according to current system time, but during replay simulations, this method can
     * return the date according to replay time.
     *
     * @param timeZone the time zone
     * @return the current date according to the current clock and time zone formatted as "yyyy-MM-dd". {@code null} if
     *         the input is {@code null}.
     * @see #currentClock()
     * @see #setClock(Clock)
     */
    @ScriptApi
    @Nullable
    public static LocalDate todayLocalDate(@Nullable final ZoneId timeZone) {
        if (timeZone == null) {
            return null;
        }

        return getCachedCurrentDate(timeZone).getLocalDate();
    }

    /**
     * Provides the current date according to the {@link #currentClock() current clock} and the
     * {@link ZoneId#systemDefault() default time zone}. Under most circumstances, this method will return the date
     * according to current system time, but during replay simulations, this method can return the date according to
     * replay time.
     *
     * @return the current date according to the current clock and default time zone formatted as "yyyy-MM-dd"
     * @see #currentClock()
     * @see #setClock(Clock)
     * @see ZoneId#systemDefault()
     */
    @ScriptApi
    @NotNull
    public static LocalDate todayLocalDate() {
        // noinspection ConstantConditions
        return todayLocalDate(DateTimeUtils.timeZone());
    }

    // endregion

    // region Time Zone

    /**
     * Gets the time zone for a time zone name.
     *
     * @param timeZone the time zone name
     * @return the corresponding time zone. {@code null} if the input is {@code null}.
     * @throws DateTimeException if {@code timeZone} has an invalid format
     * @throws ZoneRulesException if {@code timeZone} cannot be found
     */
    @ScriptApi
    @Nullable
    public static ZoneId timeZone(@Nullable String timeZone) {
        if (timeZone == null) {
            return null;
        }

        return TimeZoneAliases.zoneId(timeZone);
    }

    /**
     * Gets the {@link ZoneId#systemDefault() system default time zone}.
     *
     * @return the system default time zone
     * @see ZoneId#systemDefault()
     */
    @ScriptApi
    public static ZoneId timeZone() {
        return ZoneId.systemDefault();
    }

    /**
     * Adds a new time zone alias.
     *
     * @param alias the alias name
     * @param timeZone the time zone id name
     * @throws IllegalArgumentException if the alias already exists or the time zone is invalid
     */
    @ScriptApi
    public static void timeZoneAliasAdd(@NotNull final String alias, @NotNull final String timeZone) {
        TimeZoneAliases.addAlias(alias, timeZone);
    }

    /**
     * Removes a time zone alias.
     *
     * @param alias the alias name
     * @return whether {@code alias} was present
     */
    @ScriptApi
    public static boolean timeZoneAliasRm(@NotNull final String alias) {
        return TimeZoneAliases.rmAlias(alias);
    }

    // endregion

    // region Conversions: Time Units

    /**
     * Converts microseconds to nanoseconds.
     *
     * @param micros the microseconds to convert
     * @return {@link QueryConstants#NULL_LONG} if the input is {@link QueryConstants#NULL_LONG}; otherwise the input
     *         microseconds converted to nanoseconds
     */
    @ScriptApi
    public static long microsToNanos(final long micros) {
        if (micros == NULL_LONG) {
            return NULL_LONG;
        }
        if (Math.abs(micros) > MAX_CONVERTIBLE_MICROS) {
            throw new DateTimeOverflowException("Converting " + micros + " micros to nanos would overflow");
        }
        return micros * 1000;
    }

    /**
     * Converts milliseconds to nanoseconds.
     *
     * @param millis the milliseconds to convert
     * @return {@link QueryConstants#NULL_LONG} if the input is {@link QueryConstants#NULL_LONG}; otherwise the input
     *         milliseconds converted to nanoseconds
     */
    @ScriptApi
    public static long millisToNanos(final long millis) {
        if (millis == NULL_LONG) {
            return NULL_LONG;
        }
        if (Math.abs(millis) > MAX_CONVERTIBLE_MILLIS) {
            throw new DateTimeOverflowException("Converting " + millis + " millis to nanos would overflow");
        }
        return millis * 1000000;
    }

    /**
     * Converts seconds to nanoseconds.
     *
     * @param seconds the seconds to convert
     * @return {@link QueryConstants#NULL_LONG} if the input is {@link QueryConstants#NULL_LONG}; otherwise the input
     *         seconds converted to nanoseconds
     */
    @ScriptApi
    public static long secondsToNanos(final long seconds) {
        if (seconds == NULL_LONG) {
            return NULL_LONG;
        }
        if (Math.abs(seconds) > MAX_CONVERTIBLE_SECONDS) {
            throw new DateTimeOverflowException("Converting " + seconds + " seconds to nanos would overflow");
        }

        return seconds * 1000000000L;
    }

    /**
     * Converts nanoseconds to microseconds.
     *
     * @param nanos the nanoseconds to convert
     * @return {@link QueryConstants#NULL_LONG} if the input is {@link QueryConstants#NULL_LONG}; otherwise the input
     *         nanoseconds converted to microseconds, rounded down
     */
    @ScriptApi
    public static long nanosToMicros(final long nanos) {
        if (nanos == NULL_LONG) {
            return NULL_LONG;
        }
        return nanos / MICRO;
    }

    /**
     * Converts milliseconds to microseconds.
     *
     * @param millis the milliseconds to convert
     * @return {@link QueryConstants#NULL_LONG} if the input is {@link QueryConstants#NULL_LONG}; otherwise the input
     *         milliseconds converted to microseconds, rounded down
     */
    @ScriptApi
    public static long millisToMicros(final long millis) {
        if (millis == NULL_LONG) {
            return NULL_LONG;
        }
        return millis * 1000L;
    }

    /**
     * Converts seconds to microseconds.
     *
     * @param seconds the seconds to convert
     * @return {@link QueryConstants#NULL_LONG} if the input is {@link QueryConstants#NULL_LONG}; otherwise the input
     *         seconds converted to microseconds, rounded down
     */
    @ScriptApi
    public static long secondsToMicros(final long seconds) {
        if (seconds == NULL_LONG) {
            return NULL_LONG;
        }
        return seconds * 1_000_000;
    }

    /**
     * Converts nanoseconds to milliseconds.
     *
     * @param nanos the nanoseconds to convert
     * @return {@link QueryConstants#NULL_LONG} if the input is {@link QueryConstants#NULL_LONG}; otherwise the input
     *         nanoseconds converted to milliseconds, rounded down
     */
    @ScriptApi
    public static long nanosToMillis(final long nanos) {
        if (nanos == NULL_LONG) {
            return NULL_LONG;
        }

        return nanos / MILLI;
    }

    /**
     * Converts microseconds to milliseconds.
     *
     * @param micros the microseconds to convert
     * @return {@link QueryConstants#NULL_LONG} if the input is {@link QueryConstants#NULL_LONG}; otherwise the input
     *         microseconds converted to milliseconds, rounded down
     */
    @ScriptApi
    public static long microsToMillis(final long micros) {
        if (micros == NULL_LONG) {
            return NULL_LONG;
        }

        return micros / 1000L;
    }

    /**
     * Converts seconds to milliseconds.
     *
     * @param seconds the nanoseconds to convert
     * @return {@link QueryConstants#NULL_LONG} if the input is {@link QueryConstants#NULL_LONG}; otherwise the input
     *         seconds converted to milliseconds, rounded down
     */
    @ScriptApi
    public static long secondsToMillis(final long seconds) {
        if (seconds == NULL_LONG) {
            return NULL_LONG;
        }

        return seconds * 1_000L;
    }

    /**
     * Converts nanoseconds to seconds.
     *
     * @param nanos the nanoseconds to convert
     * @return {@link QueryConstants#NULL_LONG} if the input is {@link QueryConstants#NULL_LONG}; otherwise the input
     *         nanoseconds converted to seconds, rounded down
     */
    @ScriptApi
    public static long nanosToSeconds(final long nanos) {
        if (nanos == NULL_LONG) {
            return NULL_LONG;
        }
        return nanos / SECOND;
    }

    /**
     * Converts microseconds to seconds.
     *
     * @param micros the microseconds to convert
     * @return {@link QueryConstants#NULL_LONG} if the input is {@link QueryConstants#NULL_LONG}; otherwise the input
     *         microseconds converted to seconds, rounded down
     */
    @ScriptApi
    public static long microsToSeconds(final long micros) {
        if (micros == NULL_LONG) {
            return NULL_LONG;
        }
        return micros / 1_000_000L;
    }

    /**
     * Converts milliseconds to seconds.
     *
     * @param millis the milliseconds to convert
     * @return {@link QueryConstants#NULL_LONG} if the input is {@link QueryConstants#NULL_LONG}; otherwise the input
     *         milliseconds converted to seconds, rounded down
     */
    @ScriptApi
    public static long millisToSeconds(final long millis) {
        if (millis == NULL_LONG) {
            return NULL_LONG;
        }
        return millis / 1_000L;
    }

    // endregion

    // region Conversions: Date Time Types


    /**
     * Converts a {@link ZonedDateTime} to an {@link Instant}.
     *
     * @param dateTime the zoned date time to convert
     * @return the {@link Instant}, or {@code null} if {@code dateTime} is {@code null}
     */
    @ScriptApi
    @Nullable
    public static Instant toInstant(@Nullable final ZonedDateTime dateTime) {
        if (dateTime == null) {
            return null;
        }

        return dateTime.toInstant();
    }

    /**
     * Converts a {@link LocalDateTime} and {@link ZoneId} to an {@link Instant}.
     *
     * @param localDateTime the local date time
     * @param timeZone the time zone
     * @return the {@link Instant}, or {@code null} if any input is {@code null}
     */
    @ScriptApi
    @Nullable
    public static Instant toInstant(
            @Nullable final LocalDateTime localDateTime,
            @Nullable final ZoneId timeZone) {
        if (localDateTime == null || timeZone == null) {
            return null;
        }

        return localDateTime // LocalDateTime
                .atZone(timeZone) // ZonedDateTime
                .toInstant(); // Instant
    }

    /**
     * Converts a {@link LocalDate}, {@link LocalTime}, and {@link ZoneId} to an {@link Instant}.
     *
     * @param date the local date
     * @param time the local time
     * @param timeZone the time zone
     * @return the {@link Instant}, or {@code null} if any input is {@code null}
     */
    @ScriptApi
    @Nullable
    public static Instant toInstant(
            @Nullable final LocalDate date,
            @Nullable final LocalTime time,
            @Nullable final ZoneId timeZone) {
        if (date == null || time == null || timeZone == null) {
            return null;
        }

        return time // LocalTime
                .atDate(date) // LocalDateTime
                .atZone(timeZone) // ZonedDateTime
                .toInstant(); // Instant
    }

    /**
     * Converts a {@link Date} to an {@link Instant}.
     *
     * @param date the date to convert
     * @return the {@link Instant}, or {@code null} if {@code date} is {@code null}
     */
    @Deprecated
    @ScriptApi
    @Nullable
    public static Instant toInstant(@Nullable final Date date) {
        if (date == null) {
            return null;
        }

        return epochMillisToInstant(date.getTime());
    }

    /**
     * Converts an {@link Instant} to a {@link ZonedDateTime}.
     *
     * @param instant the instant to convert
     * @param timeZone the time zone to use
     * @return the {@link ZonedDateTime}, or {@code null} if any input is {@code null}
     */
    @ScriptApi
    @Nullable
    public static ZonedDateTime toZonedDateTime(@Nullable final Instant instant, @Nullable final ZoneId timeZone) {
        if (instant == null || timeZone == null) {
            return null;
        }

        return ZonedDateTime.ofInstant(instant, timeZone);
    }

    /**
     * Converts a {@link LocalDateTime} and {@link ZoneId} to a {@link ZonedDateTime}.
     *
     * @param localDateTime the local date time
     * @param timeZone the time zone
     * @return the {@link ZonedDateTime}, or {@code null} if any input is {@code null}
     */
    @ScriptApi
    @Nullable
    public static ZonedDateTime toZonedDateTime(
            @Nullable final LocalDateTime localDateTime,
            @Nullable final ZoneId timeZone) {
        if (localDateTime == null || timeZone == null) {
            return null;
        }

        return localDateTime // LocalDateTime
                .atZone(timeZone); // ZonedDateTime
    }

    /**
     * Converts a {@link LocalDate}, {@link LocalTime}, and {@link ZoneId} to a {@link ZonedDateTime}.
     *
     * @param date the local date
     * @param time the local time
     * @param timeZone the time zone to use
     * @return the {@link ZonedDateTime}, or {@code null} if any input is {@code null}
     */
    @ScriptApi
    @Nullable
    public static ZonedDateTime toZonedDateTime(
            @Nullable final LocalDate date,
            @Nullable final LocalTime time,
            @Nullable final ZoneId timeZone) {
        if (date == null || time == null || timeZone == null) {
            return null;
        }

        return time // LocalTime
                .atDate(date) // LocalDateTime
                .atZone(timeZone); // ZonedDateTime
    }

    /**
     * Converts an {@link Instant} to a {@link LocalDateTime} with the specified {@link ZoneId}.
     *
     * @param instant the instant to convert
     * @param timeZone the time zone
     * @return the {@link LocalDateTime}, or {@code null} if any input is {@code null}
     */
    @ScriptApi
    @Nullable
    public static LocalDateTime toLocalDateTime(@Nullable final Instant instant, @Nullable final ZoneId timeZone) {
        if (instant == null || timeZone == null) {
            return null;
        }
        return toZonedDateTime(instant, timeZone).toLocalDateTime();
    }

    /**
     * Gets the {@link LocalDateTime} portion of a {@link ZonedDateTime}.
     *
     * @param dateTime the zoned date time to convert
     * @return the {@link LocalDateTime}, or {@code null} if {@code dateTime} is {@code null}
     */
    @ScriptApi
    @Nullable
    public static LocalDateTime toLocalDateTime(@Nullable final ZonedDateTime dateTime) {
        if (dateTime == null) {
            return null;
        }
        return dateTime.toLocalDateTime();
    }

    /**
     * Converts a {@link LocalDate} and {@link LocalTime} pair to a {@link LocalDateTime}.
     *
     * @param localDate the local date to convert
     * @param localTime the local time to convert
     * @return the {@link LocalDateTime}, or {@code null} if {@code localDateTime} is {@code null}
     */
    @ScriptApi
    @Nullable
    public static LocalDateTime toLocalDateTime(
            @Nullable final LocalDate localDate,
            @Nullable final LocalTime localTime) {
        if (localDate == null || localTime == null) {
            return null;
        }
        return LocalDateTime.of(localDate, localTime);
    }

    /**
     * Converts an {@link Instant} to a {@link LocalDate} with the specified {@link ZoneId}.
     *
     * @param instant the instant to convert
     * @param timeZone the time zone
     * @return the {@link LocalDate}, or {@code null} if any input is {@code null}
     */
    @ScriptApi
    @Nullable
    public static LocalDate toLocalDate(@Nullable final Instant instant, @Nullable final ZoneId timeZone) {
        if (instant == null || timeZone == null) {
            return null;
        }

        return toZonedDateTime(instant, timeZone).toLocalDate();
    }

    /**
     * Gets the {@link LocalDate} portion of a {@link ZonedDateTime}.
     *
     * @param dateTime the zoned date time to convert
     * @return the {@link LocalDate}, or {@code null} if {@code dateTime} is {@code null}
     */
    @ScriptApi
    @Nullable
    public static LocalDate toLocalDate(@Nullable final ZonedDateTime dateTime) {
        if (dateTime == null) {
            return null;
        }

        return dateTime.toLocalDate();
    }

    /**
     * Gets the {@link LocalDate} portion of a {@link LocalDateTime}.
     *
     * @param localDateTime the local date time to convert
     * @return the {@link LocalDate}, or {@code null} if {@code localDateTime} is {@code null}
     */
    @ScriptApi
    @Nullable
    public static LocalDate toLocalDate(@Nullable final LocalDateTime localDateTime) {
        if (localDateTime == null) {
            return null;
        }

        return localDateTime.toLocalDate();
    }

    /**
     * Converts an {@link Instant} to a {@link LocalTime} with the specified {@link ZoneId}.
     *
     * @param instant the instant to convert
     * @param timeZone the time zone
     * @return the {@link LocalTime}, or {@code null} if any input is {@code null}
     */
    @ScriptApi
    @Nullable
    public static LocalTime toLocalTime(@Nullable final Instant instant, @Nullable final ZoneId timeZone) {
        if (instant == null || timeZone == null) {
            return null;
        }
        return toZonedDateTime(instant, timeZone).toLocalTime();
    }

    /**
     * Gets the {@link LocalTime} portion of a {@link ZonedDateTime}.
     *
     * @param dateTime the zoned date time to convert
     * @return the {@link LocalTime}, or {@code null} if {@code dateTime} is {@code null}
     */
    @ScriptApi
    @Nullable
    public static LocalTime toLocalTime(@Nullable final ZonedDateTime dateTime) {
        if (dateTime == null) {
            return null;
        }
        return dateTime.toLocalTime();
    }

    /**
     * Gets the {@link LocalTime} portion of a {@link LocalDateTime}.
     *
     * @param localDateTime the local date time to convert
     * @return the {@link LocalTime}, or {@code null} if {@code localDateTime} is {@code null}
     */
    @ScriptApi
    @Nullable
    public static LocalTime toLocalTime(@Nullable final LocalDateTime localDateTime) {
        if (localDateTime == null) {
            return null;
        }
        return localDateTime.toLocalTime();
    }

    /**
     * Converts the number of milliseconds from midnight to a {@link LocalTime}
     *
     * @param millis milliseconds from midnight
     * @return the {@link LocalTime}, or {@code null} if any input is {@link QueryConstants#NULL_INT NULL_INT}
     */
    @ScriptApi
    public static @Nullable LocalTime millisOfDayToLocalTime(final int millis) {
        if (millis == NULL_INT) {
            return null;
        }
        return LocalTime.ofNanoOfDay(millis * MILLI);
    }

    /**
     * Converts the number of microseconds from midnight to a {@link LocalTime}
     *
     * @param micros microseconds from midnight
     * @return the {@link LocalTime}, or {@code null} if any input is {@link QueryConstants#NULL_LONG NULL_LONG}
     */
    @ScriptApi
    public static @Nullable LocalTime microsOfDayToLocalTime(final long micros) {
        if (micros == NULL_LONG) {
            return null;
        }
        return LocalTime.ofNanoOfDay(micros * MICRO);
    }

    /**
     * Converts the number of nanoseconds from midnight to a {@link LocalTime}
     *
     * @param nanos nanoseconds from midnight
     * @return the {@link LocalTime}, or {@code null} if any input is {@link QueryConstants#NULL_LONG NULL_LONG}
     */
    @ScriptApi
    public static @Nullable LocalTime nanosOfDayToLocalTime(final long nanos) {
        if (nanos == NULL_LONG) {
            return null;
        }
        return LocalTime.ofNanoOfDay(nanos);
    }

    /**
     * Converts an {@link Instant} to a {@link Date}. {@code instant} will be truncated to millisecond resolution.
     *
     * @param instant the instant to convert
     * @return the {@link Date}, or {@code null} if {@code instant} is {@code null}
     * @deprecated
     */
    @Deprecated
    @ScriptApi
    @Nullable
    public static Date toDate(@Nullable final Instant instant) {
        if (instant == null) {
            return null;
        }
        return new Date(epochMillis(instant));
    }

    /**
     * Converts a {@link ZonedDateTime} to a {@link Date}. {@code dateTime} will be truncated to millisecond resolution.
     *
     * @param dateTime the zoned date time to convert
     * @return the {@link Date}, or {@code null} if {@code dateTime} is {@code null}
     * @deprecated
     */
    @Deprecated
    @ScriptApi
    @Nullable
    public static Date toDate(@Nullable final ZonedDateTime dateTime) {
        if (dateTime == null) {
            return null;
        }
        return new Date(epochMillis(dateTime));
    }

    // endregion

    // region Conversions: Epoch

    /**
     * Returns nanoseconds from the Epoch for an {@link Instant} value.
     *
     * @param instant instant to compute the Epoch offset for
     * @return nanoseconds since Epoch, or a NULL_LONG value if the instant is null
     */
    @ScriptApi
    public static long epochNanos(@Nullable final Instant instant) {
        if (instant == null) {
            return NULL_LONG;
        }

        return safeComputeNanos(instant.getEpochSecond(), instant.getNano());
    }

    /**
     * Returns nanoseconds from the Epoch for a {@link ZonedDateTime} value.
     *
     * @param dateTime the zoned date time to compute the Epoch offset for
     * @return nanoseconds since Epoch, or a NULL_LONG value if the zoned date time is null
     */
    @ScriptApi
    public static long epochNanos(@Nullable final ZonedDateTime dateTime) {
        if (dateTime == null) {
            return NULL_LONG;
        }

        return safeComputeNanos(dateTime.toEpochSecond(), dateTime.getNano());
    }

    /**
     * Returns microseconds from the Epoch for an {@link Instant} value.
     *
     * @param instant instant to compute the Epoch offset for
     * @return microseconds since Epoch, or a {@link QueryConstants#NULL_LONG NULL_LONG} value if the instant is
     *         {@code null}
     */
    @ScriptApi
    public static long epochMicros(@Nullable final Instant instant) {
        if (instant == null) {
            return NULL_LONG;
        }

        return nanosToMicros(epochNanos(instant));
    }

    /**
     * Returns microseconds from the Epoch for a {@link ZonedDateTime} value.
     *
     * @param dateTime zoned date time to compute the Epoch offset for
     * @return microseconds since Epoch, or a {@link QueryConstants#NULL_LONG NULL_LONG} value if the zoned date time is
     *         {@code null}
     */
    @ScriptApi
    public static long epochMicros(@Nullable final ZonedDateTime dateTime) {
        if (dateTime == null) {
            return NULL_LONG;
        }

        return nanosToMicros(epochNanos(dateTime));
    }

    /**
     * Returns milliseconds from the Epoch for an {@link Instant} value.
     *
     * @param instant instant to compute the Epoch offset for
     * @return milliseconds since Epoch, or a {@link QueryConstants#NULL_LONG NULL_LONG} value if the instant is
     *         {@code null}
     */
    @ScriptApi
    public static long epochMillis(@Nullable final Instant instant) {
        if (instant == null) {
            return NULL_LONG;
        }

        return instant.toEpochMilli();
    }

    /**
     * Returns milliseconds from the Epoch for a {@link ZonedDateTime} value.
     *
     * @param dateTime zoned date time to compute the Epoch offset for
     * @return milliseconds since Epoch, or a {@link QueryConstants#NULL_LONG NULL_LONG} value if the zoned date time is
     *         {@code null}
     */
    @ScriptApi
    public static long epochMillis(@Nullable final ZonedDateTime dateTime) {
        if (dateTime == null) {
            return NULL_LONG;
        }

        return nanosToMillis(epochNanos(dateTime));
    }

    /**
     * Returns seconds since from the Epoch for an {@link Instant} value.
     *
     * @param instant instant to compute the Epoch offset for
     * @return seconds since Epoch, or a {@link QueryConstants#NULL_LONG NULL_LONG} value if the instant is {@code null}
     */
    @ScriptApi
    public static long epochSeconds(@Nullable final Instant instant) {
        if (instant == null) {
            return NULL_LONG;
        }

        return instant.getEpochSecond();
    }

    /**
     * Returns seconds since from the Epoch for a {@link ZonedDateTime} value.
     *
     * @param dateTime zoned date time to compute the Epoch offset for
     * @return seconds since Epoch, or a {@link QueryConstants#NULL_LONG NULL_LONG} value if the zoned date time is
     *         {@code null}
     */
    @ScriptApi
    public static long epochSeconds(@Nullable final ZonedDateTime dateTime) {
        if (dateTime == null) {
            return NULL_LONG;
        }

        return dateTime.toEpochSecond();
    }

    /**
     * Returns number of days from the Epoch for a {@link LocalDate} value.
     *
     * @param date date to compute the Epoch offset for
     * @return days since Epoch, or a {@link QueryConstants#NULL_LONG NULL_LONG} value if the instant is {@code null}
     */
    @ScriptApi
    public static long epochDays(@Nullable final LocalDate date) {
        if (date == null) {
            return NULL_LONG;
        }
        return date.toEpochDay();
    }

    /**
     * Returns number of days (as an {@code int}) from the Epoch for a {@link LocalDate} value.
     *
     * @param date date to compute the Epoch offset for
     * @return days since Epoch, or a {@link QueryConstants#NULL_INT NULL_INT} value if the instant is {@code null}
     */
    @ScriptApi
    public static int epochDaysAsInt(@Nullable final LocalDate date) {
        if (date == null) {
            return NULL_INT;
        }
        return Math.toIntExact(date.toEpochDay());
    }

    /**
     * Converts nanoseconds from the Epoch to an {@link Instant}.
     *
     * @param nanos nanoseconds since Epoch
     * @return {@code null} if the input is {@link QueryConstants#NULL_LONG}; otherwise the input nanoseconds from the
     *         Epoch converted to an {@link Instant}
     */
    @ScriptApi
    @Nullable
    public static Instant epochNanosToInstant(final long nanos) {
        return nanos == NULL_LONG ? null : Instant.ofEpochSecond(nanos / 1_000_000_000L, nanos % 1_000_000_000L);
    }

    /**
     * Converts microseconds from the Epoch to an {@link Instant}.
     *
     * @param micros microseconds since Epoch
     * @return {@code null} if the input is {@link QueryConstants#NULL_LONG}; otherwise the input microseconds from the
     *         Epoch converted to an {@link Instant}
     */
    @ScriptApi
    @Nullable
    public static Instant epochMicrosToInstant(final long micros) {
        return micros == NULL_LONG ? null : Instant.ofEpochSecond(micros / 1_000_000L, (micros % 1_000_000L) * 1_000L);
    }

    /**
     * Converts milliseconds from the Epoch to an {@link Instant}.
     *
     * @param millis milliseconds since Epoch
     * @return {@code null} if the input is {@link QueryConstants#NULL_LONG}; otherwise the input milliseconds from the
     *         Epoch converted to an {@link Instant}
     */
    @ScriptApi
    @Nullable
    public static Instant epochMillisToInstant(final long millis) {
        return millis == NULL_LONG ? null : Instant.ofEpochSecond(millis / 1_000L, (millis % 1_000L) * 1_000_000L);
    }

    /**
     * Converts seconds from the Epoch to an {@link Instant}.
     *
     * @param seconds seconds since Epoch
     * @return {@code null} if the input is {@link QueryConstants#NULL_LONG}; otherwise the input seconds from the Epoch
     *         converted to an {@link Instant}
     */
    @ScriptApi
    @Nullable
    public static Instant epochSecondsToInstant(final long seconds) {
        return seconds == NULL_LONG ? null : Instant.ofEpochSecond(seconds, 0);
    }

    /**
     * Converts nanoseconds from the Epoch to a {@link ZonedDateTime}.
     *
     * @param nanos nanoseconds since Epoch
     * @param timeZone time zone
     * @return {@code null} if the input is {@link QueryConstants#NULL_LONG}; otherwise the input nanoseconds from the
     *         Epoch converted to a {@link ZonedDateTime}
     */
    @ScriptApi
    @Nullable
    public static ZonedDateTime epochNanosToZonedDateTime(final long nanos, final ZoneId timeZone) {
        if (timeZone == null) {
            return null;
        }

        // noinspection ConstantConditions
        return nanos == NULL_LONG ? null : ZonedDateTime.ofInstant(epochNanosToInstant(nanos), timeZone);
    }

    /**
     * Converts microseconds from the Epoch to a {@link ZonedDateTime}.
     *
     * @param micros microseconds since Epoch
     * @param timeZone time zone
     * @return {@code null} if the input is {@link QueryConstants#NULL_LONG}; otherwise the input microseconds from the
     *         Epoch converted to a {@link ZonedDateTime}
     */
    @ScriptApi
    @Nullable
    public static ZonedDateTime epochMicrosToZonedDateTime(final long micros, @Nullable ZoneId timeZone) {
        if (timeZone == null) {
            return null;
        }

        // noinspection ConstantConditions
        return micros == NULL_LONG ? null : ZonedDateTime.ofInstant(epochMicrosToInstant(micros), timeZone);
    }

    /**
     * Converts milliseconds from the Epoch to a {@link ZonedDateTime}.
     *
     * @param millis milliseconds since Epoch
     * @param timeZone time zone
     * @return {@code null} if the input is {@link QueryConstants#NULL_LONG}; otherwise the input milliseconds from the
     *         Epoch converted to a {@link ZonedDateTime}
     */
    @ScriptApi
    @Nullable
    public static ZonedDateTime epochMillisToZonedDateTime(final long millis, @Nullable final ZoneId timeZone) {
        if (timeZone == null) {
            return null;
        }

        // noinspection ConstantConditions
        return millis == NULL_LONG ? null : ZonedDateTime.ofInstant(epochMillisToInstant(millis), timeZone);
    }

    /**
     * Converts seconds from the Epoch to a {@link ZonedDateTime}.
     *
     * @param seconds seconds since Epoch
     * @param timeZone time zone
     * @return {@code null} if the input is {@link QueryConstants#NULL_LONG}; otherwise the input seconds from the Epoch
     *         converted to a {@link ZonedDateTime}
     */
    @ScriptApi
    @Nullable
    public static ZonedDateTime epochSecondsToZonedDateTime(final long seconds, @Nullable final ZoneId timeZone) {
        if (timeZone == null) {
            return null;
        }
        // noinspection ConstantConditions
        return seconds == NULL_LONG ? null : ZonedDateTime.ofInstant(epochSecondsToInstant(seconds), timeZone);
    }

    /**
     * Converts an offset from the Epoch to a nanoseconds from the Epoch. The offset can be in milliseconds,
     * microseconds, or nanoseconds. Expected date ranges are used to infer the units for the offset.
     *
     * @param epochOffset time offset from the Epoch
     * @return {@code null} if the input is {@link QueryConstants#NULL_LONG}; otherwise the input offset from the Epoch
     *         converted to nanoseconds from the Epoch
     */
    @ScriptApi
    public static long epochAutoToEpochNanos(final long epochOffset) {
        if (epochOffset == NULL_LONG) {
            return epochOffset;
        }
        final long absEpoch = Math.abs(epochOffset);
        if (absEpoch > 1000 * TimeConstants.MICROTIME_THRESHOLD) { // Nanoseconds
            return epochOffset;
        }
        if (absEpoch > TimeConstants.MICROTIME_THRESHOLD) { // Microseconds
            return 1000 * epochOffset;
        }
        if (absEpoch > TimeConstants.MICROTIME_THRESHOLD / 1000) { // Milliseconds
            return 1000 * 1000 * epochOffset;
        }
        // Seconds
        return 1000 * 1000 * 1000 * epochOffset;
    }

    /**
     * Converts an offset from the Epoch to an {@link Instant}. The offset can be in milliseconds, microseconds, or
     * nanoseconds. Expected date ranges are used to infer the units for the offset.
     *
     * @param epochOffset time offset from the Epoch
     * @return {@code null} if the input is {@link QueryConstants#NULL_LONG}; otherwise the input offset from the Epoch
     *         converted to an {@link Instant}
     */
    @ScriptApi
    @Nullable
    public static Instant epochAutoToInstant(final long epochOffset) {
        if (epochOffset == NULL_LONG) {
            return null;
        }
        return epochNanosToInstant(epochAutoToEpochNanos(epochOffset));
    }

    /**
     * Converts an offset from the Epoch to a {@link ZonedDateTime}. The offset can be in milliseconds, microseconds, or
     * nanoseconds. Expected date ranges are used to infer the units for the offset.
     *
     * @param epochOffset time offset from the Epoch
     * @param timeZone time zone
     * @return {@code null} if any input is {@code null} or {@link QueryConstants#NULL_LONG}; otherwise the input offset
     *         from the Epoch converted to a {@link ZonedDateTime}
     */
    @ScriptApi
    @Nullable
    public static ZonedDateTime epochAutoToZonedDateTime(final long epochOffset, @Nullable ZoneId timeZone) {
        if (epochOffset == NULL_LONG || timeZone == null) {
            return null;
        }
        return epochNanosToZonedDateTime(epochAutoToEpochNanos(epochOffset), timeZone);
    }

    /**
     * Converts days from the Epoch to a {@link LocalDate}.
     *
     * @param days days since Epoch
     * @return {@code null} if the input is {@link QueryConstants#NULL_LONG}; otherwise the input days from the Epoch
     *         converted to a {@link LocalDate}
     */
    @ScriptApi
    public static @Nullable LocalDate epochDaysToLocalDate(final long days) {
        return days == NULL_LONG ? null : LocalDate.ofEpochDay(days);
    }

    /**
     * Converts days from the Epoch (stored as {@code int}) to a {@link LocalDate}.
     *
     * @param days days since Epoch
     * @return {@code null} if the input is {@link QueryConstants#NULL_INT}; otherwise the input days from the Epoch
     *         converted to a {@link LocalDate}
     */
    @ScriptApi
    public static @Nullable LocalDate epochDaysAsIntToLocalDate(final int days) {
        return days == NULL_INT ? null : LocalDate.ofEpochDay(days);
    }

    // endregion

    // region Conversions: Excel

    private static double epochMillisToExcelTime(final long millis, final ZoneId timeZone) {
        return (double) (millis + java.util.TimeZone.getTimeZone(timeZone).getOffset(millis)) / 86400000 + 25569;
    }

    private static long excelTimeToEpochMillis(final double excel, final ZoneId timeZone) {
        final java.util.TimeZone tz = java.util.TimeZone.getTimeZone(timeZone);

        final long mpo = (long) ((excel - 25569) * 86400000);
        final long o = tz.getOffset(mpo);
        final long m = mpo - o;
        final long o2 = tz.getOffset(m);
        return mpo - o2;
    }

    /**
     * Converts an {@link Instant} to an Excel time represented as a double.
     *
     * @param instant instant to convert
     * @param timeZone time zone to use when interpreting the instant
     * @return 0.0 if either input is {@code null}; otherwise, the input instant converted to an Excel time represented
     *         as a double
     */
    @ScriptApi
    public static double toExcelTime(@Nullable final Instant instant, @Nullable final ZoneId timeZone) {
        if (instant == null || timeZone == null) {
            return 0.0;
        }

        return epochMillisToExcelTime(epochMillis(instant), timeZone);
    }

    /**
     * Converts a {@link ZonedDateTime} to an Excel time represented as a double.
     *
     * @param dateTime zoned date time to convert
     * @return 0.0 if either input is {@code null}; otherwise, the input zoned date time converted to an Excel time
     *         represented as a double
     */
    @ScriptApi
    public static double toExcelTime(@Nullable final ZonedDateTime dateTime) {
        if (dateTime == null) {
            return 0.0;
        }

        return toExcelTime(toInstant(dateTime), dateTime.getZone());
    }

    /**
     * Converts an Excel time represented as a double to an {@link Instant}.
     *
     * @param excel excel time represented as a double
     * @param timeZone time zone to use when interpreting the Excel time
     * @return {@code null} if timeZone is {@code null}; otherwise, the input Excel time converted to an {@link Instant}
     */
    @ScriptApi
    @Nullable
    public static Instant excelToInstant(final double excel, @Nullable final ZoneId timeZone) {
        if (timeZone == null) {
            return null;
        }

        return epochMillisToInstant(excelTimeToEpochMillis(excel, timeZone));
    }

    /**
     * Converts an Excel time represented as a double to a {@link ZonedDateTime}.
     *
     * @param excel excel time represented as a double
     * @param timeZone time zone to use when interpreting the Excel time
     * @return {@code null} if timeZone is {@code null}; otherwise, the input Excel time converted to a
     *         {@link ZonedDateTime}
     */
    @ScriptApi
    @Nullable
    public static ZonedDateTime excelToZonedDateTime(final double excel, @Nullable final ZoneId timeZone) {
        if (timeZone == null) {
            return null;
        }

        return epochMillisToZonedDateTime(excelTimeToEpochMillis(excel, timeZone), timeZone);
    }

    // endregion

    // region Arithmetic

    /**
     * Adds days to a {@link LocalDate}.
     *
     * @param localDateTime starting local date time
     * @param days number of days to add
     * @return {@code null} if either input is {@code null} or {@link QueryConstants#NULL_LONG}; otherwise the starting
     *         local date time plus the specified number of days
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    @ScriptApi
    @Nullable
    public static LocalDateTime plusDays(@Nullable final LocalDateTime localDateTime, final long days) {
        if (localDateTime == null || days == NULL_LONG) {
            return null;
        }

        try {
            return localDateTime.plusDays(days);
        } catch (DateTimeException | ArithmeticException ex) {
            throw new DateTimeOverflowException(ex);
        }
    }

    /**
     * Adds days to a {@link LocalDate}.
     *
     * @param localDate starting local date
     * @param days number of days to add
     * @return {@code null} if either input is {@code null} or {@link QueryConstants#NULL_LONG}; otherwise the starting
     *         local date plus the specified number of days
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    @ScriptApi
    @Nullable
    public static LocalDate plusDays(@Nullable final LocalDate localDate, final long days) {
        if (localDate == null || days == NULL_LONG) {
            return null;
        }

        try {
            return localDate.plusDays(days);
        } catch (DateTimeException | ArithmeticException ex) {
            throw new DateTimeOverflowException(ex);
        }
    }

    /**
     * Adds a time period to a {@link LocalDateTime}.
     *
     * @param localDateTime starting local date time
     * @param period time period
     * @return {@code null} if either input is {@code null}; otherwise the starting local date time plus the specified
     *         time period
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    @ScriptApi
    @Nullable
    public static LocalDateTime plus(@Nullable final LocalDateTime localDateTime, final Period period) {
        if (localDateTime == null || period == null) {
            return null;
        }

        try {
            return localDateTime.plus(period);
        } catch (DateTimeException | ArithmeticException ex) {
            throw new DateTimeOverflowException(ex);
        }
    }

    /**
     * Adds a time period to a {@link LocalDate}.
     *
     * @param localDate starting local date
     * @param period time period
     * @return {@code null} if either input is {@code null}; otherwise the starting local date plus the specified time
     *         period
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    @ScriptApi
    @Nullable
    public static LocalDate plus(@Nullable final LocalDate localDate, final Period period) {
        if (localDate == null || period == null) {
            return null;
        }

        try {
            return localDate.plus(period);
        } catch (DateTimeException | ArithmeticException ex) {
            throw new DateTimeOverflowException(ex);
        }
    }

    /**
     * Adds nanoseconds to an {@link Instant}.
     *
     * @param instant starting instant value
     * @param nanos number of nanoseconds to add
     * @return {@code null} if either input is {@code null} or {@link QueryConstants#NULL_LONG}; otherwise the starting
     *         instant plus the specified number of nanoseconds
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    @ScriptApi
    @Nullable
    public static Instant plus(@Nullable final Instant instant, final long nanos) {
        if (instant == null || nanos == NULL_LONG) {
            return null;
        }

        try {
            return instant.plusNanos(nanos);
        } catch (DateTimeException | ArithmeticException ex) {
            throw new DateTimeOverflowException(ex);
        }
    }

    /**
     * Adds nanoseconds to a {@link ZonedDateTime}.
     *
     * @param dateTime starting zoned date time value
     * @param nanos number of nanoseconds to add
     * @return {@code null} if either input is {@code null} or {@link QueryConstants#NULL_LONG}; otherwise the starting
     *         zoned date time plus the specified number of nanoseconds
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    @ScriptApi
    @Nullable
    public static ZonedDateTime plus(@Nullable final ZonedDateTime dateTime, final long nanos) {
        if (dateTime == null || nanos == NULL_LONG) {
            return null;
        }

        try {
            return dateTime.plusNanos(nanos);
        } catch (DateTimeException | ArithmeticException ex) {
            throw new DateTimeOverflowException(ex);
        }
    }

    /**
     * Adds a time period to an {@link Instant}.
     *
     * @param instant starting instant value
     * @param duration time period
     * @return {@code null} if either input is {@code null} or {@link QueryConstants#NULL_LONG}; otherwise the starting
     *         instant plus the specified time period
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    @ScriptApi
    @Nullable
    public static Instant plus(@Nullable final Instant instant, @Nullable final Duration duration) {
        if (instant == null || duration == null) {
            return null;
        }

        try {
            return instant.plus(duration);
        } catch (DateTimeException | ArithmeticException ex) {
            throw new DateTimeOverflowException(ex);
        }
    }

    /**
     * Adds a time period to an {@link Instant}.
     *
     * @param instant starting instant value
     * @param period time period
     * @return {@code null} if either input is {@code null} or {@link QueryConstants#NULL_LONG}; otherwise the starting
     *         instant plus the specified time period
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    @ScriptApi
    @Nullable
    public static Instant plus(@Nullable final Instant instant, @Nullable final Period period) {
        if (instant == null || period == null) {
            return null;
        }

        try {
            return instant.plus(period);
        } catch (DateTimeException | ArithmeticException ex) {
            throw new DateTimeOverflowException(ex);
        }
    }

    /**
     * Adds a time period to a {@link ZonedDateTime}.
     *
     * @param dateTime starting zoned date time value
     * @param duration time period
     * @return {@code null} if either input is {@code null} or {@link QueryConstants#NULL_LONG}; otherwise the starting
     *         zoned date time plus the specified time period
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    @ScriptApi
    @Nullable
    public static ZonedDateTime plus(@Nullable final ZonedDateTime dateTime, @Nullable final Duration duration) {
        if (dateTime == null || duration == null) {
            return null;
        }

        try {
            return dateTime.plus(duration);
        } catch (DateTimeException | ArithmeticException ex) {
            throw new DateTimeOverflowException(ex);
        }
    }

    /**
     * Adds a time period to a {@link ZonedDateTime}.
     *
     * @param dateTime starting zoned date time value
     * @param period time period
     * @return {@code null} if either input is {@code null} or {@link QueryConstants#NULL_LONG}; otherwise the starting
     *         zoned date time plus the specified time period
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    @ScriptApi
    @Nullable
    public static ZonedDateTime plus(@Nullable final ZonedDateTime dateTime, @Nullable final Period period) {
        if (dateTime == null || period == null) {
            return null;
        }

        try {
            return dateTime.plus(period);
        } catch (DateTimeException | ArithmeticException ex) {
            throw new DateTimeOverflowException(ex);
        }
    }

    /**
     * Adds two durations.
     *
     * @param duration1 first duration
     * @param duration2 second duration
     * @return {@code null} if either input is {@code null}; otherwise the sum of the two durations
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    public static Duration plus(@Nullable final Duration duration1, @Nullable final Duration duration2) {
        if (duration1 == null || duration2 == null) {
            return null;
        }

        try {
            return duration1.plus(duration2);
        } catch (DateTimeException | ArithmeticException ex) {
            throw new DateTimeOverflowException(ex);
        }
    }

    /**
     * Adds two periods.
     *
     * @param period1 first period
     * @param period2 second period
     * @return {@code null} if either input is {@code null}; otherwise the sum of the two periods
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    public static Period plus(@Nullable final Period period1, @Nullable final Period period2) {
        if (period1 == null || period2 == null) {
            return null;
        }

        try {
            return period1.plus(period2);
        } catch (DateTimeException | ArithmeticException ex) {
            throw new DateTimeOverflowException(ex);
        }
    }

    /**
     * Subtracts days from a {@link LocalDate}.
     *
     * @param localDateTime starting date
     * @param days number of days to subtract
     * @return {@code null} if either input is {@code null} or {@link QueryConstants#NULL_LONG}; otherwise the starting
     *         local date time minus the specified number of days
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    @ScriptApi
    @Nullable
    public static LocalDateTime minusDays(@Nullable final LocalDateTime localDateTime, final long days) {
        if (localDateTime == null || days == NULL_LONG) {
            return null;
        }

        try {
            return localDateTime.minusDays(days);
        } catch (DateTimeException | ArithmeticException ex) {
            throw new DateTimeOverflowException(ex);
        }
    }

    /**
     * Subtracts days from a {@link LocalDate}.
     *
     * @param localDate starting local date
     * @param days number of days to subtract
     * @return {@code null} if either input is {@code null} or {@link QueryConstants#NULL_LONG}; otherwise the starting
     *         local date minus the specified number of days
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    @ScriptApi
    @Nullable
    public static LocalDate minusDays(@Nullable final LocalDate localDate, final long days) {
        if (localDate == null || days == NULL_LONG) {
            return null;
        }

        try {
            return localDate.minusDays(days);
        } catch (DateTimeException | ArithmeticException ex) {
            throw new DateTimeOverflowException(ex);
        }
    }

    /**
     * Subtracts a time period from a {@link LocalDateTime}.
     *
     * @param localDateTime starting local date time
     * @param period time period
     * @return {@code null} if either input is {@code null}; otherwise the starting local date time minus the specified
     *         time period
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    @ScriptApi
    @Nullable
    public static LocalDateTime minus(@Nullable final LocalDateTime localDateTime, final Period period) {
        if (localDateTime == null || period == null) {
            return null;
        }

        try {
            return localDateTime.minus(period);
        } catch (DateTimeException | ArithmeticException ex) {
            throw new DateTimeOverflowException(ex);
        }
    }

    /**
     * Subtracts a time period from a {@link LocalDate}.
     *
     * @param localDate starting local date
     * @param period time period
     * @return {@code null} if either input is {@code null}; otherwise the starting local date minus the specified time
     *         period
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    @ScriptApi
    @Nullable
    public static LocalDate minus(@Nullable final LocalDate localDate, final Period period) {
        if (localDate == null || period == null) {
            return null;
        }

        try {
            return localDate.minus(period);
        } catch (DateTimeException | ArithmeticException ex) {
            throw new DateTimeOverflowException(ex);
        }
    }

    /**
     * Subtracts nanoseconds from an {@link Instant}.
     *
     * @param instant starting instant value
     * @param nanos number of nanoseconds to subtract
     * @return {@code null} if either input is {@code null} or {@link QueryConstants#NULL_LONG}; otherwise the starting
     *         instant minus the specified number of nanoseconds
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    @ScriptApi
    @Nullable
    public static Instant minus(@Nullable final Instant instant, final long nanos) {
        if (instant == null || nanos == NULL_LONG) {
            return null;
        }

        try {
            return instant.minusNanos(nanos);
        } catch (DateTimeException | ArithmeticException ex) {
            throw new DateTimeOverflowException(ex);
        }
    }

    /**
     * Subtracts nanoseconds from a {@link ZonedDateTime}.
     *
     * @param dateTime starting zoned date time value
     * @param nanos number of nanoseconds to subtract
     * @return {@code null} if either input is {@code null} or {@link QueryConstants#NULL_LONG}; otherwise the starting
     *         zoned date time minus the specified number of nanoseconds
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    @ScriptApi
    @Nullable
    public static ZonedDateTime minus(@Nullable final ZonedDateTime dateTime, final long nanos) {
        if (dateTime == null || nanos == NULL_LONG) {
            return null;
        }

        try {
            return dateTime.minusNanos(nanos);
        } catch (DateTimeException | ArithmeticException ex) {
            throw new DateTimeOverflowException(ex);
        }
    }

    /**
     * Subtracts a time period to an {@link Instant}.
     *
     * @param instant starting instant value
     * @param duration time period
     * @return {@code null} if either input is {@code null} or {@link QueryConstants#NULL_LONG}; otherwise the starting
     *         instant minus the specified time period
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    @ScriptApi
    @Nullable
    public static Instant minus(@Nullable final Instant instant, @Nullable final Duration duration) {
        if (instant == null || duration == null) {
            return null;
        }

        try {
            return instant.minus(duration);
        } catch (DateTimeException | ArithmeticException ex) {
            throw new DateTimeOverflowException(ex);
        }
    }

    /**
     * Subtracts a time period to an {@link Instant}.
     *
     * @param instant starting instant value
     * @param period time period
     * @return {@code null} if either input is {@code null} or {@link QueryConstants#NULL_LONG}; otherwise the starting
     *         instant minus the specified time period
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    @ScriptApi
    @Nullable
    public static Instant minus(@Nullable final Instant instant, @Nullable final Period period) {
        if (instant == null || period == null) {
            return null;
        }

        try {
            return instant.minus(period);
        } catch (DateTimeException | ArithmeticException ex) {
            throw new DateTimeOverflowException(ex);
        }
    }

    /**
     * Subtracts a time period to a {@link ZonedDateTime}.
     *
     * @param dateTime starting zoned date time value
     * @param duration time period
     * @return {@code null} if either input is {@code null} or {@link QueryConstants#NULL_LONG}; otherwise the starting
     *         zoned date time minus the specified time period
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    @ScriptApi
    @Nullable
    public static ZonedDateTime minus(@Nullable final ZonedDateTime dateTime, @Nullable final Duration duration) {
        if (dateTime == null || duration == null) {
            return null;
        }

        try {
            return dateTime.minus(duration);
        } catch (DateTimeException | ArithmeticException ex) {
            throw new DateTimeOverflowException(ex);
        }
    }

    /**
     * Subtracts a time period to a {@link ZonedDateTime}.
     *
     * @param dateTime starting zoned date time value
     * @param period time period
     * @return {@code null} if either input is {@code null} or {@link QueryConstants#NULL_LONG}; otherwise the starting
     *         zoned date time minus the specified time period
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    @ScriptApi
    @Nullable
    public static ZonedDateTime minus(@Nullable final ZonedDateTime dateTime, @Nullable final Period period) {
        if (dateTime == null || period == null) {
            return null;
        }

        try {
            return dateTime.minus(period);
        } catch (DateTimeException | ArithmeticException ex) {
            throw new DateTimeOverflowException(ex);
        }
    }

    /**
     * Subtract one instant from another and return the difference in nanoseconds.
     *
     * @param instant1 first instant
     * @param instant2 second instant
     * @return {@link QueryConstants#NULL_LONG} if either input is {@code null}; otherwise the difference in instant1
     *         and instant2 in nanoseconds
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    @ScriptApi
    public static long minus(@Nullable final Instant instant1, @Nullable final Instant instant2) {
        if (instant1 == null || instant2 == null) {
            return NULL_LONG;
        }

        return checkUnderflowMinus(epochNanos(instant1), epochNanos(instant2), true);
    }

    /**
     * Subtract one zoned date time from another and return the difference in nanoseconds.
     *
     * @param dateTime1 first zoned date time
     * @param dateTime2 second zoned date time
     * @return {@link QueryConstants#NULL_LONG} if either input is {@code null}; otherwise the difference in dateTime1
     *         and dateTime2 in nanoseconds
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    @ScriptApi
    public static long minus(@Nullable final ZonedDateTime dateTime1, @Nullable final ZonedDateTime dateTime2) {
        if (dateTime1 == null || dateTime2 == null) {
            return NULL_LONG;
        }

        return checkUnderflowMinus(epochNanos(dateTime1), epochNanos(dateTime2), true);
    }

    /**
     * Subtracts two durations.
     *
     * @param duration1 first duration
     * @param duration2 second duration
     * @return {@code null} if either input is {@code null}; otherwise the difference of the two durations
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    public static Duration minus(@Nullable final Duration duration1, @Nullable final Duration duration2) {
        if (duration1 == null || duration2 == null) {
            return null;
        }

        try {
            return duration1.minus(duration2);
        } catch (DateTimeException | ArithmeticException ex) {
            throw new DateTimeOverflowException(ex);
        }
    }

    /**
     * Subtracts two periods.
     *
     * @param period1 first period
     * @param period2 second period
     * @return {@code null} if either input is {@code null}; otherwise the difference of the two periods
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    public static Period minus(@Nullable final Period period1, @Nullable final Period period2) {
        if (period1 == null || period2 == null) {
            return null;
        }

        try {
            return period1.minus(period2);
        } catch (DateTimeException | ArithmeticException ex) {
            throw new DateTimeOverflowException(ex);
        }
    }

    /**
     * Multiply a duration by a scalar.
     *
     * @param duration the duration to multiply
     * @param scalar the scalar to multiply by
     * @return {@code null} if either input is {@code null}; otherwise the duration multiplied by the scalar
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    public static Duration multiply(final Duration duration, final long scalar) {
        if (duration == null || scalar == NULL_LONG) {
            return null;
        }

        try {
            return duration.multipliedBy(scalar);
        } catch (DateTimeException | ArithmeticException ex) {
            throw new DateTimeOverflowException(ex);
        }
    }

    /**
     * Multiply a duration by a scalar.
     *
     * @param duration the duration to multiply
     * @param scalar the scalar to multiply by
     * @return {@code null} if either input is {@code null}; otherwise the duration multiplied by the scalar
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    public static Duration multiply(final long scalar, final Duration duration) {
        return multiply(duration, scalar);
    }

    /**
     * Multiply a period by a scalar.
     *
     * @param period the period to multiply
     * @param scalar the scalar to multiply by
     * @return {@code null} if either input is {@code null}; otherwise the period multiplied by the scalar
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    public static Period multiply(final Period period, final int scalar) {
        if (period == null || scalar == NULL_INT) {
            return null;
        }

        try {
            return period.multipliedBy(scalar);
        } catch (DateTimeException | ArithmeticException ex) {
            throw new DateTimeOverflowException(ex);
        }
    }

    /**
     * Multiply a period by a scalar.
     *
     * @param period the period to multiply
     * @param scalar the scalar to multiply by
     * @return {@code null} if either input is {@code null}; otherwise the period multiplied by the scalar
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    public static Period multiply(final int scalar, final Period period) {
        return multiply(period, scalar);
    }

    /**
     * Divide a duration by a scalar.
     *
     * @param duration the duration to divide
     * @param scalar the scalar to divide by
     * @return {@code null} if either input is {@code null}; otherwise the duration divide by the scalar
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    public static Duration divide(final Duration duration, final long scalar) {
        if (duration == null || scalar == NULL_LONG) {
            return null;
        }

        try {
            return duration.dividedBy(scalar);
        } catch (DateTimeException | ArithmeticException ex) {
            throw new DateTimeOverflowException(ex);
        }
    }

    /**
     * Returns the difference in nanoseconds between two instant values.
     *
     * @param start start time
     * @param end end time
     * @return {@link QueryConstants#NULL_LONG} if either input is {@code null}; otherwise the difference in start and
     *         end in nanoseconds
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    @ScriptApi
    public static long diffNanos(@Nullable final Instant start, @Nullable final Instant end) {
        return minus(end, start);
    }

    /**
     * Returns the difference in nanoseconds between two zoned date time values.
     *
     * @param start start time
     * @param end end time
     * @return {@link QueryConstants#NULL_LONG} if either input is {@code null}; otherwise the difference in start and
     *         end in nanoseconds
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    @ScriptApi
    public static long diffNanos(@Nullable final ZonedDateTime start, @Nullable final ZonedDateTime end) {
        return minus(end, start);
    }

    /**
     * Returns the difference in microseconds between two instant values.
     *
     * @param start start time
     * @param end end time
     * @return {@link QueryConstants#NULL_LONG} if either input is {@code null}; otherwise the difference in start and
     *         end in microseconds
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    @ScriptApi
    public static long diffMicros(@Nullable final Instant start, @Nullable final Instant end) {
        if (start == null || end == null) {
            return io.deephaven.util.QueryConstants.NULL_LONG;
        }

        return nanosToMicros(diffNanos(start, end));
    }

    /**
     * Returns the difference in microseconds between two zoned date time values.
     *
     * @param start start time
     * @param end end time
     * @return {@link QueryConstants#NULL_LONG} if either input is {@code null}; otherwise the difference in start and
     *         end in microseconds
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    @ScriptApi
    public static long diffMicros(@Nullable final ZonedDateTime start, @Nullable final ZonedDateTime end) {
        if (start == null || end == null) {
            return io.deephaven.util.QueryConstants.NULL_LONG;
        }

        return nanosToMicros(diffNanos(start, end));
    }

    /**
     * Returns the difference in milliseconds between two instant values.
     *
     * @param start start time
     * @param end end time
     * @return {@link QueryConstants#NULL_LONG} if either input is {@code null}; otherwise the difference in start and
     *         end in milliseconds
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    @ScriptApi
    public static long diffMillis(@Nullable final Instant start, @Nullable final Instant end) {
        if (start == null || end == null) {
            return io.deephaven.util.QueryConstants.NULL_LONG;
        }

        return nanosToMillis(diffNanos(start, end));
    }

    /**
     * Returns the difference in milliseconds between two zoned date time values.
     *
     * @param start start time
     * @param end end time
     * @return {@link QueryConstants#NULL_LONG} if either input is {@code null}; otherwise the difference in start and
     *         end in milliseconds
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    @ScriptApi
    public static long diffMillis(@Nullable final ZonedDateTime start, @Nullable final ZonedDateTime end) {
        if (start == null || end == null) {
            return io.deephaven.util.QueryConstants.NULL_LONG;
        }

        return nanosToMillis(diffNanos(start, end));
    }

    /**
     * Returns the difference in seconds between two instant values.
     *
     * @param start start time
     * @param end end time
     * @return {@link QueryConstants#NULL_DOUBLE} if either input is {@code null}; otherwise the difference in start and
     *         end in seconds
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    @ScriptApi
    public static double diffSeconds(@Nullable final Instant start, @Nullable final Instant end) {
        if (start == null || end == null) {
            return io.deephaven.util.QueryConstants.NULL_DOUBLE;
        }

        return (double) diffNanos(start, end) / SECOND;
    }

    /**
     * Returns the difference in seconds between two zoned date time values.
     *
     * @param start start time
     * @param end end time
     * @return {@link QueryConstants#NULL_DOUBLE} if either input is {@code null}; otherwise the difference in start and
     *         end in seconds
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    @ScriptApi
    public static double diffSeconds(@Nullable final ZonedDateTime start, @Nullable final ZonedDateTime end) {
        if (start == null || end == null) {
            return io.deephaven.util.QueryConstants.NULL_DOUBLE;
        }

        return (double) diffNanos(start, end) / SECOND;
    }

    /**
     * Returns the difference in minutes between two instant values.
     *
     * @param start start time
     * @param end end time
     * @return {@link QueryConstants#NULL_DOUBLE} if either input is {@code null}; otherwise the difference in start and
     *         end in minutes
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    @ScriptApi
    public static double diffMinutes(@Nullable final Instant start, @Nullable final Instant end) {
        if (start == null || end == null) {
            return io.deephaven.util.QueryConstants.NULL_DOUBLE;
        }

        return (double) diffNanos(start, end) / MINUTE;
    }

    /**
     * Returns the difference in minutes between two zoned date time values.
     *
     * @param start start time
     * @param end end time
     * @return {@link QueryConstants#NULL_DOUBLE} if either input is {@code null}; otherwise the difference in start and
     *         end in minutes
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    @ScriptApi
    public static double diffMinutes(@Nullable final ZonedDateTime start, @Nullable final ZonedDateTime end) {
        if (start == null || end == null) {
            return io.deephaven.util.QueryConstants.NULL_DOUBLE;
        }

        return (double) diffNanos(start, end) / MINUTE;
    }

    /**
     * Returns the difference in days between two instant values.
     *
     * @param start start time
     * @param end end time
     * @return {@link QueryConstants#NULL_DOUBLE} if either input is {@code null}; otherwise the difference in start and
     *         end in days
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    @ScriptApi
    public static double diffDays(@Nullable final Instant start, @Nullable final Instant end) {
        if (start == null || end == null) {
            return io.deephaven.util.QueryConstants.NULL_DOUBLE;
        }

        return (double) diffNanos(start, end) / DAY;
    }

    /**
     * Returns the difference in days between two zoned date time values.
     *
     * @param start start time
     * @param end end time
     * @return {@link QueryConstants#NULL_DOUBLE} if either input is {@code null}; otherwise the difference in start and
     *         end in days
     * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows
     */
    @ScriptApi
    public static double diffDays(@Nullable final ZonedDateTime start, @Nullable final ZonedDateTime end) {
        if (start == null || end == null) {
            return io.deephaven.util.QueryConstants.NULL_DOUBLE;
        }

        return (double) diffNanos(start, end) / DAY;
    }

    /**
     * Returns the difference in years between two instant values.
     * 

* Years are defined in terms of 365 day years. * * @param start start time * @param end end time * @return {@link QueryConstants#NULL_DOUBLE} if either input is {@code null}; otherwise the difference in start and * end in years * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows */ @ScriptApi public static double diffYears365(@Nullable final Instant start, @Nullable final Instant end) { if (start == null || end == null) { return io.deephaven.util.QueryConstants.NULL_DOUBLE; } return (double) diffNanos(start, end) * YEARS_PER_NANO_365; } /** * Returns the difference in years between two zoned date time values. *

* Years are defined in terms of 365 day years. * * @param start start time * @param end end time * @return {@link QueryConstants#NULL_DOUBLE} if either input is {@code null}; otherwise the difference in start and * end in years * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows */ @ScriptApi public static double diffYears365(@Nullable final ZonedDateTime start, @Nullable final ZonedDateTime end) { if (start == null || end == null) { return io.deephaven.util.QueryConstants.NULL_DOUBLE; } return (double) diffNanos(start, end) * YEARS_PER_NANO_365; } /** * Returns the difference in years between two instant values. *

* Years are defined in terms of 365.2425 day years. * * @param start start time * @param end end time * @return {@link QueryConstants#NULL_DOUBLE} if either input is {@code null}; otherwise the difference in start and * end in years * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows */ @ScriptApi public static double diffYearsAvg(@Nullable final Instant start, @Nullable final Instant end) { if (start == null || end == null) { return io.deephaven.util.QueryConstants.NULL_DOUBLE; } return (double) diffNanos(start, end) * YEARS_PER_NANO_AVG; } /** * Returns the difference in years between two zoned date time values. *

* Years are defined in terms of 365.2425 day years. * * @param start start time * @param end end time * @return {@link QueryConstants#NULL_DOUBLE} if either input is {@code null}; otherwise the difference in start and * end in years * @throws DateTimeOverflowException if the datetime arithmetic overflows or underflows */ @ScriptApi public static double diffYearsAvg(@Nullable final ZonedDateTime start, @Nullable final ZonedDateTime end) { if (start == null || end == null) { return io.deephaven.util.QueryConstants.NULL_DOUBLE; } return (double) diffNanos(start, end) * YEARS_PER_NANO_AVG; } // endregion // region Comparisons /** * Evaluates whether one instant value is before a second instant value. * * @param instant1 first instant * @param instant2 second instant * @return {@code true} if instant1 is before instant2; otherwise, {@code false} if either value is {@code null} or * if instant2 is equal to or before instant1 */ @ScriptApi public static boolean isBefore(@Nullable final Instant instant1, @Nullable final Instant instant2) { if (instant1 == null || instant2 == null) { return false; } return instant1.isBefore(instant2); } /** * Evaluates whether one zoned date time value is before a second zoned date time value. * * @param dateTime1 first zoned date time * @param dateTime2 second zoned date time * @return {@code true} if dateTime1 is before dateTime2; otherwise, {@code false} if either value is {@code null} * or if dateTime2 is equal to or before dateTime1 */ @ScriptApi public static boolean isBefore(@Nullable final ZonedDateTime dateTime1, @Nullable final ZonedDateTime dateTime2) { if (dateTime1 == null || dateTime2 == null) { return false; } return dateTime1.isBefore(dateTime2); } /** * Evaluates whether one instant value is before or equal to a second instant value. * * @param instant1 first instant * @param instant2 second instant * @return {@code true} if instant1 is before or equal to instant2; otherwise, {@code false} if either value is * {@code null} or if instant2 is before instant1 */ @ScriptApi public static boolean isBeforeOrEqual(@Nullable final Instant instant1, @Nullable final Instant instant2) { if (instant1 == null || instant2 == null) { return false; } return instant1.isBefore(instant2) || instant1.equals(instant2); } /** * Evaluates whether one zoned date time value is before or equal to a second zoned date time value. * * @param dateTime1 first zoned date time * @param dateTime2 second zoned date time * @return {@code true} if dateTime1 is before or equal to dateTime2; otherwise, {@code false} if either value is * {@code null} or if dateTime2 is before dateTime1 */ @ScriptApi public static boolean isBeforeOrEqual( @Nullable final ZonedDateTime dateTime1, @Nullable final ZonedDateTime dateTime2) { if (dateTime1 == null || dateTime2 == null) { return false; } return dateTime1.isBefore(dateTime2) || dateTime1.equals(dateTime2); } /** * Evaluates whether one instant value is after a second instant value. * * @param instant1 first instant * @param instant2 second instant * @return {@code true} if instant1 is after instant2; otherwise, {@code false} if either value is {@code null} or * if instant2 is equal to or after instant1 */ @ScriptApi public static boolean isAfter(@Nullable final Instant instant1, @Nullable final Instant instant2) { if (instant1 == null || instant2 == null) { return false; } return instant1.isAfter(instant2); } /** * Evaluates whether one zoned date time value is after a second zoned date time value. * * @param dateTime1 first zoned date time * @param dateTime2 second zoned date time * @return {@code true} if dateTime1 is after dateTime2; otherwise, {@code false} if either value is {@code null} or * if dateTime2 is equal to or after dateTime1 */ @ScriptApi public static boolean isAfter(@Nullable final ZonedDateTime dateTime1, @Nullable final ZonedDateTime dateTime2) { if (dateTime1 == null || dateTime2 == null) { return false; } return dateTime1.isAfter(dateTime2); } /** * Evaluates whether one instant value is after or equal to a second instant value. * * @param instant1 first instant * @param instant2 second instant * @return {@code true} if instant1 is after or equal to instant2; otherwise, {@code false} if either value is * {@code null} or if instant2 is after instant1 */ @ScriptApi public static boolean isAfterOrEqual(@Nullable final Instant instant1, @Nullable final Instant instant2) { if (instant1 == null || instant2 == null) { return false; } return instant1.isAfter(instant2) || instant1.equals(instant2); } /** * Evaluates whether one zoned date time value is after or equal to a second zoned date time value. * * @param dateTime1 first zoned date time * @param dateTime2 second zoned date time * @return {@code true} if dateTime1 is after or equal to dateTime2; otherwise, {@code false} if either value is * {@code null} or if dateTime2 is after dateTime1 */ @ScriptApi public static boolean isAfterOrEqual(@Nullable final ZonedDateTime dateTime1, @Nullable ZonedDateTime dateTime2) { if (dateTime1 == null || dateTime2 == null) { return false; } return dateTime1.isAfter(dateTime2) || dateTime1.equals(dateTime2); } // endregion // region Chronology /** * Returns the number of nanoseconds that have elapsed since the top of the millisecond. * * @param instant time * @return {@link QueryConstants#NULL_INT} if the input is {@code null}; otherwise, number of nanoseconds that have * elapsed since the top of the millisecond */ @ScriptApi public static int nanosOfMilli(@Nullable final Instant instant) { if (instant == null) { return NULL_INT; } return (int) (epochNanos(instant) % 1000000); } /** * Returns the number of nanoseconds that have elapsed since the top of the millisecond. * * @param dateTime time * @return {@link QueryConstants#NULL_INT} if the input is {@code null}; otherwise, number of nanoseconds that have * elapsed since the top of the millisecond */ @ScriptApi public static int nanosOfMilli(@Nullable final ZonedDateTime dateTime) { if (dateTime == null) { return NULL_INT; } return nanosOfMilli(toInstant(dateTime)); } /** * Returns the number of microseconds that have elapsed since the top of the millisecond. Nanoseconds are rounded, * not dropped -- '20:41:39.123456700' has 457 micros, not 456. * * @param instant time * @return {@link QueryConstants#NULL_INT} if the input is {@code null}; otherwise, number of microseconds that have * elapsed since the top of the millisecond */ @ScriptApi public static int microsOfMilli(@Nullable final Instant instant) { if (instant == null) { return NULL_INT; } return (int) Math.round((epochNanos(instant) % 1000000) / 1000d); } /** * Returns the number of microseconds that have elapsed since the top of the millisecond. Nanoseconds are rounded, * not dropped -- '20:41:39.123456700' has 457 micros, not 456. * * @param dateTime time * @return {@link QueryConstants#NULL_INT} if the input is {@code null}; otherwise, number of microseconds that have * elapsed since the top of the millisecond */ @ScriptApi public static int microsOfMilli(@Nullable final ZonedDateTime dateTime) { if (dateTime == null) { return NULL_INT; } return microsOfMilli(toInstant(dateTime)); } /** * Returns the number of nanoseconds that have elapsed since the top of the second. * * @param instant time * @param timeZone time zone * @return {@link QueryConstants#NULL_LONG} if either input is {@code null}; otherwise, number of nanoseconds that * have elapsed since the top of the second */ @ScriptApi public static long nanosOfSecond(@Nullable final Instant instant, @Nullable final ZoneId timeZone) { if (instant == null || timeZone == null) { return NULL_LONG; } return nanosOfSecond(toZonedDateTime(instant, timeZone)); } /** * Returns the number of nanoseconds that have elapsed since the top of the second. * * @param dateTime time * @return {@link QueryConstants#NULL_LONG} if either input is {@code null}; otherwise, number of nanoseconds that * have elapsed since the top of the second */ @ScriptApi public static long nanosOfSecond(@Nullable final ZonedDateTime dateTime) { if (dateTime == null) { return NULL_LONG; } return dateTime.getNano(); } /** * Returns the number of microseconds that have elapsed since the top of the second. * * @param instant time * @param timeZone time zone * @return {@link QueryConstants#NULL_LONG} if either input is {@code null}; otherwise, number of microseconds that * have elapsed since the top of the second */ @ScriptApi public static long microsOfSecond(@Nullable final Instant instant, @Nullable final ZoneId timeZone) { if (instant == null || timeZone == null) { return NULL_LONG; } return nanosToMicros(nanosOfSecond(instant, timeZone)); } /** * Returns the number of microseconds that have elapsed since the top of the second. * * @param dateTime time * @return {@link QueryConstants#NULL_LONG} if either input is {@code null}; otherwise, number of microseconds that * have elapsed since the top of the second */ @ScriptApi public static long microsOfSecond(@Nullable final ZonedDateTime dateTime) { if (dateTime == null) { return NULL_LONG; } return nanosToMicros(nanosOfSecond(dateTime)); } /** * Returns the number of milliseconds that have elapsed since the top of the second. * * @param instant time * @param timeZone time zone * @return {@link QueryConstants#NULL_INT} if either input is {@code null}; otherwise, number of milliseconds that * have elapsed since the top of the second */ @ScriptApi public static int millisOfSecond(@Nullable final Instant instant, @Nullable final ZoneId timeZone) { if (instant == null || timeZone == null) { return NULL_INT; } return (int) nanosToMillis(nanosOfSecond(instant, timeZone)); } /** * Returns the number of milliseconds that have elapsed since the top of the second. * * @param dateTime time * @return {@link QueryConstants#NULL_INT} if either input is {@code null}; otherwise, number of milliseconds that * have elapsed since the top of the second */ @ScriptApi public static int millisOfSecond(@Nullable final ZonedDateTime dateTime) { if (dateTime == null) { return NULL_INT; } return (int) nanosToMillis(nanosOfSecond(dateTime)); } /** * Returns the number of seconds that have elapsed since the top of the minute. * * @param instant time * @param timeZone time zone * @return {@link QueryConstants#NULL_INT} if either input is {@code null}; otherwise, number of seconds that have * elapsed since the top of the minute */ @ScriptApi public static int secondOfMinute(@Nullable final Instant instant, @Nullable final ZoneId timeZone) { if (instant == null || timeZone == null) { return NULL_INT; } return toZonedDateTime(instant, timeZone).getSecond(); } /** * Returns the number of seconds that have elapsed since the top of the minute. * * @param dateTime time * @return {@link QueryConstants#NULL_INT} if either input is {@code null}; otherwise, number of seconds that have * elapsed since the top of the minute */ @ScriptApi public static int secondOfMinute(@Nullable final ZonedDateTime dateTime) { if (dateTime == null) { return NULL_INT; } return dateTime.getSecond(); } /** * Returns the number of minutes that have elapsed since the top of the hour. * * @param instant time * @param timeZone time zone * @return {@link QueryConstants#NULL_INT} if either input is {@code null}; otherwise, number of minutes that have * elapsed since the top of the hour */ @ScriptApi public static int minuteOfHour(@Nullable final Instant instant, @Nullable final ZoneId timeZone) { if (instant == null || timeZone == null) { return NULL_INT; } return toZonedDateTime(instant, timeZone).getMinute(); } /** * Returns the number of minutes that have elapsed since the top of the hour. * * @param dateTime time * @return {@link QueryConstants#NULL_INT} if either input is {@code null}; otherwise, number of minutes that have * elapsed since the top of the hour */ @ScriptApi public static int minuteOfHour(@Nullable final ZonedDateTime dateTime) { if (dateTime == null) { return NULL_INT; } return dateTime.getMinute(); } /** * Returns the number of nanoseconds that have elapsed since the start of the day. * * @param instant time * @param timeZone time zone * @param asLocalTime If {@code true}, returns the number of nanos from the start of the day according to the local * time. In this case, 9:30AM always returns the same value. If {@code false}, returns the number of nanos * from the start of the day. On days when daylight savings time events occur, results may be different from * what is expected based upon the local time. For example, on daylight savings time change days, 9:30AM may * be earlier or later in the day based upon if the daylight savings time adjustment is forwards or * backwards. On non DST days, the result is the same regardless of the value of {@code asLocalTime}. * @return {@link QueryConstants#NULL_LONG} if either input is {@code null}; otherwise, number of nanoseconds that * have elapsed since the start of the day */ @ScriptApi public static long nanosOfDay(@Nullable final Instant instant, @Nullable final ZoneId timeZone, final boolean asLocalTime) { if (instant == null || timeZone == null) { return NULL_LONG; } return nanosOfDay(toZonedDateTime(instant, timeZone), asLocalTime); } /** * Returns the number of nanoseconds that have elapsed since the start of the day. * * @param dateTime time * @param asLocalTime If {@code true}, returns the number of nanos from the start of the day according to the local * time. In this case, 9:30AM always returns the same value. If {@code false}, returns the number of nanos * from the start of the day. On days when daylight savings time events occur, results may be different from * what is expected based upon the local time. For example, on daylight savings time change days, 9:30AM may * be earlier or later in the day based upon if the daylight savings time adjustment is forwards or * backwards. On non DST days, the result is the same regardless of the value of {@code asLocalTime}. * @return {@link QueryConstants#NULL_LONG} if either input is {@code null}; otherwise, number of nanoseconds that * have elapsed since the start of the day */ @ScriptApi public static long nanosOfDay(@Nullable final ZonedDateTime dateTime, final boolean asLocalTime) { if (dateTime == null) { return NULL_LONG; } if (asLocalTime) { return dateTime.toLocalTime().toNanoOfDay(); } else { return epochNanos(dateTime) - epochNanos(atMidnight(dateTime)); } } /** * Returns the number of nanoseconds that have elapsed since the start of the day. * * @param localDateTime local date time * @return {@link QueryConstants#NULL_LONG} if input is {@code null}; otherwise, number of nanoseconds that have * elapsed since the start of the day */ @ScriptApi public static long nanosOfDay(@Nullable final LocalDateTime localDateTime) { if (localDateTime == null) { return NULL_LONG; } return localDateTime.toLocalTime().toNanoOfDay(); } /** * Returns the number of nanoseconds that have elapsed since the start of the day. * * @param localTime local time * @return {@link QueryConstants#NULL_LONG} if input is {@code null}; otherwise, number of nanoseconds that have * elapsed since the start of the day */ @ScriptApi public static long nanosOfDay(@Nullable final LocalTime localTime) { if (localTime == null) { return NULL_LONG; } return localTime.toNanoOfDay(); } /** * Returns the number of milliseconds that have elapsed since the start of the day. * * @param instant time * @param timeZone time zone * @param asLocalTime If {@code true}, returns the number of milliseconds from the start of the day according to the * local time. In this case, 9:30AM always returns the same value. If {@code false}, returns the number of * milliseconds from the start of the day. On days when daylight savings time events occur, results may be * different from what is expected based upon the local time. For example, on daylight savings time change * days, 9:30AM may be earlier or later in the day based upon if the daylight savings time adjustment is * forwards or backwards. On non DST days, the result is the same regardless of the value of * {@code asLocalTime}. * @return {@link QueryConstants#NULL_INT} if either input is {@code null}; otherwise, number of milliseconds that * have elapsed since the start of the day */ @ScriptApi public static int millisOfDay(@Nullable final Instant instant, @Nullable final ZoneId timeZone, final boolean asLocalTime) { if (instant == null || timeZone == null) { return NULL_INT; } return (int) nanosToMillis(nanosOfDay(instant, timeZone, asLocalTime)); } /** * Returns the number of milliseconds that have elapsed since the start of the day. * * @param dateTime time * @param asLocalTime If {@code true}, returns the number of milliseconds from the start of the day according to the * local time. In this case, 9:30AM always returns the same value. If {@code false}, returns the number of * milliseconds from the start of the day. On days when daylight savings time events occur, results may be * different from what is expected based upon the local time. For example, on daylight savings time change * days, 9:30AM may be earlier or later in the day based upon if the daylight savings time adjustment is * forwards or backwards. On non DST days, the result is the same regardless of the value of * {@code asLocalTime}. * @return {@link QueryConstants#NULL_INT} if either input is {@code null}; otherwise, number of milliseconds that * have elapsed since the start of the day */ @ScriptApi public static int millisOfDay(@Nullable final ZonedDateTime dateTime, final boolean asLocalTime) { if (dateTime == null) { return NULL_INT; } return (int) nanosToMillis(nanosOfDay(dateTime, asLocalTime)); } /** * Returns the number of milliseconds that have elapsed since the start of the day. * * @param localDateTime local date time * @return {@link QueryConstants#NULL_INT} if input is {@code null}; otherwise, number of milliseconds that have * elapsed since the start of the day */ @ScriptApi public static int millisOfDay(@Nullable final LocalDateTime localDateTime) { if (localDateTime == null) { return NULL_INT; } return (int) nanosToMillis(localDateTime.toLocalTime().toNanoOfDay()); } /** * Returns the number of milliseconds that have elapsed since the start of the day. * * @param localTime local time * @return {@link QueryConstants#NULL_INT} if input is {@code null}; otherwise, number of milliseconds that have * elapsed since the start of the day */ @ScriptApi public static int millisOfDay(@Nullable final LocalTime localTime) { if (localTime == null) { return NULL_INT; } return (int) nanosToMillis(localTime.toNanoOfDay()); } /** * Returns the number of seconds that have elapsed since the start of the day. * * @param instant time * @param timeZone time zone * @param asLocalTime If {@code true}, returns the number of nanos from the start of the day according to the local * time. In this case, 9:30AM always returns the same value. If {@code false}, returns the number of nanos * from the start of the day. On days when daylight savings time events occur, results may be different from * what is expected based upon the local time. For example, on daylight savings time change days, 9:30AM may * be earlier or later in the day based upon if the daylight savings time adjustment is forwards or * backwards. On non DST days, the result is the same regardless of the value of {@code asLocalTime}. * @return {@link QueryConstants#NULL_INT} if either input is {@code null}; otherwise, number of seconds that have * elapsed since the start of the day */ @ScriptApi public static int secondOfDay(@Nullable final Instant instant, @Nullable final ZoneId timeZone, final boolean asLocalTime) { if (instant == null || timeZone == null) { return NULL_INT; } return (int) nanosToSeconds(nanosOfDay(instant, timeZone, asLocalTime)); } /** * Returns the number of seconds that have elapsed since the start of the day. * * @param dateTime time * @param asLocalTime If {@code true}, returns the number of seconds from the start of the day according to the * local time. In this case, 9:30AM always returns the same value. If {@code false}, returns the number of * seconds from the start of the day. On days when daylight savings time events occur, results may be * different from what is expected based upon the local time. For example, on daylight savings time change * days, 9:30AM may be earlier or later in the day based upon if the daylight savings time adjustment is * forwards or backwards. On non DST days, the result is the same regardless of the value of * {@code asLocalTime}. * @return {@link QueryConstants#NULL_INT} if either input is {@code null}; otherwise, number of seconds that have * elapsed since the start of the day */ @ScriptApi public static int secondOfDay(@Nullable final ZonedDateTime dateTime, final boolean asLocalTime) { if (dateTime == null) { return NULL_INT; } return (int) nanosToSeconds(nanosOfDay(dateTime, asLocalTime)); } /** * Returns the number of seconds that have elapsed since the start of the day. * * @param localDateTime local date time * @return {@link QueryConstants#NULL_INT} if input is {@code null}; otherwise, number of seconds that have elapsed * since the start of the day */ @ScriptApi public static int secondOfDay(@Nullable final LocalDateTime localDateTime) { if (localDateTime == null) { return NULL_INT; } return localDateTime.toLocalTime().toSecondOfDay(); } /** * Returns the number of seconds that have elapsed since the start of the day. * * @param localTime local time * @return {@link QueryConstants#NULL_INT} if input is {@code null}; otherwise, number of seconds that have elapsed * since the start of the day */ @ScriptApi public static int secondOfDay(@Nullable final LocalTime localTime) { if (localTime == null) { return NULL_INT; } return localTime.toSecondOfDay(); } /** * Returns the number of minutes that have elapsed since the start of the day. * * @param instant time * @param timeZone time zone * @param asLocalTime If {@code true}, returns the number of minutes from the start of the day according to the * local time. In this case, 9:30AM always returns the same value. If {@code false}, returns the number of * minutes from the start of the day. On days when daylight savings time events occur, results may be * different from what is expected based upon the local time. For example, on daylight savings time change * days, 9:30AM may be earlier or later in the day based upon if the daylight savings time adjustment is * forwards or backwards. On non DST days, the result is the same as if asLocalTime is false. * @return {@link QueryConstants#NULL_INT} if either input is {@code null}; otherwise, number of minutes that have * elapsed since the start of the day */ @ScriptApi public static int minuteOfDay(@Nullable final Instant instant, @Nullable final ZoneId timeZone, final boolean asLocalTime) { if (instant == null || timeZone == null) { return NULL_INT; } return secondOfDay(instant, timeZone, asLocalTime) / 60; } /** * Returns the number of minutes that have elapsed since the start of the day. * * @param dateTime time * @param asLocalTime If {@code true}, returns the number of minutes from the start of the day according to the * local time. In this case, 9:30AM always returns the same value. If {@code false}, returns the number of * minutes from the start of the day. On days when daylight savings time events occur, results may be * different from what is expected based upon the local time. For example, on daylight savings time change * days, 9:30AM may be earlier or later in the day based upon if the daylight savings time adjustment is * forwards or backwards. On non DST days, the result is the same regardless of the value of * {@code asLocalTime}. * @return {@link QueryConstants#NULL_INT} if either input is {@code null}; otherwise, number of minutes that have * elapsed since the start of the day */ @ScriptApi public static int minuteOfDay(@Nullable final ZonedDateTime dateTime, final boolean asLocalTime) { if (dateTime == null) { return NULL_INT; } return secondOfDay(dateTime, asLocalTime) / 60; } /** * Returns the number of minutes that have elapsed since the start of the day. * * @param localDateTime local date time * @return {@link QueryConstants#NULL_INT} if input is {@code null}; otherwise, number of minutes that have elapsed * since the start of the day */ @ScriptApi public static int minuteOfDay(@Nullable final LocalDateTime localDateTime) { if (localDateTime == null) { return NULL_INT; } return secondOfDay(localDateTime) / 60; } /** * Returns the number of minutes that have elapsed since the start of the day. * * @param localTime local time * @return {@link QueryConstants#NULL_INT} if input is {@code null}; otherwise, number of minutes that have elapsed * since the start of the day */ @ScriptApi public static int minuteOfDay(@Nullable final LocalTime localTime) { if (localTime == null) { return NULL_INT; } return secondOfDay(localTime) / 60; } /** * Returns the number of hours that have elapsed since the start of the day. * * @param instant time * @param timeZone time zone * @param asLocalTime If {@code true}, returns the number of hours from the start of the day according to the local * time. In this case, 9:30AM always returns the same value. If {@code false}, returns the number of hours * from the start of the day. On days when daylight savings time events occur, results may be different from * what is expected based upon the local time. For example, on daylight savings time change days, 9:30AM may * be earlier or later in the day based upon if the daylight savings time adjustment is forwards or * backwards. On non DST days, the result is the same regardless of the value of {@code asLocalTime}. * @return {@link QueryConstants#NULL_INT} if either input is {@code null}; otherwise, number of hours that have * elapsed since the start of the day */ @ScriptApi public static int hourOfDay(@Nullable final Instant instant, @Nullable final ZoneId timeZone, final boolean asLocalTime) { if (instant == null || timeZone == null) { return NULL_INT; } return hourOfDay(toZonedDateTime(instant, timeZone), asLocalTime); } /** * Returns the number of hours that have elapsed since the start of the day. * * @param dateTime time * @param asLocalTime If {@code true}, returns the number of hours from the start of the day according to the local * time. In this case, 9:30AM always returns the same value. If {@code false}, returns the number of hours * from the start of the day. On days when daylight savings time events occur, results may be different from * what is expected based upon the local time. For example, on daylight savings time change days, 9:30AM may * be earlier or later in the day based upon if the daylight savings time adjustment is forwards or * backwards. On non DST days, the result is the same regardless of the value of {@code asLocalTime}. * @return {@link QueryConstants#NULL_INT} if either input is {@code null}; otherwise, number of hours that have * elapsed since the start of the day */ @ScriptApi public static int hourOfDay(@Nullable final ZonedDateTime dateTime, final boolean asLocalTime) { if (dateTime == null) { return NULL_INT; } return minuteOfDay(dateTime, asLocalTime) / 60; } /** * Returns the number of hours that have elapsed since the start of the day. * * @param localDateTime local date time * @return {@link QueryConstants#NULL_INT} if input is {@code null}; otherwise, number of hours that have elapsed * since the start of the day */ @ScriptApi public static int hourOfDay(@Nullable final LocalDateTime localDateTime) { if (localDateTime == null) { return NULL_INT; } return localDateTime.getHour(); } /** * Returns the number of hours that have elapsed since the start of the day. * * @param localTime local time * @return {@link QueryConstants#NULL_INT} if input is {@code null}; otherwise, number of hours that have elapsed * since the start of the day */ @ScriptApi public static int hourOfDay(@Nullable final LocalTime localTime) { if (localTime == null) { return NULL_INT; } return localTime.getHour(); } /** * Returns athe day of the week for a {@link ZonedDateTime}. * * @param localDateTime local date time to find the day of the week of * @return {@code null} if {@code localDateTime} is {@code null}; otherwise, the day of the week */ @ScriptApi public static DayOfWeek dayOfWeek(@Nullable final LocalDateTime localDateTime) { if (localDateTime == null) { return null; } return localDateTime.getDayOfWeek(); } /** * Returns the day of the week for a {@link LocalDate}. * * @param localDate local date to find the day of the week of * @return {@code null} if {@code localDate} is {@code null}; otherwise, the day of the week */ @ScriptApi public static DayOfWeek dayOfWeek(@Nullable final LocalDate localDate) { if (localDate == null) { return null; } return localDate.getDayOfWeek(); } /** * Returns the day of the week for an {@link Instant} in the specified time zone. * * @param instant time to find the day of the week of * @param timeZone time zone * @return {@code null} if either input is {@code null}; otherwise, the day of the week */ @ScriptApi public static DayOfWeek dayOfWeek(@Nullable final Instant instant, @Nullable final ZoneId timeZone) { if (instant == null || timeZone == null) { return null; } return dayOfWeek(toZonedDateTime(instant, timeZone)); } /** * Returns the day of the week for a {@link ZonedDateTime} in the specified time zone. * * @param dateTime time to find the day of the week of * @return {@code null} if either input is {@code null}; otherwise, the day of the week */ @ScriptApi public static DayOfWeek dayOfWeek(@Nullable final ZonedDateTime dateTime) { if (dateTime == null) { return null; } return dateTime.getDayOfWeek(); } /** * Returns a 1-based int value of the day of the week for a {@link LocalDateTime}, with 1 being Monday and 7 being * Sunday. * * @param localDateTime local date time to find the day of the week of * @return {@link QueryConstants#NULL_INT} if {@code localDateTime} is {@code null}; otherwise, the day of the week */ @ScriptApi public static int dayOfWeekValue(@Nullable final LocalDateTime localDateTime) { if (localDateTime == null) { return io.deephaven.util.QueryConstants.NULL_INT; } return localDateTime.getDayOfWeek().getValue(); } /** * Returns a 1-based int value of the day of the week for a {@link LocalDate}, with 1 being Monday and 7 being * Sunday. * * @param localDate local date to find the day of the week of * @return {@link QueryConstants#NULL_INT} if {@code localDate} is {@code null}; otherwise, the day of the week */ @ScriptApi public static int dayOfWeekValue(@Nullable final LocalDate localDate) { if (localDate == null) { return io.deephaven.util.QueryConstants.NULL_INT; } return localDate.getDayOfWeek().getValue(); } /** * Returns a 1-based int value of the day of the week for an {@link Instant} in the specified time zone, with 1 * being Monday and 7 being Sunday. * * @param instant time to find the day of the week of * @param timeZone time zone * @return {@link QueryConstants#NULL_INT} if either input is {@code null}; otherwise, the day of the week */ @ScriptApi public static int dayOfWeekValue(@Nullable final Instant instant, @Nullable final ZoneId timeZone) { if (instant == null || timeZone == null) { return NULL_INT; } return dayOfWeekValue(toZonedDateTime(instant, timeZone)); } /** * Returns a 1-based int value of the day of the week for a {@link ZonedDateTime} in the specified time zone, with 1 * being Monday and 7 being Sunday. * * @param dateTime time to find the day of the week of * @return {@link QueryConstants#NULL_INT} if either input is {@code null}; otherwise, the day of the week */ @ScriptApi public static int dayOfWeekValue(@Nullable final ZonedDateTime dateTime) { if (dateTime == null) { return NULL_INT; } return dateTime.getDayOfWeek().getValue(); } /** * Returns a 1-based int value of the day of the month for a {@link LocalDateTime}. The first day of the month * returns 1, the second day returns 2, etc. * * @param localDateTime local date time to find the day of the month of * @return A {@link QueryConstants#NULL_INT} if {@code localDateTime} is {@code null}; otherwise, the day of the * month */ @ScriptApi public static int dayOfMonth(@Nullable final LocalDateTime localDateTime) { if (localDateTime == null) { return io.deephaven.util.QueryConstants.NULL_INT; } return localDateTime.getDayOfMonth(); } /** * Returns a 1-based int value of the day of the month for a {@link LocalDate}. The first day of the month returns * 1, the second day returns 2, etc. * * @param localDate local date to find the day of the month of * @return A {@link QueryConstants#NULL_INT} if {@code localDate} is {@code null}; otherwise, the day of the month */ @ScriptApi public static int dayOfMonth(@Nullable final LocalDate localDate) { if (localDate == null) { return io.deephaven.util.QueryConstants.NULL_INT; } return localDate.getDayOfMonth(); } /** * Returns a 1-based int value of the day of the month for an {@link Instant} and specified time zone. The first day * of the month returns 1, the second day returns 2, etc. * * @param instant time to find the day of the month of * @param timeZone time zone * @return A {@link QueryConstants#NULL_INT} if either input is {@code null}; otherwise, the day of the month */ @ScriptApi public static int dayOfMonth(@Nullable final Instant instant, @Nullable final ZoneId timeZone) { if (instant == null || timeZone == null) { return NULL_INT; } return dayOfMonth(toZonedDateTime(instant, timeZone)); } /** * Returns a 1-based int value of the day of the month for a {@link ZonedDateTime} and specified time zone. The * first day of the month returns 1, the second day returns 2, etc. * * @param dateTime time to find the day of the month of * @return A {@link QueryConstants#NULL_INT} if either input is {@code null}; otherwise, the day of the month */ @ScriptApi public static int dayOfMonth(@Nullable final ZonedDateTime dateTime) { if (dateTime == null) { return NULL_INT; } return dateTime.getDayOfMonth(); } /** * Returns a 1-based int value of the day of the year (Julian date) for a {@link LocalDateTime}. The first day of * the year returns 1, the second day returns 2, etc. * * @param localDateTime local date time to find the day of the year of * @return {@link QueryConstants#NULL_INT} if {@code localDateTime} is {@code null}; otherwise, the day of the year */ @ScriptApi public static int dayOfYear(@Nullable final LocalDateTime localDateTime) { if (localDateTime == null) { return io.deephaven.util.QueryConstants.NULL_INT; } return localDateTime.getDayOfYear(); } /** * Returns a 1-based int value of the day of the year (Julian date) for a {@link LocalDate}. The first day of the * year returns 1, the second day returns 2, etc. * * @param localDate local date to find the day of the year of * @return {@link QueryConstants#NULL_INT} if {@code localDate} is {@code null}; otherwise, the day of the year */ @ScriptApi public static int dayOfYear(@Nullable final LocalDate localDate) { if (localDate == null) { return io.deephaven.util.QueryConstants.NULL_INT; } return localDate.getDayOfYear(); } /** * Returns a 1-based int value of the day of the year (Julian date) for an {@link Instant} in the specified time * zone. The first day of the year returns 1, the second day returns 2, etc. * * @param instant time to find the day of the year of * @param timeZone time zone * @return {@link QueryConstants#NULL_INT} if either input is {@code null}; otherwise, the day of the year */ @ScriptApi public static int dayOfYear(@Nullable final Instant instant, @Nullable final ZoneId timeZone) { if (instant == null || timeZone == null) { return NULL_INT; } return dayOfYear(toZonedDateTime(instant, timeZone)); } /** * Returns a 1-based int value of the day of the year (Julian date) for a {@link ZonedDateTime} in the specified * time zone. The first day of the year returns 1, the second day returns 2, etc. * * @param dateTime time to find the day of the year of * @return {@link QueryConstants#NULL_INT} if either input is {@code null}; otherwise, the day of the year */ @ScriptApi public static int dayOfYear(@Nullable final ZonedDateTime dateTime) { if (dateTime == null) { return NULL_INT; } return dateTime.getDayOfYear(); } /** * Returns a 1-based int value of the month of the year (Julian date) for a {@link LocalDateTime}. January is 1, * February is 2, etc. * * @param localDateTime local date time to find the month of the year of * @return {@link QueryConstants#NULL_INT} if {@code localDateTime} is {@code null}; otherwise, the month of the * year */ @ScriptApi public static int monthOfYear(@Nullable final LocalDateTime localDateTime) { if (localDateTime == null) { return io.deephaven.util.QueryConstants.NULL_INT; } return localDateTime.getMonthValue(); } /** * Returns a 1-based int value of the month of the year (Julian date) for a {@link LocalDate}. January is 1, * February is 2, etc. * * @param localDate local date to find the month of the year of * @return {@link QueryConstants#NULL_INT} if {@code localDate} is {@code null}; otherwise, the month of the year */ @ScriptApi public static int monthOfYear(@Nullable final LocalDate localDate) { if (localDate == null) { return io.deephaven.util.QueryConstants.NULL_INT; } return localDate.getMonthValue(); } /** * Returns a 1-based int value of the month of the year (Julian date) for an {@link Instant} in the specified time * zone. January is 1, February is 2, etc. * * @param instant time to find the month of the year of * @param timeZone time zone * @return {@link QueryConstants#NULL_INT} if either input is {@code null}; otherwise, the month of the year */ @ScriptApi public static int monthOfYear(@Nullable final Instant instant, @Nullable final ZoneId timeZone) { if (instant == null || timeZone == null) { return NULL_INT; } return monthOfYear(toZonedDateTime(instant, timeZone)); } /** * Returns a 1-based int value of the month of the year (Julian date) for a {@link ZonedDateTime} in the specified * time zone. January is 1, February is 2, etc. * * @param dateTime time to find the month of the year of * @return {@link QueryConstants#NULL_INT} if either input is {@code null}; otherwise, the month of the year */ @ScriptApi public static int monthOfYear(@Nullable final ZonedDateTime dateTime) { if (dateTime == null) { return NULL_INT; } return dateTime.getMonthValue(); } /** * Returns the year for a {@link LocalDateTime}. * * @param localDateTime local date to find the year of * @return {@link QueryConstants#NULL_INT} if {@code localDateTime} is {@code null}; otherwise, the year */ @ScriptApi public static int year(@Nullable final LocalDateTime localDateTime) { if (localDateTime == null) { return io.deephaven.util.QueryConstants.NULL_INT; } return localDateTime.getYear(); } /** * Returns the year for a {@link LocalDate}. * * @param localDate local date to find the year of * @return {@link QueryConstants#NULL_INT} if {@code localDate} is {@code null}; otherwise, the year */ @ScriptApi public static int year(@Nullable final LocalDate localDate) { if (localDate == null) { return io.deephaven.util.QueryConstants.NULL_INT; } return localDate.getYear(); } /** * Returns the year for an {@link Instant} in the specified time zone. * * @param instant time to find the year of * @param timeZone time zone * @return {@link QueryConstants#NULL_INT} if either input is {@code null}; otherwise, the year */ @ScriptApi public static int year(@Nullable final Instant instant, @Nullable final ZoneId timeZone) { if (instant == null || timeZone == null) { return NULL_INT; } return year(toZonedDateTime(instant, timeZone)); } /** * Returns the year for a {@link ZonedDateTime} in the specified time zone. * * @param dateTime time to find the year of * @return {@link QueryConstants#NULL_INT} if either input is {@code null}; otherwise, the year */ @ScriptApi public static int year(@Nullable final ZonedDateTime dateTime) { if (dateTime == null) { return NULL_INT; } return dateTime.getYear(); } /** * Returns the year of the century (two-digit year) for a {@link LocalDateTime}. * * @param localDateTime local date time to find the year of * @return {@link QueryConstants#NULL_INT} if {@code localDateTime} is {@code null}; otherwise, the year of the * century (two-digit year) */ @ScriptApi public static int yearOfCentury(@Nullable final LocalDateTime localDateTime) { if (localDateTime == null) { return io.deephaven.util.QueryConstants.NULL_INT; } return year(localDateTime) % 100; } /** * Returns the year of the century (two-digit year) for a {@link LocalDate}. * * @param localDate local date to find the year of * @return {@link QueryConstants#NULL_INT} if {@code localDate} is {@code null}; otherwise, the year of the century * (two-digit year) */ @ScriptApi public static int yearOfCentury(@Nullable final LocalDate localDate) { if (localDate == null) { return io.deephaven.util.QueryConstants.NULL_INT; } return year(localDate) % 100; } /** * Returns the year of the century (two-digit year) for an {@link Instant} in the specified time zone. * * @param instant time to find the year of * @param timeZone time zone * @return {@link QueryConstants#NULL_INT} if either input is {@code null}; otherwise, the year of the century * (two-digit year) */ @ScriptApi public static int yearOfCentury(@Nullable final Instant instant, @Nullable final ZoneId timeZone) { if (instant == null || timeZone == null) { return NULL_INT; } return year(instant, timeZone) % 100; } /** * Returns the year of the century (two-digit year) for a {@link ZonedDateTime} in the specified time zone. * * @param dateTime time to find the day of the month of * @return {@link QueryConstants#NULL_INT} if either input is {@code null}; otherwise, the year of the century * (two-digit year) */ @ScriptApi public static int yearOfCentury(@Nullable final ZonedDateTime dateTime) { if (dateTime == null) { return NULL_INT; } return year(dateTime) % 100; } /** * Returns a {@link ZonedDateTime} for the prior midnight in the specified time zone. * * @param localDateTime local date time to compute the prior midnight for * @param timeZone time zone * @return {@code null} if either input is {@code null}; otherwise a {@link ZonedDateTime} representing the prior * midnight in the specified time zone */ @ScriptApi @Nullable public static ZonedDateTime atMidnight(@Nullable final LocalDateTime localDateTime, @Nullable ZoneId timeZone) { if (localDateTime == null || timeZone == null) { return null; } return localDateTime.toLocalDate().atStartOfDay(timeZone); } /** * Returns a {@link ZonedDateTime} for the prior midnight in the specified time zone. * * @param localDate local date to compute the prior midnight for * @param timeZone time zone * @return {@code null} if either input is {@code null}; otherwise a {@link ZonedDateTime} representing the prior * midnight in the specified time zone */ @ScriptApi @Nullable public static ZonedDateTime atMidnight(@Nullable final LocalDate localDate, @Nullable ZoneId timeZone) { if (localDate == null || timeZone == null) { return null; } return localDate.atStartOfDay(timeZone); } /** * Returns an {@link Instant} for the prior midnight in the specified time zone. * * @param instant time to compute the prior midnight for * @param timeZone time zone * @return {@code null} if either input is {@code null}; otherwise an {@link Instant} representing the prior * midnight in the specified time zone */ @ScriptApi @Nullable public static Instant atMidnight(@Nullable final Instant instant, @Nullable final ZoneId timeZone) { if (instant == null || timeZone == null) { return null; } return toInstant(atMidnight(toZonedDateTime(instant, timeZone))); } /** * Returns a {@link ZonedDateTime} for the prior midnight in the specified time zone. * * @param dateTime time to compute the prior midnight for * @return {@code null} if either input is {@code null}; otherwise a {@link ZonedDateTime} representing the prior * midnight in the specified time zone */ @ScriptApi @Nullable public static ZonedDateTime atMidnight(@Nullable final ZonedDateTime dateTime) { if (dateTime == null) { return null; } return dateTime.toLocalDate().atStartOfDay(dateTime.getZone()); } // endregion // region Binning /** * Returns an {@link Instant} value, which is at the starting (lower) end of a time range defined by the interval * nanoseconds. For example, a five-minute {@code intervalNanos} value would return the instant value for the start * of the five-minute window that contains the input instant. * * @param instant instant for which to evaluate the start of the containing window * @param intervalNanos size of the window in nanoseconds * @return {@code null} if either input is {@code null}; otherwise, an {@link Instant} representing the start of the * window */ @ScriptApi @Nullable public static Instant lowerBin(@Nullable final Instant instant, long intervalNanos) { if (instant == null || intervalNanos == NULL_LONG) { return null; } return epochNanosToInstant(Numeric.lowerBin(epochNanos(instant), intervalNanos)); } /** * Returns an {@link Instant} value, which is at the starting (lower) end of a time range defined by the interval * nanoseconds. For example, a five-minute {@code interval} value would return the instant value for the start of * the five-minute window that contains the input instant. * * @param instant instant for which to evaluate the start of the containing window * @param interval size of the window * @return {@code null} if either input is {@code null}; otherwise, an {@link Instant} representing the start of the * window */ @ScriptApi @Nullable public static Instant lowerBin(@Nullable final Instant instant, Duration interval) { if (instant == null || interval == null) { return null; } return lowerBin(instant, interval.toNanos()); } /** * Returns a {@link ZonedDateTime} value, which is at the starting (lower) end of a time range defined by the * interval nanoseconds. For example, a five-minute {@code intervalNanos} value would return the zoned date time * value for the start of the five-minute window that contains the input zoned date time. * * @param dateTime zoned date time for which to evaluate the start of the containing window * @param intervalNanos size of the window in nanoseconds * @return {@code null} if either input is {@code null}; otherwise, a {@link ZonedDateTime} representing the start * of the window */ @ScriptApi @Nullable public static ZonedDateTime lowerBin(@Nullable final ZonedDateTime dateTime, long intervalNanos) { if (dateTime == null || intervalNanos == NULL_LONG) { return null; } return epochNanosToZonedDateTime(Numeric.lowerBin(epochNanos(dateTime), intervalNanos), dateTime.getZone()); } /** * Returns a {@link ZonedDateTime} value, which is at the starting (lower) end of a time range defined by the * interval nanoseconds. For example, a five-minute {@code interval} value would return the zoned date time value * for the start of the five-minute window that contains the input zoned date time. * * @param dateTime zoned date time for which to evaluate the start of the containing window * @param interval size of the window * @return {@code null} if either input is {@code null}; otherwise, a {@link ZonedDateTime} representing the start * of the window */ @ScriptApi @Nullable public static ZonedDateTime lowerBin(@Nullable final ZonedDateTime dateTime, Duration interval) { if (dateTime == null || interval == null) { return null; } return lowerBin(dateTime, interval.toNanos()); } /** * Returns an {@link Instant} value, which is at the starting (lower) end of a time range defined by the interval * nanoseconds. For example, a five-minute {@code intervalNanos} value would return the instant value for the start * of the five-minute window that contains the input instant. * * @param instant instant for which to evaluate the start of the containing window * @param intervalNanos size of the window in nanoseconds * @param offset The window start offset in nanoseconds. For example, a value of MINUTE would offset all windows by * one minute. * @return {@code null} if any input is {@code null}; otherwise, an {@link Instant} representing the start of the * window */ @ScriptApi @Nullable public static Instant lowerBin(@Nullable final Instant instant, long intervalNanos, long offset) { if (instant == null || intervalNanos == NULL_LONG || offset == NULL_LONG) { return null; } return epochNanosToInstant(Numeric.lowerBin(epochNanos(instant) - offset, intervalNanos) + offset); } /** * Returns an {@link Instant} value, which is at the starting (lower) end of a time range defined by the interval * nanoseconds. For example, a five-minute {@code interval} value would return the instant value for the start of * the five-minute window that contains the input instant. * * @param instant instant for which to evaluate the start of the containing window * @param interval size of the window * @param offset The window start offset. For example, a value of 'PT1m' would offset all windows by one minute. * @return {@code null} if any input is {@code null}; otherwise, an {@link Instant} representing the start of the * window */ @ScriptApi @Nullable public static Instant lowerBin(@Nullable final Instant instant, Duration interval, Duration offset) { if (instant == null || interval == null || offset == null) { return null; } return lowerBin(instant, interval.toNanos(), offset.toNanos()); } /** * Returns a {@link ZonedDateTime} value, which is at the starting (lower) end of a time range defined by the * interval nanoseconds. For example, a five-minute {@code intervalNanos} value would return the zoned date time * value for the start of the five-minute window that contains the input zoned date time. * * @param dateTime zoned date time for which to evaluate the start of the containing window * @param intervalNanos size of the window in nanoseconds * @param offset The window start offset in nanoseconds. For example, a value of MINUTE would offset all windows by * one minute. * @return {@code null} if any input is {@code null}; otherwise, a {@link ZonedDateTime} representing the start of * the window */ @ScriptApi @Nullable public static ZonedDateTime lowerBin(@Nullable final ZonedDateTime dateTime, long intervalNanos, long offset) { if (dateTime == null || intervalNanos == NULL_LONG || offset == NULL_LONG) { return null; } return epochNanosToZonedDateTime(Numeric.lowerBin(epochNanos(dateTime) - offset, intervalNanos) + offset, dateTime.getZone()); } /** * Returns a {@link ZonedDateTime} value, which is at the starting (lower) end of a time range defined by the * interval nanoseconds. For example, a five-minute {@code interval} intervalNanos value would return the zoned date * time value for the start of the five-minute window that contains the input zoned date time. * * @param dateTime zoned date time for which to evaluate the start of the containing window * @param interval size of the window * @param offset The window start offset. For example, a value of MINUTE would offset all windows by one minute. * @return {@code null} if any input is {@code null}; otherwise, a {@link ZonedDateTime} representing the start of * the window */ @ScriptApi @Nullable public static ZonedDateTime lowerBin(@Nullable final ZonedDateTime dateTime, Duration interval, Duration offset) { if (dateTime == null || interval == null || offset == null) { return null; } return lowerBin(dateTime, interval.toNanos(), offset.toNanos()); } /** * Returns an {@link Instant} value, which is at the ending (upper) end of a time range defined by the interval * nanoseconds. For example, a five-minute {@code intervalNanos} value would return the instant value for the end of * the five-minute window that contains the input instant. * * @param instant instant for which to evaluate the start of the containing window * @param intervalNanos size of the window in nanoseconds * @return {@code null} if either input is {@code null}; otherwise, an {@link Instant} representing the end of the * window */ @ScriptApi @Nullable public static Instant upperBin(@Nullable final Instant instant, long intervalNanos) { if (instant == null || intervalNanos == NULL_LONG) { return null; } return epochNanosToInstant(Numeric.upperBin(epochNanos(instant), intervalNanos)); } /** * Returns an {@link Instant} value, which is at the ending (upper) end of a time range defined by the interval * nanoseconds. For example, a five-minute {@code interval} value would return the instant value for the end of the * five-minute window that contains the input instant. * * @param instant instant for which to evaluate the start of the containing window * @param interval size of the window * @return {@code null} if either input is {@code null}; otherwise, an {@link Instant} representing the end of the * window */ @ScriptApi @Nullable public static Instant upperBin(@Nullable final Instant instant, Duration interval) { if (instant == null || interval == null) { return null; } return upperBin(instant, interval.toNanos()); } /** * Returns a {@link ZonedDateTime} value, which is at the ending (upper) end of a time range defined by the interval * nanoseconds. For example, a five-minute {@code intervalNanos} value would return the zoned date time value for * the end of the five-minute window that contains the input zoned date time. * * @param dateTime zoned date time for which to evaluate the start of the containing window * @param intervalNanos size of the window in nanoseconds * @return {@code null} if either input is {@code null}; otherwise, a {@link ZonedDateTime} representing the end of * the window */ @ScriptApi @Nullable public static ZonedDateTime upperBin(@Nullable final ZonedDateTime dateTime, long intervalNanos) { if (dateTime == null || intervalNanos == NULL_LONG) { return null; } return epochNanosToZonedDateTime(Numeric.upperBin(epochNanos(dateTime), intervalNanos), dateTime.getZone()); } /** * Returns a {@link ZonedDateTime} value, which is at the ending (upper) end of a time range defined by the interval * nanoseconds. For example, a five-minute {@code interval} value would return the zoned date time value for the end * of the five-minute window that contains the input zoned date time. * * @param dateTime zoned date time for which to evaluate the start of the containing window * @param interval size of the window * @return {@code null} if either input is {@code null}; otherwise, a {@link ZonedDateTime} representing the end of * the window */ @ScriptApi @Nullable public static ZonedDateTime upperBin(@Nullable final ZonedDateTime dateTime, Duration interval) { if (dateTime == null || interval == null) { return null; } return upperBin(dateTime, interval.toNanos()); } /** * Returns an {@link Instant} value, which is at the ending (upper) end of a time range defined by the interval * nanoseconds. For example, a five-minute {@code intervalNanos} value would return the instant value for the end of * the five-minute window that contains the input instant. * * @param instant instant for which to evaluate the start of the containing window * @param intervalNanos size of the window in nanoseconds * @param offset The window start offset in nanoseconds. For example, a value of MINUTE would offset all windows by * one minute. * @return {@code null} if any input is {@code null}; otherwise, an {@link Instant} representing the end of the * window */ @ScriptApi @Nullable public static Instant upperBin(@Nullable final Instant instant, long intervalNanos, long offset) { if (instant == null || intervalNanos == NULL_LONG || offset == NULL_LONG) { return null; } return epochNanosToInstant(Numeric.upperBin(epochNanos(instant) - offset, intervalNanos) + offset); } /** * Returns an {@link Instant} value, which is at the ending (upper) end of a time range defined by the interval * nanoseconds. For example, a five-minute {@code interval} value would return the instant value for the end of the * five-minute window that contains the input instant. * * @param instant instant for which to evaluate the start of the containing window * @param interval size of the window * @param offset The window start offset. For example, a value of 'PT1m' would offset all windows by one minute. * @return {@code null} if any input is {@code null}; otherwise, an {@link Instant} representing the end of the * window */ @ScriptApi @Nullable public static Instant upperBin(@Nullable final Instant instant, Duration interval, Duration offset) { if (instant == null || interval == null || offset == null) { return null; } return upperBin(instant, interval.toNanos(), offset.toNanos()); } /** * Returns a {@link ZonedDateTime} value, which is at the ending (upper) end of a time range defined by the interval * nanoseconds. For example, a five-minute {@code intervalNanos} value would return the zoned date time value for * the end of the five-minute window that contains the input zoned date time. * * @param dateTime zoned date time for which to evaluate the start of the containing window * @param intervalNanos size of the window in nanoseconds * @param offset The window start offset in nanoseconds. For example, a value of MINUTE would offset all windows by * one minute. * @return {@code null} if any input is {@code null}; otherwise, a {@link ZonedDateTime} representing the end of the * window */ @ScriptApi @Nullable public static ZonedDateTime upperBin(@Nullable final ZonedDateTime dateTime, long intervalNanos, long offset) { if (dateTime == null || intervalNanos == NULL_LONG || offset == NULL_LONG) { return null; } return epochNanosToZonedDateTime(Numeric.upperBin(epochNanos(dateTime) - offset, intervalNanos) + offset, dateTime.getZone()); } /** * Returns a {@link ZonedDateTime} value, which is at the ending (upper) end of a time range defined by the interval * nanoseconds. For example, a five-minute {@code interval} value would return the zoned date time value for the end * of the five-minute window that contains the input zoned date time. * * @param dateTime zoned date time for which to evaluate the start of the containing window * @param interval size of the window * @param offset The window start offset. For example, a value of 'PT1m' would offset all windows by one minute. * @return {@code null} if any input is {@code null}; otherwise, a {@link ZonedDateTime} representing the end of the * window */ @ScriptApi @Nullable public static ZonedDateTime upperBin(@Nullable final ZonedDateTime dateTime, Duration interval, Duration offset) { if (dateTime == null || interval == null || offset == null) { return null; } return upperBin(dateTime, interval.toNanos(), offset.toNanos()); } // endregion // region Format /** * Pads a string with zeros. * * @param str string * @param length desired time string length * @return input string padded with zeros to the desired length. If the input string is longer than the desired * length, the input string is returned. */ @NotNull static String padZeros(@NotNull final String str, final int length) { if (length <= str.length()) { return str; } return "0".repeat(length - str.length()) + str; } /** * Returns a nanosecond duration formatted as a "[-]PThhh:mm:ss.nnnnnnnnn" string. * * @param nanos nanoseconds, or {@code null} if the input is {@link QueryConstants#NULL_LONG} * @return the nanosecond duration formatted as a "[-]PThhh:mm:ss.nnnnnnnnn" string */ @ScriptApi @Nullable public static String formatDurationNanos(long nanos) { if (nanos == NULL_LONG) { return null; } StringBuilder buf = new StringBuilder(25); if (nanos < 0) { buf.append('-'); nanos = -nanos; } buf.append("PT"); int hours = (int) (nanos / 3600000000000L); nanos %= 3600000000000L; int minutes = (int) (nanos / 60000000000L); nanos %= 60000000000L; int seconds = (int) (nanos / 1000000000L); nanos %= 1000000000L; buf.append(hours).append(':').append(padZeros(String.valueOf(minutes), 2)).append(':') .append(padZeros(String.valueOf(seconds), 2)); if (nanos != 0) { buf.append('.').append(padZeros(String.valueOf(nanos), 9)); } return buf.toString(); } /** * Returns an {@link Instant} formatted as a "yyyy-MM-ddThh:mm:ss.SSSSSSSSS TZ" string. * * @param instant time to format as a string * @param timeZone time zone to use when formatting the string. * @return {@code null} if either input is {@code null}; otherwise, the time formatted as a * "yyyy-MM-ddThh:mm:ss.nnnnnnnnn TZ" string */ @ScriptApi @Nullable public static String formatDateTime(@Nullable final Instant instant, @Nullable final ZoneId timeZone) { if (instant == null || timeZone == null) { return null; } return formatDateTime(toZonedDateTime(instant, timeZone)); } /** * Returns a {@link ZonedDateTime} formatted as a "yyyy-MM-ddThh:mm:ss.SSSSSSSSS TZ" string. * * @param dateTime time to format as a string * @return {@code null} if either input is {@code null}; otherwise, the time formatted as a * "yyyy-MM-ddThh:mm:ss.nnnnnnnnn TZ" string */ @ScriptApi @Nullable public static String formatDateTime(@Nullable final ZonedDateTime dateTime) { if (dateTime == null) { return null; } final String timeZone = TimeZoneAliases.zoneName(dateTime.getZone()); final String ldt = ISO_LOCAL_DATE_TIME.format(dateTime); final StringBuilder sb = new StringBuilder(); sb.append(ldt); int pad = 29 - ldt.length(); if (ldt.length() == 19) { sb.append("."); pad--; } return sb.append("0".repeat(Math.max(0, pad))).append(" ").append(timeZone).toString(); } /** * Returns an {@link Instant} formatted as a "yyyy-MM-dd" string. * * @param instant time to format as a string * @param timeZone time zone to use when formatting the string. * @return {@code null} if either input is {@code null}; otherwise, the time formatted as a "yyyy-MM-dd" string */ @ScriptApi @Nullable public static String formatDate(@Nullable final Instant instant, @Nullable final ZoneId timeZone) { if (instant == null || timeZone == null) { return null; } return formatDate(toZonedDateTime(instant, timeZone)); } /** * Returns a {@link ZonedDateTime} formatted as a "yyyy-MM-dd" string. * * @param dateTime time to format as a string * @return {@code null} if either input is {@code null}; otherwise, the time formatted as a "yyyy-MM-dd" string */ @ScriptApi @Nullable public static String formatDate(@Nullable final ZonedDateTime dateTime) { if (dateTime == null) { return null; } return ISO_LOCAL_DATE.format(dateTime); } /** * Returns a {@link LocalDateTime} formatted as a "yyyy-MM-dd" string. * * @param localDateTime local date time to format as a string * @return {@code null} if either input is {@code null}; otherwise, the time formatted as a "yyyy-MM-dd" string */ @ScriptApi @Nullable public static String formatDate(@Nullable final LocalDateTime localDateTime) { if (localDateTime == null) { return null; } return ISO_LOCAL_DATE.format(localDateTime); } /** * Returns a {@link LocalDate} formatted as a "yyyy-MM-dd" string. * * @param localDate local date to format as a string * @return {@code null} if either input is {@code null}; otherwise, the time formatted as a "yyyy-MM-dd" string */ @ScriptApi @Nullable public static String formatDate(@Nullable final LocalDate localDate) { if (localDate == null) { return null; } return ISO_LOCAL_DATE.format(localDate); } // endregion // region Parse /** * Exception type thrown when date time string representations can not be parsed. */ public static class DateTimeParseException extends RuntimeException { private DateTimeParseException(String msg) { super(msg); } private DateTimeParseException(String msg, Exception ex) { super(msg, ex); } } /** * Parses the string argument as a time zone. * * @param s string to be converted * @return a {@link ZoneId} represented by the input string * @throws DateTimeParseException if the string cannot be converted * @see ZoneId * @see TimeZoneAliases */ @ScriptApi @NotNull public static ZoneId parseTimeZone(@NotNull final String s) { // noinspection ConstantConditions if (s == null) { throw new DateTimeParseException("Cannot parse time zone (null): " + s); } try { return TimeZoneAliases.zoneId(s); } catch (Exception ex) { throw new DateTimeParseException("Cannot parse time zone: " + s); } } /** * Parses the string argument as a time zone. * * @param s string to be converted * @return a {@link ZoneId} represented by the input string, or {@code null} if the string can not be parsed * @see ZoneId * @see TimeZoneAliases */ @ScriptApi @Nullable public static ZoneId parseTimeZoneQuiet(@Nullable final String s) { if (s == null || s.length() <= 1) { return null; } try { return parseTimeZone(s); } catch (Exception ex) { return null; } } /** * Parses the string argument as a time duration in nanoseconds. *

* Time duration strings can be formatted as {@code [-]PT[-]hh:mm:[ss.nnnnnnnnn]} or as a duration string formatted * as {@code [-]PnDTnHnMn.nS}. * * @param s string to be converted * @return the number of nanoseconds represented by the string * @throws DateTimeParseException if the string cannot be parsed * @see #parseDuration(String) * @see #parseDurationQuiet(String) */ @ScriptApi public static long parseDurationNanos(@NotNull String s) { // noinspection ConstantConditions if (s == null) { throw new DateTimeParseException("Cannot parse time: " + s); } try { return parseDuration(s).toNanos(); } catch (Exception e) { throw new DateTimeParseException("Cannot parse time: " + s, e); } } /** * Parses the string argument as a time duration in nanoseconds. *

* Time duration strings can be formatted as {@code [-]PT[-]hh:mm:[ss.nnnnnnnnn]} or as a duration string formatted * as {@code [-]PnDTnHnMn.nS}. * * @param s string to be converted * @return the number of nanoseconds represented by the string, or {@link QueryConstants#NULL_LONG} if the string * cannot be parsed * @see #parseDuration(String) * @see #parseDurationQuiet(String) */ @ScriptApi public static long parseDurationNanosQuiet(@Nullable String s) { if (s == null || s.length() <= 1) { return NULL_LONG; } try { return parseDurationNanos(s); } catch (Exception e) { return NULL_LONG; } } /** * Parses the string argument as a period, which is a unit of time in terms of calendar time (days, weeks, months, * years, etc.). *

* Period strings are formatted according to the ISO-8601 duration format as {@code PnYnMnD} and {@code PnW}, where * the coefficients can be positive or negative. Zero coefficients can be omitted. Optionally, the string can begin * with a negative sign. *

* Examples: * *

     *   "P2Y"             -- Period.ofYears(2)
     *   "P3M"             -- Period.ofMonths(3)
     *   "P4W"             -- Period.ofWeeks(4)
     *   "P5D"             -- Period.ofDays(5)
     *   "P1Y2M3D"         -- Period.of(1, 2, 3)
     *   "P1Y2M3W4D"       -- Period.of(1, 2, 25)
     *   "P-1Y2M"          -- Period.of(-1, 2, 0)
     *   "-P1Y2M"          -- Period.of(-1, -2, 0)
     * 
* * @param s period string * @return the period * @throws DateTimeParseException if the string cannot be parsed * @see Period#parse(CharSequence) */ @ScriptApi @NotNull public static Period parsePeriod(@NotNull final String s) { // noinspection ConstantConditions if (s == null) { throw new DateTimeParseException("Cannot parse period (null): " + s); } try { return Period.parse(s); } catch (Exception ex) { throw new DateTimeParseException("Cannot parse period: " + s, ex); } } /** * Parses the string argument as a period, which is a unit of time in terms of calendar time (days, weeks, months, * years, etc.). *

* Period strings are formatted according to the ISO-8601 duration format as {@code PnYnMnD} and {@code PnW}, where * the coefficients can be positive or negative. Zero coefficients can be omitted. Optionally, the string can begin * with a negative sign. *

* Examples: * *

     *   "P2Y"             -- Period.ofYears(2)
     *   "P3M"             -- Period.ofMonths(3)
     *   "P4W"             -- Period.ofWeeks(4)
     *   "P5D"             -- Period.ofDays(5)
     *   "P1Y2M3D"         -- Period.of(1, 2, 3)
     *   "P1Y2M3W4D"       -- Period.of(1, 2, 25)
     *   "P-1Y2M"          -- Period.of(-1, 2, 0)
     *   "-P1Y2M"          -- Period.of(-1, -2, 0)
     * 
* * @param s period string * @return the period, or {@code null} if the string can not be parsed * @see Period#parse(CharSequence) */ @ScriptApi @Nullable public static Period parsePeriodQuiet(@Nullable final String s) { if (s == null || s.length() <= 1) { return null; } try { return parsePeriod(s); } catch (Exception e) { return null; } } /** * Parses the string argument as a duration, which is a unit of time in terms of clock time (24-hour days, hours, * minutes, seconds, and nanoseconds). *

* Duration strings are formatted according to the ISO-8601 duration format as {@code [-]PnDTnHnMn.nS}, where the * coefficients can be positive or negative. Zero coefficients can be omitted. Optionally, the string can begin with * a negative sign. *

* Examples: * *

     *    "PT20.345S" -- parses as "20.345 seconds"
     *    "PT15M"     -- parses as "15 minutes" (where a minute is 60 seconds)
     *    "PT10H"     -- parses as "10 hours" (where an hour is 3600 seconds)
     *    "P2D"       -- parses as "2 days" (where a day is 24 hours or 86400 seconds)
     *    "P2DT3H4M"  -- parses as "2 days, 3 hours and 4 minutes"
     *    "PT-6H3M"    -- parses as "-6 hours and +3 minutes"
     *    "-PT6H3M"    -- parses as "-6 hours and -3 minutes"
     *    "-PT-6H+3M"  -- parses as "+6 hours and -3 minutes"
     * 
* * @param s duration string * @return the duration * @throws DateTimeParseException if the string cannot be parsed * @see Duration#parse(CharSequence) */ @ScriptApi @NotNull public static Duration parseDuration(@NotNull final String s) { // noinspection ConstantConditions if (s == null) { throw new DateTimeParseException("Cannot parse duration (null): " + s); } try { final Matcher tdMatcher = TIME_DURATION_PATTERN.matcher(s); if (tdMatcher.matches()) { final String sign1Str = tdMatcher.group("sign1"); final String sign2Str = tdMatcher.group("sign2"); final String hourStr = tdMatcher.group("hour"); final String minuteStr = tdMatcher.group("minute"); final String secondStr = tdMatcher.group("second"); final String nanosStr = tdMatcher.group("nanos"); long sign1 = 0; if (sign1Str == null || sign1Str.isEmpty() || sign1Str.equals("+")) { sign1 = 1; } else if (sign1Str.equals("-")) { sign1 = -1; } else { throw new RuntimeException("Unsupported sign: '" + sign1 + "'"); } long sign2 = 0; if (sign2Str == null || sign2Str.isEmpty() || sign2Str.equals("+")) { sign2 = 1; } else if (sign2Str.equals("-")) { sign2 = -1; } else { throw new RuntimeException("Unsupported sign: '" + sign2 + "'"); } if (hourStr == null) { throw new RuntimeException("Missing hour value"); } long rst = Long.parseLong(hourStr) * HOUR; if (minuteStr == null) { throw new RuntimeException("Missing minute value"); } rst += Long.parseLong(minuteStr) * MINUTE; if (secondStr != null) { rst += Long.parseLong(secondStr.substring(1)) * SECOND; } if (nanosStr != null) { final String sn = nanosStr.substring(1) + "0".repeat(10 - nanosStr.length()); rst += Long.parseLong(sn); } return Duration.ofNanos(sign1 * sign2 * rst); } return Duration.parse(s); } catch (Exception ex) { throw new DateTimeParseException("Cannot parse duration: " + s, ex); } } /** * Parses the string argument as a duration, which is a unit of time in terms of clock time (24-hour days, hours, * minutes, seconds, and nanoseconds). *

* Duration strings are formatted according to the ISO-8601 duration format as {@code [-]PnDTnHnMn.nS}, where the * coefficients can be positive or negative. Zero coefficients can be omitted. Optionally, the string can begin with * a negative sign. *

* Examples: * *

     *    "PT20.345S" -- parses as "20.345 seconds"
     *    "PT15M"     -- parses as "15 minutes" (where a minute is 60 seconds)
     *    "PT10H"     -- parses as "10 hours" (where an hour is 3600 seconds)
     *    "P2D"       -- parses as "2 days" (where a day is 24 hours or 86400 seconds)
     *    "P2DT3H4M"  -- parses as "2 days, 3 hours and 4 minutes"
     *    "PT-6H3M"    -- parses as "-6 hours and +3 minutes"
     *    "-PT6H3M"    -- parses as "-6 hours and -3 minutes"
     *    "-PT-6H+3M"  -- parses as "+6 hours and -3 minutes"
     * 
* * @param s duration string * @return the duration, or {@code null} if the string can not be parsed * @see Duration#parse(CharSequence) */ @ScriptApi @Nullable public static Duration parseDurationQuiet(@Nullable final String s) { if (s == null || s.length() <= 1) { return null; } try { return parseDuration(s); } catch (Exception e) { return null; } } /** * Parses the string argument as nanoseconds since the Epoch. *

* Date time strings are formatted according to the ISO 8601 date time format * {@code yyyy-MM-ddThh:mm:ss[.SSSSSSSSS] TZ} and others. Additionally, date time strings can be integer values that * are nanoseconds, milliseconds, or seconds from the Epoch. Expected date ranges are used to infer the units. * * @param s date time string * @return a long number of nanoseconds since the Epoch, matching the instant represented by the input string * @throws DateTimeParseException if the string cannot be parsed * @see #epochAutoToEpochNanos * @see #parseInstant(String) * @see DateTimeFormatter#ISO_INSTANT */ @ScriptApi public static long parseEpochNanos(@NotNull final String s) { // noinspection ConstantConditions if (s == null) { throw new DateTimeParseException("Cannot parse epoch nanos (null): " + s); } try { if (LONG_PATTERN.matcher(s).matches()) { return epochAutoToEpochNanos(Long.parseLong(s)); } return epochNanos(parseZonedDateTime(s)); } catch (Exception e) { throw new DateTimeParseException("Cannot parse epoch nanos: " + s, e); } } /** * Parses the string argument as a nanoseconds since the Epoch. *

* Date time strings are formatted according to the ISO 8601 date time format * {@code yyyy-MM-ddThh:mm:ss[.SSSSSSSSS] TZ} and others. Additionally, date time strings can be integer values that * are nanoseconds, milliseconds, or seconds from the Epoch. Expected date ranges are used to infer the units. * * @param s date time string * @return a long number of nanoseconds since the Epoch, matching the instant represented by the input string, or * {@code null} if the string can not be parsed * @see DateTimeFormatter#ISO_INSTANT */ @ScriptApi public static long parseEpochNanosQuiet(@Nullable final String s) { if (s == null || s.length() <= 1) { return NULL_LONG; } try { return parseEpochNanos(s); } catch (Exception e) { return NULL_LONG; } } /** * Parses the string argument as an {@link Instant}. *

* Date time strings are formatted according to the ISO 8601 date time format * {@code yyyy-MM-ddThh:mm:ss[.SSSSSSSSS] TZ} and others. Additionally, date time strings can be integer values that * are nanoseconds, milliseconds, or seconds from the Epoch. Expected date ranges are used to infer the units. * * @param s date time string * @return an {@link Instant} represented by the input string * @throws DateTimeParseException if the string cannot be parsed * @see DateTimeFormatter#ISO_INSTANT */ @ScriptApi @NotNull public static Instant parseInstant(@NotNull final String s) { // noinspection ConstantConditions if (s == null) { throw new DateTimeParseException("Cannot parse instant (null): " + s); } try { if (LONG_PATTERN.matcher(s).matches()) { final long nanos = epochAutoToEpochNanos(Long.parseLong(s)); // noinspection ConstantConditions return epochNanosToInstant(nanos); } return parseZonedDateTime(s).toInstant(); } catch (Exception e) { throw new DateTimeParseException("Cannot parse instant: " + s, e); } } /** * Parses the string argument as an {@link Instant}. *

* Date time strings are formatted according to the ISO 8601 date time format * {@code yyyy-MM-ddThh:mm:ss[.SSSSSSSSS] TZ} and others. Additionally, date time strings can be integer values that * are nanoseconds, milliseconds, or seconds from the Epoch. Expected date ranges are used to infer the units. * * @param s date time string * @return an {@link Instant} represented by the input string, or {@code null} if the string can not be parsed * @see DateTimeFormatter#ISO_INSTANT */ @ScriptApi @Nullable public static Instant parseInstantQuiet(@Nullable final String s) { if (s == null || s.length() <= 1) { return null; } try { return parseInstant(s); } catch (Exception e) { return null; } } /** * Parses the string argument as a {@link LocalDateTime}. *

* Date time strings are formatted according to the ISO 8601 date time format * {@code yyyy-MM-ddThh:mm:ss[.SSSSSSSSS]} and others. * * @param s date time string * @return a {@link LocalDateTime} represented by the input string * @throws DateTimeParseException if the string cannot be parsed */ @ScriptApi @NotNull public static LocalDateTime parseLocalDateTime(@NotNull final String s) { // noinspection ConstantConditions if (s == null) { throw new DateTimeParseException("Cannot parse local date time (null): " + s); } try { return LocalDateTime.parse(s); } catch (java.time.format.DateTimeParseException e) { // ignore } try { final Matcher dtMatcher = LOCAL_DATE_PATTERN.matcher(s); if (dtMatcher.matches()) { final String dateString = dtMatcher.group("date"); return LocalDate.parse(dateString, FORMATTER_ISO_LOCAL_DATE).atTime(LocalTime.of(0, 0)); } return LocalDateTime.parse(s, FORMATTER_ISO_LOCAL_DATE_TIME); } catch (Exception ex) { throw new DateTimeParseException("Cannot parse local date time: " + s, ex); } } /** * Parses the string argument as a {@link LocalDateTime}. *

* Date time strings are formatted according to the ISO 8601 date time format * {@code yyyy-MM-ddThh:mm:ss[.SSSSSSSSS]} and others. * * @param s date time string * @return a {@link LocalDateTime} represented by the input string, or {@code null} if the string can not be parsed */ @ScriptApi @Nullable public static LocalDateTime parseLocalDateTimeQuiet(@Nullable final String s) { if (s == null || s.length() <= 1) { return null; } try { return parseLocalDateTime(s); } catch (Exception e) { return null; } } /** * Parses the string argument as a {@link ZonedDateTime}. *

* Date time strings are formatted according to the ISO 8601 date time format * {@code yyyy-MM-ddThh:mm:ss[.SSSSSSSSS] TZ} and others. * * @param s date time string * @return a {@link ZonedDateTime} represented by the input string * @throws DateTimeParseException if the string cannot be parsed * @see DateTimeFormatter#ISO_INSTANT */ @ScriptApi @NotNull public static ZonedDateTime parseZonedDateTime(@NotNull final String s) { // noinspection ConstantConditions if (s == null) { throw new DateTimeParseException("Cannot parse datetime (null): " + s); } try { return ZonedDateTime.parse(s); } catch (java.time.format.DateTimeParseException e) { // ignore } try { final Matcher dtzMatcher = DATE_TZ_PATTERN.matcher(s); if (dtzMatcher.matches()) { final String dateString = dtzMatcher.group("date"); final String timeZoneString = dtzMatcher.group("timezone"); final ZoneId timeZone = parseTimeZoneQuiet(timeZoneString); if (timeZone == null) { throw new RuntimeException("No matching time zone: '" + timeZoneString + "'"); } return LocalDate.parse(dateString, FORMATTER_ISO_LOCAL_DATE).atTime(LocalTime.of(0, 0)) .atZone(timeZone); } int spaceIndex = s.indexOf(' '); if (spaceIndex == -1) { throw new RuntimeException("No time zone provided"); } final String dateTimeString = s.substring(0, spaceIndex); final String timeZoneString = s.substring(spaceIndex + 1); final ZoneId timeZone = parseTimeZoneQuiet(timeZoneString); if (timeZone == null) { throw new RuntimeException("No matching time zone: " + timeZoneString); } return LocalDateTime.parse(dateTimeString, FORMATTER_ISO_LOCAL_DATE_TIME).atZone(timeZone); } catch (Exception ex) { throw new DateTimeParseException("Cannot parse zoned date time: " + s, ex); } } /** * Parses the string argument as a {@link ZonedDateTime}. *

* Date time strings are formatted according to the ISO 8601 date time format * {@code yyyy-MM-ddThh:mm:ss[.SSSSSSSSS] TZ} and others. * * @param s date time string * @return a {@link ZonedDateTime} represented by the input string, or {@code null} if the string can not be parsed * @see DateTimeFormatter#ISO_INSTANT */ @ScriptApi @Nullable public static ZonedDateTime parseZonedDateTimeQuiet(@Nullable final String s) { if (s == null || s.length() <= 1) { return null; } try { return parseZonedDateTime(s); } catch (Exception e) { return null; } } private enum DateGroupId { // Date(1), Year(2, ChronoField.YEAR), Month(3, ChronoField.MONTH_OF_YEAR), Day(4, ChronoField.DAY_OF_MONTH), // Tod(5), Hours(6, ChronoField.HOUR_OF_DAY), Minutes(7, ChronoField.MINUTE_OF_HOUR), Seconds(8, ChronoField.SECOND_OF_MINUTE), Fraction(9, ChronoField.MILLI_OF_SECOND); // TODO MICRO and NANOs are not supported! -- fix and unit test! final int id; final ChronoField field; DateGroupId(int id, @NotNull ChronoField field) { this.id = id; this.field = field; } } /** * Returns a {@link ChronoField} indicating the level of precision in a time, datetime, or period nanos string. * * @param s time string * @return {@link ChronoField} for the finest units in the string (e.g. "10:00:00" would yield SecondOfMinute) * @throws RuntimeException if the string cannot be parsed */ @ScriptApi @NotNull public static ChronoField parseTimePrecision(@NotNull final String s) { // noinspection ConstantConditions if (s == null) { throw new DateTimeParseException("Cannot parse time precision (null): " + s); } try { Matcher dtMatcher = CAPTURING_DATETIME_PATTERN.matcher(s); if (dtMatcher.matches()) { DateGroupId[] parts = DateGroupId.values(); for (int i = parts.length - 1; i >= 0; i--) { String part = dtMatcher.group(parts[i].id); if (part != null && !part.isEmpty()) { return parts[i].field; } } } if (TIME_DURATION_PATTERN.matcher(s).matches()) { return parseTimePrecision(s.replace("PT", "")); } throw new RuntimeException("Time precision does not match expected pattern"); } catch (Exception ex) { throw new DateTimeParseException("Cannot parse time precision: " + s, ex); } } /** * Returns a {@link ChronoField} indicating the level of precision in a time or datetime string. * * @param s time string * @return {@code null} if the time string cannot be parsed; otherwise, a {@link ChronoField} for the finest units * in the string (e.g. "10:00:00" would yield SecondOfMinute) * @throws RuntimeException if the string cannot be parsed */ @ScriptApi @Nullable public static ChronoField parseTimePrecisionQuiet(@Nullable final String s) { if (s == null || s.length() <= 1) { return null; } try { return parseTimePrecision(s); } catch (Exception e) { return null; } } /** * Parses the string argument as a local date, which is a date without a time or time zone. *

* Date strings are formatted according to the ISO 8601 date time format as {@code YYYY-MM-DD}. * * @param s date string * @return local date parsed according to the default date style * @throws DateTimeParseException if the string cannot be parsed * @see DateTimeFormatter#ISO_LOCAL_DATE */ @ScriptApi @NotNull public static LocalDate parseLocalDate(@NotNull final String s) { // noinspection ConstantConditions if (s == null) { throw new DateTimeParseException("Cannot parse datetime (null): " + s); } try { return LocalDate.parse(s, FORMATTER_ISO_LOCAL_DATE); } catch (java.time.format.DateTimeParseException e) { throw new DateTimeParseException("Cannot parse local date: " + s, e); } } /** * Parses the string argument as a local date, which is a date without a time or time zone. *

* Date strings are formatted according to the ISO 8601 date time format as {@code YYYY-MM-DD}. * * @param s date string * @return local date parsed according to the default date style, or {@code null} if the string can not be parsed * @see DateTimeFormatter#ISO_LOCAL_DATE */ @ScriptApi @Nullable public static LocalDate parseLocalDateQuiet(@Nullable final String s) { if (s == null || s.length() <= 1) { return null; } try { return parseLocalDate(s); } catch (Exception e) { return null; } } /** * Parses the string argument as a local time, which is the time that would be read from a clock and does not have a * date or timezone. *

* Local time strings can be formatted as {@code hh:mm:ss[.nnnnnnnnn]}. * * @param s string to be converted * @return a {@link LocalTime} represented by the input string. * @throws DateTimeParseException if the string cannot be converted, otherwise a {@link LocalTime} from the parsed * string */ @ScriptApi @NotNull public static LocalTime parseLocalTime(@NotNull final String s) { // noinspection ConstantConditions if (s == null) { throw new DateTimeParseException("Cannot parse local time (null): " + s); } try { return LocalTime.parse(s, FORMATTER_ISO_LOCAL_TIME); } catch (java.time.format.DateTimeParseException e) { throw new DateTimeParseException("Cannot parse local time: " + s, e); } } /** * Parses the string argument as a local time, which is the time that would be read from a clock and does not have a * date or timezone. *

* Local time strings can be formatted as {@code hh:mm:ss[.nnnnnnnnn]}. * * @param s string to be converted * @return a {@link LocalTime} represented by the input string, or {@code null} if the string can not be parsed */ @ScriptApi @Nullable public static LocalTime parseLocalTimeQuiet(@Nullable final String s) { if (s == null || s.length() <= 1) { return null; } try { return parseLocalTime(s); } catch (Exception e) { return null; } } // endregion }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy