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

com.axibase.date.DatetimeProcessorUtil Maven / Gradle / Ivy

There is a newer version: 1.1.0
Show newest version
package com.axibase.date;

import java.time.*;

@SuppressWarnings("squid:S109") // magic constant
public final class DatetimeProcessorUtil {
    static final int NANOS_IN_MILLIS = 1_000_000;
    static final int MILLISECONDS_IN_SECOND = 1000;
    static final int UNIX_EPOCH_YEAR = 1970;
    static final int MIN_YEAR_20_CENTURY = 1900;
    static final int MAX_YEAR = 2200;
    static final long MAX_TIME_MILLIS = LocalDate.of(MAX_YEAR, 1, 1)
        .atStartOfDay(ZoneOffset.UTC)
        .toInstant()
        .toEpochMilli();
    private static final int ISO_LENGTH = "1970-01-01T00:00:00.000000000+00:00:00".length();
    private static final int TIVOLI_LENGTH = "1yyMMddHHmmssSSS".length();
    private static final int TIVOLI_EPOCH_YEAR = 1900;

    private DatetimeProcessorUtil() {}

    /**
     * Optimized print of a timestamp in ISO8601 or local format: yyyy-MM-dd[T| ]HH:mm:ss[.SSS]
     * @param timestamp milliseconds since epoch
     * @param offsetType Zone offset format: ISO (+HH:mm), RFC (+HHmm), or NONE
     * @return String representation of the timestamp
     */
    static String printIso8601(long timestamp, char delimiter, ZoneId zone, ZoneOffsetType offsetType, int fractionsOfSecond) {
        final LocalDateTime localDateTime;
        final ZoneOffset offset;
        if (zone instanceof ZoneOffset) {
            final long secs = Math.floorDiv(timestamp, MILLISECONDS_IN_SECOND);
            final int nanos = (int)Math.floorMod(timestamp, MILLISECONDS_IN_SECOND) * NANOS_IN_MILLIS;
            offset = (ZoneOffset) zone;
            localDateTime = LocalDateTime.ofEpochSecond(secs, nanos, offset);
        } else {
            final OffsetDateTime dateTime = OffsetDateTime.ofInstant(Instant.ofEpochMilli(timestamp), zone);
            localDateTime = dateTime.toLocalDateTime();
            offset = dateTime.getOffset();
        }
        return printIso8601(localDateTime, offset, offsetType, delimiter, fractionsOfSecond);
    }

    /**
     * Optimized print of a timestamp in ISO8601 or local format: yyyy-MM-dd[T| ]HH:mm:ss[.SSS]
     * @param dateTime timestamp as LocalDateTime
     * @param offset time zone offset
     * @param offsetType Zone offset format: ISO (+HH:mm), RFC (+HHmm), or NONE
     * @return String representation of the timestamp
     */
    static String printIso8601(LocalDateTime dateTime, ZoneOffset offset, ZoneOffsetType offsetType, char delimiter, int fractionsOfSecond) {
        final StringBuilder sb = new StringBuilder(ISO_LENGTH);
        adjustPossiblyNegative(sb, dateTime.getYear(), 4).append('-');
        adjust(sb, dateTime.getMonthValue(), 2).append('-');
        adjust(sb, dateTime.getDayOfMonth(), 2).append(delimiter);
        adjust(sb, dateTime.getHour(), 2).append(':');
        adjust(sb, dateTime.getMinute(), 2).append(':');
        adjust(sb, dateTime.getSecond(), 2);
        if (fractionsOfSecond > 0) {
            sb.append('.');
            adjust(sb, dateTime.getNano() / powerOfTen(9 - fractionsOfSecond), fractionsOfSecond);
        }
        return offsetType.appendOffset(sb, offset).toString();
    }

    static LocalDateTime parseTivoliDate(String date) {
        final int length = date.length();
        if (length != TIVOLI_LENGTH) {
            throw new IllegalArgumentException(date + " is not a valid Tivoli date: length must be " + TIVOLI_LENGTH);
        }
        return parseTivoliDate(date, length);
    }

    static ZonedDateTime parseTivoliDateWithOffset(String date) {
        if (date.length() > TIVOLI_LENGTH + 1) {
            final ZoneId zoneId = ZoneOffset.of(date.substring(TIVOLI_LENGTH + 1));
            return parseTivoliDate(date, TIVOLI_LENGTH).atZone(zoneId);
        } else {
            throw new IllegalArgumentException(date + " is not a valid Tivoli date with zone id");
        }
    }

