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

oracle.nosql.driver.util.TimestampUtil Maven / Gradle / Ivy

/*-
 * Copyright (c) 2011, 2024 Oracle and/or its affiliates. All rights reserved.
 *
 * Licensed under the Universal Permissive License v 1.0 as shown at
 *  https://oss.oracle.com/licenses/upl/
 */

package oracle.nosql.driver.util;

import static oracle.nosql.driver.util.CheckNull.requireNonNull;

import java.sql.Timestamp;
import java.time.DateTimeException;
import java.time.Instant;
import java.time.LocalDate;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.DateTimeParseException;
import java.time.temporal.ChronoField;
import java.time.temporal.TemporalAccessor;

/**
 * Utility methods to parse/format a Timestamp from/to a string etc.
 */
public class TimestampUtil {

    /* The default pattern used to parse/format a Timestamp from/to a string */
    private final static String DEFAULT_PATTERN = "uuuu-MM-dd['T'HH:mm:ss]";

    /* The UTC zone */
    private final static ZoneId UTCZone = ZoneId.of(ZoneOffset.UTC.getId());

    /* The maxinum number of digits in fractional second */
    private final static int MAX_NUMBER_FRACSEC = 9;

    /*
     * The separator to conjunct components to a string, these are used parse a
     * string in default pattern. Allow ' ' in place of 'T' by default, which is
     * allowed by the spec
     */
    private final static char compSep[] = {'-', '-', 'T', ':', ':', '.'};
    private final static char compSep1[] = {'-', '-', ' ', ':', ':', '.'};

    /*
     * The name of components.
     */
    private final static String compNames[] = {
        "year", "month", "day", "hour", "minute", "second", "fractional second"
    };

    /**
     * Parses a string to a Timestamp Value, the string is in default pattern.
     * @param text the string value
     * @return the Timestamp value represented by the string
     */
    public static Timestamp parseString(String text) {
        return parseString(text, null, true, true);
    }

    /**
     * Parses a string to a Timestamp Value, the string is in specified pattern.
     * @param text the string value
     * @param pattern the regex pattern to use for parsing the string
     * @param withZoneUTC true if using UTC
     * @param optionalFracSecond optional fractional second to use
     * @return the resulting Timestamp
     */
    public static Timestamp parseString(String text,
                                        String pattern,
                                        boolean withZoneUTC,
                                        boolean optionalFracSecond) {
        requireNonNull(text, "Timestamp string must be non-null");

        /*
         * If the specified pattern is the default pattern and with UTC zone,
         * then call parseWithDefaultPattern(String) to parse the timestamp
         * string in a more efficient way.
         */
        boolean optionalZoneOffset = false;
        if (pattern == null || pattern.equals(DEFAULT_PATTERN)) {
            String tsStr = trimUTCZoneOffset(text);
            /*
             * If no zone offset or UTC zone offset in timestamp string, then
             * parse it using parseWithDefaultPattern(). Otherwise, parse it
             * with DateTimeFormatter.
             */
            if (tsStr != null) {
                return parseWithDefaultPattern(tsStr);
            }
            optionalZoneOffset = true;
        }

        String fmt = (pattern != null) ? pattern : DEFAULT_PATTERN;
        try {
            DateTimeFormatter dtf = getDateTimeFormatter(fmt,
                withZoneUTC, optionalFracSecond, optionalZoneOffset);
            TemporalAccessor ta = dtf.parse(text);
            if (!ta.isSupported(ChronoField.YEAR) ||
                !ta.isSupported(ChronoField.MONTH_OF_YEAR) ||
                !ta.isSupported(ChronoField.DAY_OF_MONTH)) {

                throw new IllegalArgumentException("The timestamp string " +
                    "must contain year, month and day");
            }
            Instant instant;
            boolean hasOffset = (ta.isSupported(ChronoField.OFFSET_SECONDS) &&
                ta.get(ChronoField.OFFSET_SECONDS) != 0);
            if (ta.isSupported(ChronoField.HOUR_OF_DAY)) {
                instant = hasOffset ? OffsetDateTime.from(ta).toInstant() :
                    Instant.from(ta);
            } else {
                instant = LocalDate.from(ta).atStartOfDay
                    ((hasOffset ? ZoneOffset.from(ta) : UTCZone)).toInstant();
            }
            return toTimestamp(instant);
        } catch (IllegalArgumentException iae) {
            throw new IllegalArgumentException("Failed to parse the date " +
                "string '" + text + "' with the pattern: " + fmt + ": " +
                iae.getMessage(), iae);
        } catch (DateTimeParseException dtpe) {
            throw new IllegalArgumentException("Failed to parse the date " +
                "string '" + text + "' with the pattern: " + fmt + ": " +
                dtpe.getMessage(), dtpe);
        } catch (DateTimeException dte) {
            throw new IllegalArgumentException("Failed to parse the date " +
                "string '" + text + "' with the pattern: " + fmt + ": " +
                dte.getMessage(), dte);
        }
    }

