TimeV2Literal.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.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.TimeV2Type;

import java.time.LocalDateTime;

/**
 * Time literal in Nereids.
 */
public class TimeV2Literal extends Literal {
    private static final LocalDateTime START_OF_A_DAY = LocalDateTime.of(0, 1, 1, 0, 0, 0);
    private static final LocalDateTime END_OF_A_DAY = LocalDateTime.of(9999, 12, 31, 23, 59, 59, 999999000);
    private static final TimeV2Literal MIN_VALUE = new TimeV2Literal(838, 59, 59, 999999, 6, true);
    private static final TimeV2Literal MAX_VALUE = new TimeV2Literal(838, 59, 59, 999999, 6, false);

    protected int hour;
    protected int minute;
    protected int second;
    protected int microsecond;
    protected boolean negative;

    public TimeV2Literal(TimeV2Type dataType, String s) {
        super(dataType);
        init(s);
    }

    /**
     * C'tor time literal.
     */
    public TimeV2Literal(double value) throws AnalysisException {
        super(TimeV2Type.of(6));
        if (value > (double) MAX_VALUE.getValue() || value < (double) MIN_VALUE.getValue()) {
            throw new AnalysisException("The value " + value + " is out of range, expect value range is ["
                    + (double) MIN_VALUE.getValue() + ", " + (double) MAX_VALUE.getValue() + "]");
        }
        this.negative = value < 0;
        long v = (long) Math.abs(value);
        this.microsecond = (int) (v % 1000000);
        v /= 1000000;
        this.second = (int) (v % 60);
        v /= 60;
        this.minute = (int) (v % 60);
        v /= 60;
        this.hour = (int) v;
    }

    /**
     * C'tor for time type.
     */
    // for -00:... so we need explicite negative
    public TimeV2Literal(int hour, int minute, int second, int microsecond, int scale, boolean negative)
            throws AnalysisException {
        super(TimeV2Type.of(scale));
        this.hour = hour;
        this.minute = minute;
        this.second = second;
        this.microsecond = (int) (microsecond / Math.pow(10, 6 - scale)) * (int) Math.pow(10, 6 - scale);
        this.negative = negative;
        if (checkRange(this.hour, this.minute, this.second, this.microsecond) || scale > 6 || scale < 0) {
            throw new AnalysisException("time literal is out of range [-838:59:59.999999, 838:59:59.999999]");
        }
    }

    protected String normalize(String s) {
        // remove suffix/prefix ' '
        s = s.trim();
        if (s.charAt(0) == '-') {
            s = s.substring(1);
            negative = true;
        } else if (s.charAt(0) == '+') {
            s = s.substring(1);
            negative = false;
        }
        // just a number
        if (!s.contains(":")) {
            String tail = "";
            if (s.contains(".")) {
                tail = s.substring(s.indexOf("."));
                s = s.substring(0, s.indexOf("."));
            }
            int len = s.length();
            if (len == 1) {
                s = "00:00:0" + s;
            } else if (len == 2) {
                s = "00:00:" + s;
            } else if (len == 3) {
                s = "00:0" + s.charAt(0) + ":" + s.substring(1);
            } else if (len == 4) {
                s = "00:" + s.substring(0, 2) + ":" + s.substring(2);
            } else {
                s = s.substring(0, len - 4) + ":" + s.substring(len - 4, len - 2) + ":" + s.substring(len - 2);
            }
            return s + tail;
        }
        // s maybe just contail 1 ":" like "12:00" so append a ":00" to the end
        if (s.indexOf(':') == s.lastIndexOf(':')) {
            s = s + ":00";
        }
        return s;
    }

    // should like be/src/vec/runtime/time_value.h timev2_to_double_from_str
    protected void init(String s) throws AnalysisException {
        s = normalize(s);
        // start parse string
        String[] parts = s.split(":");
        if (parts.length != 3) {
            throw new AnalysisException("Invalid format, must have 3 parts separated by ':'");
        }
        try {
            hour = Integer.parseInt(parts[0]);
        } catch (NumberFormatException e) {
            throw new AnalysisException("Invalid hour format", e);
        }

        try {
            minute = Integer.parseInt(parts[1]);
        } catch (NumberFormatException e) {
            throw new AnalysisException("Invalid minute format", e);
        }
        int scale = ((TimeV2Type) dataType).getScale();
        // if parts[2] is 60.000 it will cause judge feed execute error
        if (parts[2].startsWith("60")) {
            throw new AnalysisException("second out of range");
        }
        double secPart;
        try {
            secPart = Double.parseDouble(parts[2]);
        } catch (NumberFormatException e) {
            throw new AnalysisException("Invalid second format", e);
        }
        secPart = secPart * (int) Math.pow(10, scale);
        secPart = Math.round(secPart);
        secPart = (long) secPart * (long) Math.pow(10, 6 - scale);
        second = (int) (secPart / 1000000);
        if (scale != 0) {
            microsecond = (int) (secPart % 1000000);
            if (second == 60) {
                minute += 1;
                second -= 60;
                if (minute == 60) {
                    hour += 1;
                    minute -= 60;
                }
            }
        } else {
            microsecond = 0;
        }

        if (checkRange(hour, minute, second, microsecond)) {
            throw new AnalysisException("time literal [" + s + "] is out of range");
        }
    }

    protected static boolean checkRange(double hour, int minute, int second, int microsecond) {
        return hour > 838 || minute > 59 || second > 59 || microsecond > 999999 || minute < 0 || second < 0
                || microsecond < 0;
    }

    public int getHour() {
        return hour;
    }

    public int getMinute() {
        return minute;
    }

    public int getSecond() {
        return second;
    }

    public int getMicroSecond() {
        return microsecond;
    }

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

    @Override
    public LiteralExpr toLegacyLiteral() {
        int scale = ((TimeV2Type) dataType).getScale();
        return new org.apache.doris.analysis.TimeV2Literal(hour, minute, second, microsecond, scale, negative);
    }

    @Override
    public String getStringValue() {
        StringBuilder sb = new StringBuilder();
        if (negative) {
            sb.append("-");
        }
        if (hour > 99) {
            sb.append(String.format("%03d:%02d:%02d", hour, minute, second));
        } else {
            sb.append(String.format("%02d:%02d:%02d", hour, minute, second));
        }
        // why re caculate microsecond? example:
        // the microsecond is 001000, it will parsed to 1000
        // the scale is 3, we need make sure it not become start with 1
        int scale = ((TimeV2Type) dataType).getScale();
        if (scale > 0) {
            sb.append(String.format(".%0" + scale + "d", microsecond / (int) Math.pow(10, 6 - scale)));
        }
        return sb.toString();
    }

    public static boolean isDateOutOfRange(LocalDateTime dateTime) {
        return dateTime == null || dateTime.isBefore(START_OF_A_DAY) || dateTime.isAfter(END_OF_A_DAY);
    }

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

    public LocalDateTime toJavaDateType() {
        return LocalDateTime.of(0, 1, 1, ((int) getHour()), ((int) getMinute()), ((int) getSecond()),
                (int) getMicroSecond() * 1000);
    }

    @Override
    public Object getValue() {
        if (negative) {
            return (((double) (-hour * 60) - minute) * 60 - second) * 1000000 - microsecond;
        }
        return (((double) (hour * 60) + minute) * 60 + second) * 1000000 + microsecond;
    }

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