    private static LocalDateTime parseTivoliDate(String date, int length) {
        int offset = 0;
        final int centuriesSinceEpoch = parseInt(date, offset, offset += 1, length);
        final int year = parseInt(date, offset, offset += 2, length);
        final int month = parseInt(date, offset, offset += 2, length);
        final int day = parseInt(date, offset, offset += 2, length);
        final int hour = parseInt(date, offset, offset += 2, length);
        final int minutes = parseInt(date, offset, offset += 2, length);
        final int seconds = parseInt(date, offset, offset += 2, length);
        final int millis = parseInt(date, offset, offset += 3, length);
        final int fullYear = TIVOLI_EPOCH_YEAR + centuriesSinceEpoch * 100 + year;
        final int nanos = millis * NANOS_IN_MILLIS;
        return LocalDateTime.of(fullYear, month, day, hour, minutes, seconds, nanos);
    }

    static String printTivoliDate(ZonedDateTime dateTime) {
        final StringBuilder sb = new StringBuilder(TIVOLI_LENGTH);
        final int century = (dateTime.getYear() - TIVOLI_EPOCH_YEAR) / 100;
        final int year = dateTime.getYear() % 100;
        adjustPossiblyNegative(sb, century, 1);
        adjust(sb, year, 2);
        adjust(sb, dateTime.getMonthValue(), 2);
        adjust(sb, dateTime.getDayOfMonth(), 2);
        adjust(sb, dateTime.getHour(), 2);
        adjust(sb, dateTime.getMinute(), 2);
        adjust(sb, dateTime.getSecond(), 2);
        adjust(sb, dateTime.getNano() / NANOS_IN_MILLIS, 3);
        return sb.toString();
    }

    static boolean checkExpectedMilliseconds(String date, int expected) {
        final int indexOfDot = date.indexOf('.');
        if (expected <= 0) {
            return indexOfDot < 0;
        } else {
            final int length = date.length();
            int cnt = 0;
            for (int i = indexOfDot + 1; i < length; i++) {
                if (Character.isDigit(date.charAt(i))) {
                    ++cnt;
                } else {
                    break;
                }
            }
            return cnt == expected;
        }
    }

    public static ZonedDateTime parseIso8601AsZonedDateTime(String date, char delimiter,
                                                            ZoneId defaultOffset, ZoneOffsetType offsetType) {
        try {
            final ParsingContext context = new ParsingContext();
            final LocalDateTime localDateTime = parseIso8601AsLocalDateTime(date, delimiter, context);
            final ZoneId zoneId = extractOffset(date, context.offset, offsetType, defaultOffset);
            return ZonedDateTime.of(localDateTime, zoneId);
        } catch (RuntimeException e) {
            throw new IllegalArgumentException("Failed to parse date " + date, e);
        }
    }

    private static ZoneId extractOffset(String date, int offset, ZoneOffsetType offsetType, ZoneId defaultOffset) {
        final int length = date.length();
        final ZoneId zoneId;
        if (offset == length) {
            if (offsetType != ZoneOffsetType.NONE || defaultOffset == null) {
                throw new IllegalStateException("Zone offset required");
            }
            zoneId = defaultOffset;
        } else {
            if (offsetType == ZoneOffsetType.NONE) {
                throw new IllegalStateException("Zone offset unexpected");
            }
            if (offset == length - 1 && date.charAt(offset) == 'Z') {
                zoneId = ZoneOffset.UTC;
            } else {
                zoneId = ZoneOffset.of(date.substring(offset));
            }
        }
        return zoneId;
    }

    private static int parseNanos(int value, int digits) {
        return value * powerOfTen(9 - digits);
    }

    private static int parseInt(String value, int beginIndex, int endIndex, int valueLength) throws NumberFormatException {
        if (beginIndex < 0 || endIndex > valueLength || beginIndex >= endIndex) {
            throw new NumberFormatException(value);
        }
        int result = resolveDigitByCode(value.charAt(beginIndex));
        for (int i = beginIndex + 1; i < endIndex; ++i) {
            result = result * 10 + resolveDigitByCode(value.charAt(i));
        }
        return result;
    }

    private static int resolveDigitByCode(char c) {
        final int result = c - '0';
        if (result < 0 || result > 9) {
            throw new NumberFormatException("Invalid digit: " + c);
        }
        return result;
    }

    private static void checkOffset(String value, int offset, char expected) throws IndexOutOfBoundsException {
        char found = value.charAt(offset);
        if (found != expected) {
            throw new IndexOutOfBoundsException("Expected '" + expected + "' character but found '" + found + "'");
        }
    }

    private static LocalDateTime parseIso8601AsLocalDateTime(String date, char delimiter, ParsingContext context) {
        final int length = date.length();
        int offset = context.offset;

        // extract year
        int year = parseInt(date, offset, offset += 4, length);
        checkOffset(date, offset, '-');

        // extract month
        int month = parseInt(date, offset += 1, offset += 2, length);
        checkOffset(date, offset, '-');

        // extract day
        int day = parseInt(date, offset += 1, offset += 2, length);
        checkOffset(date, offset, delimiter);

        // extract hours, minutes, seconds and milliseconds
        int hour = parseInt(date, offset += 1, offset += 2, length);
        checkOffset(date, offset, ':');

        int minutes = parseInt(date, offset += 1, offset += 2, length);
        checkOffset(date, offset, ':');

        int seconds = parseInt(date, offset += 1, offset += 2, length);
        // milliseconds can be optional in the format
        final int nanos;
        if (offset < length && date.charAt(offset) == '.') {
            final int startPos = ++offset;
            final int endPosExcl = Math.min(offset + 9, length);
            int frac = resolveDigitByCode(date.charAt(offset++));
            while (offset < endPosExcl) {
                final int digit = date.charAt(offset) - '0';
                if (digit < 0 || digit > 9) {
                    break;
                }
                frac = frac * 10 + digit;
                ++offset;
            }
            nanos = parseNanos(frac, offset - startPos);
        } else {
            nanos = 0;
        }
        context.offset = offset;
        return LocalDateTime.of(year, month, day, hour, minutes, seconds, nanos);
    }

    public static OffsetDateTime parseIso8601AsOffsetDateTime(String date, char delimiter) {
        try {
            final ParsingContext parsingContext = new ParsingContext();
            final LocalDateTime localDateTime = parseIso8601AsLocalDateTime(date, delimiter, parsingContext);
            final ZoneOffset zoneOffset = parseOffset(parsingContext.offset, date);
            return OffsetDateTime.of(localDateTime, zoneOffset);
        } catch (RuntimeException e) {
            throw new IllegalArgumentException("Failed to parse date " + date, e);
        }
    }

    private static ZoneOffset parseOffset(int offset, String date) {
        final int length = date.length();
        final ZoneOffset zoneOffset;
        if (offset == length) {
            throw new IllegalStateException("Zone offset required");
        } else {
            if (offset == length - 1 && date.charAt(offset) == 'Z') {
                zoneOffset = ZoneOffset.UTC;
            } else {
                zoneOffset = ZoneOffset.of(date.substring(offset));
            }
        }
        return zoneOffset;
    }

    static StringBuilder appendformattedSecondOffset(int offsetSeconds, StringBuilder sb) {
        if (offsetSeconds == 0) {
            return sb.append('Z');
        }
        sb.append(offsetSeconds < 0 ? '-' : '+');
        final int absSeconds = Math.abs(offsetSeconds);
        adjust(sb, absSeconds / 3600, 2);
        adjust(sb, (absSeconds / 60) % 60, 2);
        return sb;
    }

    /**
     * Return number of digits in base-10 string representation.
     * @param number Non-negative number
     * @return number of digits
     */
    @SuppressWarnings("squid:S3776") // cognitive complexity
    private static int sizeInDigits(int number) {
        final int result;
        if (number < 100_000) {
            if (number < 100) {
                result = number < 10 ? 1 : 2;
            } else {
                if (number < 1000) {
                    result = 3;
                } else {
                    result = number < 10_000 ? 4 : 5;
                }
            }
        } else {
            if (number < 10_000_000) {
                result = number < 1_000_000 ? 6 : 7;
            } else {
                if (number < 100_000_000) {
                    result = 8;
                } else {
                    result = number < 1_000_000_000 ? 9 : 10;
                }
            }
        }
        return result;
    }

    @SuppressWarnings("all") // ignore static analysis for Guava code
    private static int powerOfTen(int pow) {
        switch (pow) {
            case 0: return 1;
            case 1: return 10;
            case 2: return 100;
            case 3: return 1_000;
            case 4: return 10_000;
            case 5: return 100_000;
            case 6: return 1_000_000;
            case 7: return 10_000_000;
            case 8: return 100_000_000;
            case 9: return 1_000_000_000;
        }
        for (int accum = 1, b = 10;; pow >>= 1) {
            if (pow == 1) {
                return b * accum;
            } else {
                accum *= ((pow & 1) == 0) ? 1 : b;
                b *= b;
            }
        }
    }

    private static StringBuilder adjustPossiblyNegative(StringBuilder sb, int num, int positions) {
        if (num >= 0) {
            return adjust(sb, num, positions);
        }
        return adjust(sb.append('-'), -num, positions - 1);

    }

    static StringBuilder adjust(StringBuilder sb, int num, int positions) {
        for (int i = positions - sizeInDigits(num); i > 0; --i) {
            sb.append('0');
        }
        return sb.append(num);
    }

    public static ZonedDateTime timestampToZonedDateTime(long timestamp, ZoneId zoneId) {
        return Instant.ofEpochMilli(timestamp).atZone(zoneId);
    }

    public static long toMillis(ZonedDateTime zonedDateTime) {
        return zonedDateTime.toInstant().toEpochMilli();
    }

    static boolean isNumeric(final CharSequence cs) {
        final int sz = cs.length();
        for (int i = 0; i < sz; i++) {
            if (!Character.isDigit(cs.charAt(i))) {
                return false;
            }
        }
        return true;
    }

    /**
     * 

Checks whether the String a valid Java number.

* *

Valid numbers include hexadecimal marked with the 0x or * 0X qualifier, octal numbers, scientific notation and * numbers marked with a type qualifier (e.g. 123L).

* *

Non-hexadecimal strings beginning with a leading zero are * treated as octal values. Thus the string 09 will return * false, since 9 is not a valid octal value. * However, numbers beginning with {@code 0.} are treated as decimal.

* *

null and empty/blank {@code String} will return * false.

* * @param str the String to check * @return true if the string is a correctly formatted number */ @SuppressWarnings("all") // ignore static analysis for Apache Commons code static boolean isCreatable(final String str) { if (str == null || str.length() == 0) { return false; } final char[] chars = str.toCharArray(); int sz = chars.length; boolean hasExp = false; boolean hasDecPoint = false; boolean allowSigns = false; boolean foundDigit = false; // deal with any possible sign up front final int start = chars[0] == '-' || chars[0] == '+' ? 1 : 0; if (sz > start + 1 && chars[start] == '0') { // leading 0 if (chars[start + 1] == 'x' || chars[start + 1] == 'X') { // leading 0x/0X int i = start + 2; if (i == sz) { return false; // str == "0x" } // checking hex (it can't be anything else) for (; i < chars.length; i++) { if ((chars[i] < '0' || chars[i] > '9') && (chars[i] < 'a' || chars[i] > 'f') && (chars[i] < 'A' || chars[i] > 'F')) { return false; } } return true; } else if (Character.isDigit(chars[start + 1])) { // leading 0, but not hex, must be octal int i = start + 1; for (; i < chars.length; i++) { if (chars[i] < '0' || chars[i] > '7') { return false; } } return true; } } sz--; // don't want to loop to the last char, check it afterwords // for type qualifiers int i = start; // loop to the next to last char or to the last char if we need another digit to // make a valid number (e.g. chars[0..5] = "1234E") while (i < sz || i < sz + 1 && allowSigns && !foundDigit) { if (chars[i] >= '0' && chars[i] <= '9') { foundDigit = true; allowSigns = false; } else if (chars[i] == '.') { if (hasDecPoint || hasExp) { // two decimal points or dec in exponent return false; } hasDecPoint = true; } else if (chars[i] == 'e' || chars[i] == 'E') { // we've already taken care of hex. if (hasExp) { // two E's return false; } if (!foundDigit) { return false; } hasExp = true; allowSigns = true; } else if (chars[i] == '+' || chars[i] == '-') { if (!allowSigns) { return false; } allowSigns = false; foundDigit = false; // we need a digit after the E } else { return false; } i++; } if (i < chars.length) { if (chars[i] >= '0' && chars[i] <= '9') { // no type qualifier, OK return true; } if (chars[i] == 'e' || chars[i] == 'E') { // can't have an E at the last byte return false; } if (chars[i] == '.') { if (hasDecPoint || hasExp) { // two decimal points or dec in exponent return false; } // single trailing decimal point after non-exponent is ok return foundDigit; } if (!allowSigns && (chars[i] == 'd' || chars[i] == 'D' || chars[i] == 'f' || chars[i] == 'F')) { return foundDigit; } if (chars[i] == 'l' || chars[i] == 'L') { // not allowing L with an exponent or decimal point return foundDigit && !hasExp && !hasDecPoint; } // last character is illegal return false; } // allowSigns is true iff the val ends in 'E' // found digit it to make sure weird stuff like '.' and '1E-' doesn't pass return !allowSigns && foundDigit; } private static final class ParsingContext { private int offset; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy