hazelcast-master/hazelcast-sql/src/main/java/com/hazelcast/sql/impl/expression/string/SubstringFunction.java
/*
 * Copyright 2023 Hazelcast Inc.
 *
 * Licensed under the Hazelcast Community License (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://hazelcast.com/hazelcast-community-license
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.hazelcast.sql.impl.expression.string;

import com.hazelcast.jet.sql.impl.JetSqlSerializerHook;
import com.hazelcast.sql.impl.QueryException;
import com.hazelcast.sql.impl.expression.Expression;
import com.hazelcast.sql.impl.expression.ExpressionEvalContext;
import com.hazelcast.sql.impl.expression.TriExpression;
import com.hazelcast.sql.impl.expression.math.MathFunctionUtils;
import com.hazelcast.sql.impl.row.Row;
import com.hazelcast.sql.impl.type.QueryDataType;

public class SubstringFunction extends TriExpression<String> {
    public SubstringFunction() {
        // No-op
    }

    private SubstringFunction(Expression<?> input, Expression<?> start, Expression<?> length) {
        super(input, start, length);
    }

    public static SubstringFunction create(Expression<?> input, Expression<?> start, Expression<?> length) {
        return new SubstringFunction(input, start, length);
    }

    @SuppressWarnings({"checkstyle:CyclomaticComplexity", "checkstyle:NPathComplexity"})
    @Override
    public String eval(Row row, ExpressionEvalContext context) {
        // Get input
        String input;

        try {
            input = StringFunctionUtils.asVarchar(operand1, row, context);
        } catch (Exception e) {
            // Conversion to String failed. E.g. NPE on UserClass.toString()
            throw QueryException.dataException(
                "Failed to get value of input operand of SUBSTRING function: " + e.getMessage(), e
            );
        }

        if (input == null) {
            // NULL always yields NULL
            return null;
        }

        Integer start = MathFunctionUtils.asInt(operand2, row, context);

        if (start == null) {
            return null;
        }

        if (start < 1) {
            // Different databases provide different semantics on negative values. Oracle start counting
            // from the end, SQL Server starts from the beginning, and uses the value to calculate the
            // final length, etc.
            // These semantics are pretty complicated, so wi disallow it completely.
            throw QueryException.dataException("SUBSTRING \"start\" operand must be positive");
        }

        // In SQL start position is 1-based. Convert it to 0-based for Java.
        int adjustedStart = start - 1;

        if (adjustedStart >= input.length()) {
            // Start position is beyond the string length, e.g. SUBSTRING("abc", 4)
            return "";
        }

        Integer length;

        if (operand3 != null) {
            length = MathFunctionUtils.asInt(operand3, row, context);

            if (length == null) {
                return null;
            }
        } else {
            length = null;
        }

        if (length == null) {
            // Length is not specified, just cut from the start
            return adjustedStart > 0 ? input.substring(adjustedStart) : input;
        } else if (length < 0) {
            // Negative lengths are not allowed
            throw QueryException.dataException("SUBSTRING \"length\" operand cannot be negative");
        } else if (length == 0) {
            // Zero length always yields empty string
            return "";
        } else if (adjustedStart + length > input.length()) {
            // Length is outside of input limits, ignore
            return adjustedStart > 0 ? input.substring(adjustedStart) : input;
        } else {
            // Length is within input limits, use it
            return input.substring(adjustedStart, adjustedStart + length);
        }
    }

    @Override
    public QueryDataType getType() {
        return QueryDataType.VARCHAR;
    }

    @Override
    public int getClassId() {
        return JetSqlSerializerHook.EXPRESSION_SUBSTRING;
    }
}