    /**
     * Trims the designator 'Z' or "+00:00" that represents UTC zone from the
     * Timestamp string if found, return null if Timestamp string contains
     * non-zero offset.
     */
    private static String trimUTCZoneOffset(String ts) {
        if (ts.endsWith("Z")) {
            return ts.substring(0, ts.length() - 1);
        }
        if (ts.endsWith("+00:00")) {
            return ts.substring(0, ts.length() - 6);
        }

        if (!hasSignOfZoneOffset(ts)) {
            return ts;
        }
        return null;
    }

    /**
     * Returns true if the Timestamp string in default pattern contain the
     * sign of ZoneOffset: plus(+) or hyphen(-).
     *
     * If timestamp string in default pattern contains negative zone offset, it
     * must contain 3 hyphen(-), e.g. 2017-12-05T10:20:01-03:00.
     *
     * If timestamp  string contains positive zone offset, it must contain
     * plus(+) sign.
     */
    private static boolean hasSignOfZoneOffset(String ts) {
        if (ts.indexOf('+') > 0) {
            return true;
        }
        int pos = 0;
        for (int i = 0; i < 3; i++) {
            pos = ts.indexOf('-', pos + 1);
            if (pos < 0) {
                return false;
            }
        }
        return true;
    }

    /**
     * Formats a Timestamp to a string in default pattern.
     * @param ts the timestamp value
     * @return the string representation of the timestamp
     */
    public static String formatString(Timestamp ts) {
        requireNonNull(ts, "Timestamp must be non-null");
        return formatString(ts, null, true, true);
    }

    /**
     * Formats a Timestamp to a string in specified pattern.
     */
    private static String formatString(Timestamp timestamp,
                                       String pattern,
                                       boolean withZoneUTC,
                                       boolean optionalFracSecond) {
        requireNonNull(timestamp, "Timestamp must be non-null");
        String fmt = (pattern == null) ? DEFAULT_PATTERN : pattern;
        try {
            ZonedDateTime zdt = toUTCDateTime(timestamp);
            return zdt.format(getDateTimeFormatter(fmt,
                                                   withZoneUTC,
                                                   optionalFracSecond,
                                                   true));
        } catch (IllegalArgumentException iae) {
            throw new IllegalArgumentException("Failed to format the " +
                "timestamp with pattern '" + fmt + "': " + iae.getMessage(),
                iae);
        } catch (DateTimeException dte) {
            throw new IllegalArgumentException("Failed to format the " +
                "timestamp with pattern '" + fmt + "': " + dte.getMessage(),
                dte);
        }
    }

