DateFormatParser.java

/*
 * Licensed 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 com.facebook.presto.teradata.functions.dateformat;

import com.facebook.presto.spi.PrestoException;
import com.facebook.presto.spi.StandardErrorCode;
import com.facebook.presto.teradata.functions.DateFormat;
import org.antlr.v4.runtime.ANTLRInputStream;
import org.antlr.v4.runtime.Token;

import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.util.List;

import static com.facebook.presto.spi.StandardErrorCode.INVALID_FUNCTION_ARGUMENT;
import static java.time.format.SignStyle.NOT_NEGATIVE;
import static java.time.temporal.ChronoField.AMPM_OF_DAY;
import static java.time.temporal.ChronoField.DAY_OF_MONTH;
import static java.time.temporal.ChronoField.HOUR_OF_AMPM;
import static java.time.temporal.ChronoField.HOUR_OF_DAY;
import static java.time.temporal.ChronoField.MINUTE_OF_HOUR;
import static java.time.temporal.ChronoField.MONTH_OF_YEAR;
import static java.time.temporal.ChronoField.SECOND_OF_MINUTE;
import static java.time.temporal.ChronoField.YEAR;

public class DateFormatParser
{
    public enum Mode
    {
        // Do not require leading zero for parsing two position date fields (MM, DD, HH, HH24, MI, SS)
        // E.g. "to_timestamp('1988/4/8 2:3:4','yyyy/mm/dd hh24:mi:ss')"
        PARSER(1),

        // Add leading zero for formatting single valued two position date fields (MM, DD, HH, HH24, MI, SS)
        // E.g. "to_char(TIMESTAMP '1988-4-8 2:3:4','yyyy/mm/dd hh24:mi:ss')" evaluates to "1988/04/08 02:03:04"
        FORMATTER(2);

        private final int minTwoPositionFieldWidth;

        public int getMinTwoPositionFieldWidth()
        {
            return minTwoPositionFieldWidth;
        }

        Mode(int value)
        {
            this.minTwoPositionFieldWidth = value;
        }
    }

    private DateFormatParser()
    {
    }

    public static DateTimeFormatter createDateTimeFormatter(String format, Mode mode)
    {
        DateTimeFormatterBuilder builder = new DateTimeFormatterBuilder();
        boolean formatContainsHourOfAMPM = false;
        for (Token token : tokenize(format)) {
            switch (token.getType()) {
                case DateFormat.TEXT:
                    builder.appendLiteral(token.getText());
                    break;
                case DateFormat.DD:
                    builder.appendValue(DAY_OF_MONTH, mode.getMinTwoPositionFieldWidth(), 2, NOT_NEGATIVE);
                    break;
                case DateFormat.HH24:
                    builder.appendValue(HOUR_OF_DAY, mode.getMinTwoPositionFieldWidth(), 2, NOT_NEGATIVE);
                    break;
                case DateFormat.HH:
                    builder.appendValue(HOUR_OF_AMPM, mode.getMinTwoPositionFieldWidth(), 2, NOT_NEGATIVE);
                    formatContainsHourOfAMPM = true;
                    break;
                case DateFormat.MI:
                    builder.appendValue(MINUTE_OF_HOUR, mode.getMinTwoPositionFieldWidth(), 2, NOT_NEGATIVE);
                    break;
                case DateFormat.MM:
                    builder.appendValue(MONTH_OF_YEAR, mode.getMinTwoPositionFieldWidth(), 2, NOT_NEGATIVE);
                    break;
                case DateFormat.SS:
                    builder.appendValue(SECOND_OF_MINUTE, mode.getMinTwoPositionFieldWidth(), 2, NOT_NEGATIVE);
                    break;
                case DateFormat.YY:
                    builder.appendValueReduced(YEAR, 2, 2, 2000);
                    break;
                case DateFormat.YYYY:
                    builder.appendValue(YEAR, 4);
                    break;
                case DateFormat.UNRECOGNIZED:
                default:
                    throw new PrestoException(
                            StandardErrorCode.INVALID_FUNCTION_ARGUMENT,
                            String.format("Failed to tokenize string [%s] at offset [%d]", token.getText(), token.getCharPositionInLine()));
            }
        }
        try {
            // Append default values(0) for time fields(HH24, HH, MI, SS) because JSR-310 does not accept bare Date value as DateTime

            if (formatContainsHourOfAMPM) {
                // At the moment format does not allow to include AM/PM token, thus it was never possible to specify PM hours using 'HH' token in format
                // Keep existing behaviour by defaulting to 0(AM) for AMPM_OF_DAY if format string contains 'HH'
                builder.parseDefaulting(HOUR_OF_AMPM, 0)
                        .parseDefaulting(AMPM_OF_DAY, 0);
            }
            else {
                builder.parseDefaulting(HOUR_OF_DAY, 0);
            }

            return builder.parseDefaulting(MINUTE_OF_HOUR, 0)
                    .parseDefaulting(SECOND_OF_MINUTE, 0)
                    .toFormatter();
        }
        catch (UnsupportedOperationException e) {
            throw new PrestoException(INVALID_FUNCTION_ARGUMENT, e);
        }
    }

    public static List<? extends Token> tokenize(String format)
    {
        DateFormat lexer = new com.facebook.presto.teradata.functions.DateFormat(new ANTLRInputStream(format));
        return lexer.getAllTokens();
    }
}