DateTimeLiteral.java

// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements.  See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership.  The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License.  You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied.  See the License for the
// specific language governing permissions and limitations
// under the License.

package org.apache.doris.nereids.trees.expressions.literal;

import org.apache.doris.analysis.LiteralExpr;
import org.apache.doris.catalog.Type;
import org.apache.doris.nereids.exceptions.AnalysisException;
import org.apache.doris.nereids.trees.expressions.Expression;
import org.apache.doris.nereids.trees.expressions.visitor.ExpressionVisitor;
import org.apache.doris.nereids.types.DateTimeType;
import org.apache.doris.nereids.types.DateTimeV2Type;
import org.apache.doris.nereids.types.coercion.DateLikeType;
import org.apache.doris.nereids.util.DateUtils;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.temporal.ChronoField;
import java.time.temporal.TemporalAccessor;
import java.time.temporal.TemporalQueries;
import java.util.Objects;

/**
 * date time literal.
 */
public class DateTimeLiteral extends DateLiteral {
    public static final DateTimeLiteral MIN_DATETIME = new DateTimeLiteral(0000, 1, 1, 0, 0, 0);
    public static final DateTimeLiteral MAX_DATETIME = new DateTimeLiteral(9999, 12, 31, 23, 59, 59);
    protected static final int MAX_MICROSECOND = 999999;

    private static final Logger LOG = LogManager.getLogger(DateTimeLiteral.class);

    protected long hour;
    protected long minute;
    protected long second;
    protected long microSecond;

    public DateTimeLiteral(String s) {
        this(DateTimeType.INSTANCE, s);
    }

    protected DateTimeLiteral(DateLikeType dataType, String s) {
        super(dataType);
        init(s);
    }

    /**
     * C'tor data time literal.
     */
    public DateTimeLiteral(long year, long month, long day, long hour, long minute, long second) {
        this(DateTimeType.INSTANCE, year, month, day, hour, minute, second);
    }

    /**
     * C'tor data time literal.
     */
    public DateTimeLiteral(DateLikeType dataType, long year, long month, long day,
            long hour, long minute, long second) {
        this(dataType, year, month, day, hour, minute, second, 0L);
    }

    /**
     * C'tor data time literal.
     */
    public DateTimeLiteral(DateLikeType dataType, long year, long month, long day,
            long hour, long minute, long second, long microSecond) {
        super(dataType);
        this.hour = hour;
        this.minute = minute;
        this.second = second;
        this.microSecond = microSecond;
        this.year = year;
        this.month = month;
        this.day = day;
    }

    public boolean isMidnight() {
        return hour == 0 && minute == 0 && second == 0 && microSecond == 0;
    }

    /**
     * determine scale by datetime string
     */
    public static int determineScale(String s) {
        if (!s.contains("-") && !s.contains(":")) {
            return 0;
        }
        // means basic format with timezone
        if (s.indexOf("-") == s.lastIndexOf("-") && s.indexOf(":") == s.lastIndexOf(":")) {
            return 0;
        }
        s = normalize(s).get();
        if (s.length() <= 19 || s.charAt(19) != '.') {
            return 0;
        }
        // from index 19 find the index of first char which is not digit
        int scale = 0;
        for (int i = 20; i < s.length(); i++) {
            if (!Character.isDigit(s.charAt(i))) {
                break;
            }
            scale++;
        }
        // trim the tailing zero
        for (int i = 19 + scale; i >= 19; i--) {
            if (s.charAt(i) != '0') {
                break;
            }
            scale--;
        }
        return scale;
    }

    /** parseDateTimeLiteral */
    public static Result<DateTimeLiteral, AnalysisException> parseDateTimeLiteral(String s, boolean isV2) {
        Result<TemporalAccessor, AnalysisException> parseResult = parseDateTime(s);
        if (parseResult.isError()) {
            return parseResult.cast();
        }

        TemporalAccessor temporal = parseResult.get();
        long year = DateUtils.getOrDefault(temporal, ChronoField.YEAR);
        long month = DateUtils.getOrDefault(temporal, ChronoField.MONTH_OF_YEAR);
        long day = DateUtils.getOrDefault(temporal, ChronoField.DAY_OF_MONTH);
        long hour = DateUtils.getOrDefault(temporal, ChronoField.HOUR_OF_DAY);
        long minute = DateUtils.getOrDefault(temporal, ChronoField.MINUTE_OF_HOUR);
        long second = DateUtils.getOrDefault(temporal, ChronoField.SECOND_OF_MINUTE);

        ZoneId zoneId = temporal.query(TemporalQueries.zone());
        if (zoneId != null) {
            // get correct DST of that time.
            Instant thatTime = ZonedDateTime
                    .of((int) year, (int) month, (int) day, (int) hour, (int) minute, (int) second, 0, zoneId)
                    .toInstant();

            int offset = DateUtils.getTimeZone().getRules().getOffset(thatTime).getTotalSeconds()
                    - zoneId.getRules().getOffset(thatTime).getTotalSeconds();
            if (offset != 0) {
                DateTimeLiteral tempLiteral = new DateTimeLiteral(year, month, day, hour, minute, second);
                DateTimeLiteral result = (DateTimeLiteral) tempLiteral.plusSeconds(offset);
                second = result.second;
                minute = result.minute;
                hour = result.hour;
                day = result.day;
                month = result.month;
                year = result.year;
            }
        }

        long microSecond = DateUtils.getOrDefault(temporal, ChronoField.NANO_OF_SECOND) / 100L;
        // Microseconds have 7 digits.
        long sevenDigit = microSecond % 10;
        microSecond = microSecond / 10;
        if (sevenDigit >= 5 && isV2) {
            DateTimeV2Literal tempLiteral = new DateTimeV2Literal(year, month, day, hour, minute, second, microSecond);
            DateTimeV2Literal result = (DateTimeV2Literal) tempLiteral.plusMicroSeconds(1);
            second = result.second;
            minute = result.minute;
            hour = result.hour;
            day = result.day;
            month = result.month;
            year = result.year;
            microSecond = result.microSecond;
        }

        if (checkRange(year, month, day) || checkDate(year, month, day)) {
            return Result.err(() -> new AnalysisException("datetime literal [" + s + "] is out of range"));
        }

        if (isV2) {
            DateTimeV2Type type = DateTimeV2Type.forTypeFromString(s);
            return Result.ok(new DateTimeV2Literal(type, year, month, day, hour, minute, second, microSecond));
        } else {
            return Result.ok(new DateTimeLiteral(DateTimeType.INSTANCE, year, month, day, hour, minute, second));
        }
    }

    protected void init(String s) throws AnalysisException {
        // TODO: check and do fast parse like fastParseDate
        TemporalAccessor temporal = parseDateTime(s).get();

        year = DateUtils.getOrDefault(temporal, ChronoField.YEAR);
        month = DateUtils.getOrDefault(temporal, ChronoField.MONTH_OF_YEAR);
        day = DateUtils.getOrDefault(temporal, ChronoField.DAY_OF_MONTH);
        hour = DateUtils.getOrDefault(temporal, ChronoField.HOUR_OF_DAY);
        minute = DateUtils.getOrDefault(temporal, ChronoField.MINUTE_OF_HOUR);
        second = DateUtils.getOrDefault(temporal, ChronoField.SECOND_OF_MINUTE);

        ZoneId zoneId = temporal.query(TemporalQueries.zone());
        if (zoneId != null) {
            // get correct DST of that time.
            Instant thatTime = ZonedDateTime
                    .of((int) year, (int) month, (int) day, (int) hour, (int) minute, (int) second, 0, zoneId)
                    .toInstant();

            int offset = DateUtils.getTimeZone().getRules().getOffset(thatTime).getTotalSeconds()
                    - zoneId.getRules().getOffset(thatTime).getTotalSeconds();
            if (offset != 0) {
                DateTimeLiteral result = (DateTimeLiteral) this.plusSeconds(offset);
                this.second = result.second;
                this.minute = result.minute;
                this.hour = result.hour;
                this.day = result.day;
                this.month = result.month;
                this.year = result.year;
            }
        }

        microSecond = DateUtils.getOrDefault(temporal, ChronoField.NANO_OF_SECOND) / 100L;
        // Microseconds have 7 digits.
        long sevenDigit = microSecond % 10;
        microSecond = microSecond / 10;
        if (sevenDigit >= 5 && this instanceof DateTimeV2Literal) {
            DateTimeV2Literal result = (DateTimeV2Literal) ((DateTimeV2Literal) this).plusMicroSeconds(1);
            this.second = result.second;
            this.minute = result.minute;
            this.hour = result.hour;
            this.day = result.day;
            this.month = result.month;
            this.year = result.year;
            this.microSecond = result.microSecond;
        }

        if (checkRange(year, month, day) || checkDate(year, month, day)) {
            throw new AnalysisException("datetime literal [" + s + "] is out of range");
        }
    }

    protected boolean checkRange() {
        return checkRange(year, month, day) || hour > MAX_DATETIME.getHour() || minute > MAX_DATETIME.getMinute()
                || second > MAX_DATETIME.getSecond() || microSecond > MAX_MICROSECOND;
    }

    @Override
    public <R, C> R accept(ExpressionVisitor<R, C> visitor, C context) {
        return visitor.visitDateTimeLiteral(this, context);
    }

    @Override
    public Long getValue() {
        return (year * 10000 + month * 100 + day) * 1000000L + hour * 10000 + minute * 100 + second;
    }

    @Override
    public double getDouble() {
        return (double) getValue();
    }

    @Override
    public String computeToSql() {
        return "'" + getStringValue() + "'";
    }

    @Override
    public String toString() {
        return getStringValue();
    }

    @Override
    public String getStringValue() {
        if (0 <= year && year <= 9999 && 0 <= month && month <= 99 && 0 <= day && day <= 99
                && 0 <= hour && hour <= 99 && 0 <= minute && minute <= 99 && 0 <= second && second <= 99) {
            char[] format = new char[] {
                    '0', '0', '0', '0', '-', '0', '0', '-', '0', '0', ' ', '0', '0', ':', '0', '0', ':', '0', '0'};
            int offset = 3;
            long year = this.year;
            while (year > 0) {
                format[offset--] = (char) ('0' + (year % 10));
                year /= 10;
            }

            offset = 6;
            long month = this.month;
            while (month > 0) {
                format[offset--] = (char) ('0' + (month % 10));
                month /= 10;
            }

            offset = 9;
            long day = this.day;
            while (day > 0) {
                format[offset--] = (char) ('0' + (day % 10));
                day /= 10;
            }

            offset = 12;
            long hour = this.hour;
            while (hour > 0) {
                format[offset--] = (char) ('0' + (hour % 10));
                hour /= 10;
            }

            offset = 15;
            long minute = this.minute;
            while (minute > 0) {
                format[offset--] = (char) ('0' + (minute % 10));
                minute /= 10;
            }

            offset = 18;
            long second = this.second;
            while (second > 0) {
                format[offset--] = (char) ('0' + (second % 10));
                second /= 10;
            }
            return String.valueOf(format);
        }
        return String.format("%04d-%02d-%02d %02d:%02d:%02d", year, month, day, hour, minute, second);
    }

    @Override
    public LiteralExpr toLegacyLiteral() {
        return new org.apache.doris.analysis.DateLiteral(year, month, day, hour, minute, second, Type.DATETIME);
    }

    public Expression plusDays(long days) {
        return fromJavaDateType(toJavaDateType().plusDays(days));
    }

    public Expression plusMonths(long months) {
        return fromJavaDateType(toJavaDateType().plusMonths(months));
    }

    public Expression plusWeeks(long weeks) {
        return fromJavaDateType(toJavaDateType().plusWeeks(weeks));
    }

    public Expression plusYears(long years) {
        return fromJavaDateType(toJavaDateType().plusYears(years));
    }

    public Expression plusHours(long hours) {
        return fromJavaDateType(toJavaDateType().plusHours(hours));
    }

    public Expression plusMinutes(long minutes) {
        return fromJavaDateType(toJavaDateType().plusMinutes(minutes));
    }

    public Expression plusSeconds(long seconds) {
        return fromJavaDateType(toJavaDateType().plusSeconds(seconds));
    }

    public long getHour() {
        return hour;
    }

    public long getMinute() {
        return minute;
    }

    public long getSecond() {
        return second;
    }

    public long getMicroSecond() {
        return microSecond;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        DateTimeLiteral other = (DateTimeLiteral) o;
        return Objects.equals(getValue(), other.getValue());
    }

    public LocalDateTime toJavaDateType() {
        return LocalDateTime.of(((int) getYear()), ((int) getMonth()), ((int) getDay()),
                ((int) getHour()), ((int) getMinute()), ((int) getSecond()), (int) getMicroSecond() * 1000);
    }

    public static Expression fromJavaDateType(LocalDateTime dateTime) {
        if (isDateOutOfRange(dateTime)) {
            throw new AnalysisException("datetime out of range: " + dateTime.toString());
        }
        return new DateTimeLiteral(dateTime.getYear(), dateTime.getMonthValue(), dateTime.getDayOfMonth(),
                        dateTime.getHour(), dateTime.getMinute(), dateTime.getSecond());
    }
}