    /**
     * Parses the timestamp string in format of default pattern
     * "uuuu-MM-dd[THH:mm:ss[.S..S]]" with UTC zone.
     */
    private static Timestamp parseWithDefaultPattern(String ts) {

        final int[] comps = new int[7];

        /*
         * The component that is currently being parsed, starting with 0
         * for the year, and up to 6 for the fractional seconds
         */
        int comp = 0;

        int val = 0;
        int ndigits = 0;

        int len = ts.length();
        boolean isBC = (ts.charAt(0) == '-');

        for (int i = (isBC ? 1 : 0); i < len; ++i) {

            char ch = ts.charAt(i);

            if (comp < 6) {

                switch (ch) {
                case '0': case '1': case '2': case '3': case '4':
                case '5': case '6': case '7': case '8': case '9':
                    val = val * 10 + (ch - '0');
                    ++ndigits;
                    break;
                default:
                    if (ch == compSep[comp] || ch == compSep1[comp])  {
                        checkAndSetValue(comps, comp, val, ndigits, ts);
                        ++comp;
                        val = 0;
                        ndigits = 0;

                    } else {
                        raiseParseError(
                            ts, "invalid character '" + ch +
                            "' while parsing component " + compNames[comp]);
                    }
                }
            } else {
                if (comp != 6) {
                    throw new IllegalArgumentException(
                        "Invalid component index: " + comp +
                        ", it is expected to be 6");
                }

                switch (ch) {
                case '0': case '1': case '2': case '3': case '4':
                case '5': case '6': case '7': case '8': case '9':
                    val = val * 10 + (ch - '0');
                    ndigits++;
                    break;
                default:
                    raiseParseError(
                        ts, "invalid character '" + ch +
                        "' while parsing component " + compNames[comp]);
                }
            }
        }

        /* Set the last component */
        checkAndSetValue(comps, comp, val, ndigits, ts);

        if (comp < 2) {
            raiseParseError(
                ts, "the timestamp string must have at least the 3 " +
                "date components");
        }

        if (comp == 6 && comps[6] > 0) {

            if (ndigits > MAX_NUMBER_FRACSEC) {
                raiseParseError(
                    ts, "the fractional-seconds part contains more than " +
                    MAX_NUMBER_FRACSEC + " digits");
            } else if (ndigits < MAX_NUMBER_FRACSEC) {
                /* Nanosecond *= 10 ^ (MAX_PRECISION - s.length()) */
                comps[6] *= (int)Math.pow(10, MAX_NUMBER_FRACSEC - ndigits);
            }
        }

        if (isBC) {
            comps[0] = -comps[0];
        }

        return createTimestamp(comps);
    }

    private static void checkAndSetValue(
        int[] comps,
        int comp,
        int value,
        int ndigits,
        String ts) {

        if (ndigits == 0) {
            raiseParseError(
                ts, "component " + compNames[comp] + "has 0 digits");
        }

        comps[comp] = value;
    }

    private static void raiseParseError(String ts, String err) {

        String errMsg =
            ("Failed to parse the timestamp string '" + ts +
             "' with the pattern: " + DEFAULT_PATTERN + ": ");

        throw new IllegalArgumentException(errMsg + err);
    }

    /**
     * Validates the component of Timestamp, the component is indexed from 0 to
     * 6 that maps to year, month, day, hour, minute, second and nanosecond.
     */
    private static void validateComponent(int index, int value) {
        switch(index) {
            case 1: /* Month */
                if (value < 1 || value > 12) {
                    throw new IllegalArgumentException("Invalid month, it " +
                            "should be in range from 1 to 12: " + value);
                }
                break;
            case 2: /* Day */
                if (value < 1 || value > 31) {
                    throw new IllegalArgumentException("Invalid day, it " +
                            "should be in range from 1 to 31: " + value);
                }
                break;
            case 3: /* Hour */
                if (value < 0 || value > 23) {
                    throw new IllegalArgumentException("Invalid hour, it " +
                            "should be in range from 0 to 23: " + value);
                }
                break;
            case 4: /* Minute */
                if (value < 0 || value > 59) {
                    throw new IllegalArgumentException("Invalid minute, it " +
                            "should be in range from 0 to 59: " + value);
                }
                break;
            case 5: /* Second */
                if (value < 0 || value > 59) {
                    throw new IllegalArgumentException("Invalid second, it " +
                            "should be in range from 0 to 59: " + value);
                }
                break;
            case 6: /* Nanosecond */
                if (value < 0 || value > 999999999) {
                    throw new IllegalArgumentException("Invalid second, it " +
                            "should be in range from 0 to 999999999: " + value);
                }
                break;
        }
    }

    /**
     * Converts a Instant object to Timestamp
     */
    private static Timestamp toTimestamp(Instant instant) {
        return createTimestamp(instant.getEpochSecond(), instant.getNano());
    }

    /**
     * Creates a Timestamp with given seconds since Java epoch and nanosOfSecond
     * @param seconds the seconds value
     * @param nanoSeconds the nanoseconds value
     * @return the resulting Timestamp
     */
    public static Timestamp createTimestamp(long seconds, int nanoSeconds) {
        Timestamp ts = new Timestamp(seconds * 1000);
        ts.setNanos(nanoSeconds);
        return ts;
    }

    /**
     * Creates a Timestamp from components: year, month, day, hour, minute,
     * second and nanosecond.
     */
    private static Timestamp createTimestamp(int[] comps) {

        if (comps.length < 3) {
            throw new IllegalArgumentException("Invalid timestamp " +
                "components, it should contain at least 3 components: year, " +
                "month and day, but only " + comps.length);
        } else if (comps.length > 7) {
            throw new IllegalArgumentException("Invalid timestamp " +
                "components, it should contain at most 7 components: year, " +
                "month, day, hour, minute, second and nanosecond, but has " +
                comps.length + " components");
        }

        int num = comps.length;
        for (int i = 0; i < num; i++) {
            validateComponent(i, comps[i]);
        }
        try {
            ZonedDateTime zdt = ZonedDateTime.of(comps[0],
                                                 comps[1],
                                                 comps[2],
                                                 ((num > 3) ? comps[3] : 0),
                                                 ((num > 4) ? comps[4] : 0),
                                                 ((num > 5) ? comps[5] : 0),
                                                 ((num > 6) ? comps[6] : 0),
                                                 UTCZone);
             return toTimestamp(zdt.toInstant());
        } catch (DateTimeException dte) {
            throw new IllegalArgumentException("Invalid timestamp " +
                "components: " + dte.getMessage());
        }
    }


    /**
     * Converts Timestamp to ZonedDataTime at UTC zone.
     */
    private static ZonedDateTime toUTCDateTime(Timestamp timestamp) {
        return toInstant(timestamp).atZone(UTCZone);
    }

    /**
     * Converts Timestamp to Instant
     */
    private static Instant toInstant(Timestamp timestamp) {
        return Instant.ofEpochSecond(getSeconds(timestamp),
                                     getNanoSeconds(timestamp));
    }

    /**
     * Gets the number of seconds from the Epoch (1970-01-01T00:00:00Z).
     * @param timestamp the Timestamp
     * @return the long value
     */
    public static long getSeconds(Timestamp timestamp) {
        long ms = timestamp.getTime();
        return ms > 0 ? (ms / 1000) : (ms - 999)/1000;
    }

    /**
     * Gets the nanoseconds of the Timestamp value.
     * @param timestamp the Timestamp
     * @return the value in nanoseconds
     */
    public static int getNanoSeconds(Timestamp timestamp) {
        return timestamp.getNanos();
    }

    /**
     * Returns the DateTimeFormatter with the given pattern.
     */
    private static DateTimeFormatter getDateTimeFormatter
        (String pattern,
         boolean withZoneUTC,
         boolean optionalFracSecond,
         boolean optionalOffset) {

        DateTimeFormatterBuilder dtfb = new DateTimeFormatterBuilder();
        dtfb.appendPattern(pattern);
        if (optionalFracSecond) {
            dtfb.optionalStart();
            dtfb.appendFraction(ChronoField.NANO_OF_SECOND, 0,
                MAX_NUMBER_FRACSEC, true);
            dtfb.optionalEnd();
        }
        if (optionalOffset) {
            dtfb.optionalStart();
            dtfb.appendOffset("+HH:MM", "Z");
            dtfb.optionalEnd();
        }
        return dtfb.toFormatter().withZone
            (withZoneUTC ? UTCZone : ZoneId.systemDefault());
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy