ExpressionBuilder.java

/*
 * Copyright 2014 Frank Asseg
 *
 * 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 net.objecthunter.exp4j;

import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
import java.util.regex.Pattern;
import net.objecthunter.exp4j.function.Function;
import net.objecthunter.exp4j.function.Functions;
import net.objecthunter.exp4j.operator.Operator;
import net.objecthunter.exp4j.shuntingyard.ShuntingYard;
import net.objecthunter.exp4j.tokenizer.Token;

import static net.objecthunter.exp4j.utils.Text.l10n;

/**
 * Factory class for {@link Expression} instances. This class is the main API entrypoint.
 * Users should create new {@link Expression} instances using this factory class.
 */
public class ExpressionBuilder {
    private static final Pattern VAR_NAME_PATTERN = Pattern.compile("[\\s+\\-*/%^!#§$&:~<>|=¬]+");

    private final String expression;

    private final Map<String, Function> userFunctions;

    private final Map<String, Operator> userOperators;

    private final Set<String> variableNames;

    private boolean useBuiltInFunctions = true;

    /**
     * Create a new ExpressionBuilder instance and initialize it with a given expression
     * string.
     * @param expression the expression to be parsed
     */
    public ExpressionBuilder(String expression) {
        if (expression == null || expression.trim().length() == 0) {
            throw new IllegalArgumentException(l10n("Expression can not be empty"));
        }

        this.expression = expression;
        this.userOperators = new TreeMap<>();
        this.userFunctions = new TreeMap<>();
        this.variableNames = new HashSet<>(4);
    }

    /**
     * Removes all the built-in functions
     * @return the ExpressionBuilder instance
     */
    public ExpressionBuilder disableBuiltInFunctions() {
        useBuiltInFunctions = false;
        return this;
    }

    /**
     * Add a {@link Function} implementation available for use in the expression.
     * @param function the custom {@link Function} implementation that should be available for
     * use in the expression.
     * @return the ExpressionBuilder instance
     */
    public ExpressionBuilder function(Function function) {
        this.userFunctions.put(function.getName(), function);
        return this;
    }

    /**
     * Add multiple {@link Function} implementations
     * available for use in the expression.
     * @param functions the custom {@link Function} implementations
     * @return the ExpressionBuilder instance
     */
    public ExpressionBuilder functions(Function... functions) {
        for (Function f : functions) {
            this.userFunctions.put(f.getName(), f);
        }
        return this;
    }

    /**
     * Add multiple {@link net.objecthunter.exp4j.function.Function} implementations
     * available for use in the expression
     * @param functions A {@link java.util.List} of custom {@link Function} implementations
     * @return the ExpressionBuilder instance
     */
    public ExpressionBuilder functions(List<Function> functions) {
        for (Function f : functions) {
            this.userFunctions.put(f.getName(), f);
        }
        return this;
    }

    /**
     * Add multiple {@code variables} that <b>must</b> be used in the expression.<br><br>
     * <i><b>Note:</b></i> the "must" part of that statement will change on future versions.
     *
     * @param variableNames variables to use
     * @return the ExpressionBuilder instance
     * @throws IllegalArgumentException if the variable name contains spaces or
     * operator characters
     */
    public ExpressionBuilder variables(Set<String> variableNames) {
        for (String variableName : variableNames) {
            variable(variableName);
        }
        return this;
    }

    /**
     * Add multiple {@code variables} that <b>must</b> be used in the expression.<br><br>
     * <i><b>Note:</b></i> the "must" part of that statement will change on future versions.
     *
     * @param variableNames variables to use
     * @return the ExpressionBuilder instance
     * @throws IllegalArgumentException if the variable name contains spaces or
     * operator characters
     */
    public ExpressionBuilder variables(String ... variableNames) {
        for (String variableName : variableNames) {
            variable(variableName);
        }
        return this;
    }

    /**
     * Add a {@code variable} that <b>must</b> be used in the expression.<br><br>
     * <i><b>Note:</b></i> the "must" part of that statement will change on future versions.
     *
     * @param variableName variable to use
     * @return the ExpressionBuilder instance
     * @throws IllegalArgumentException if the variable name contains spaces or
     * operator characters
     */
    public ExpressionBuilder variable(String variableName) {
        checkVariableName(variableName);
        this.variableNames.add(variableName);
        return this;
    }

    /**
     * Add an {@link Operator} which should be available for use in the expression
     * @param operator the custom {@link Operator} to add
     * @return the ExpressionBuilder instance
     */
    public ExpressionBuilder operator(Operator operator) {
        this.checkOperatorSymbol(operator);
        this.userOperators.put(operator.getSymbol(), operator);
        return this;
    }

    private void checkOperatorSymbol(Operator op) {
        String name = op.getSymbol();
        for (char ch : name.toCharArray()) {
            if (!Operator.isAllowedOperatorChar(ch)) {
                throw new IllegalArgumentException(l10n(
                    "The operator symbol '%s' is invalid", name
                ));
            }
        }
    }

    /**
     * Add multiple {@link Operator} implementations
     * which should be available for use in the expression
     * @param operators the set of custom {@link Operator} implementations to add
     * @return the ExpressionBuilder instance
     */
    public ExpressionBuilder operators(Operator... operators) {
        for (Operator o : operators) {
            this.operator(o);
        }
        return this;
    }

    /**
     * Add multiple {@link Operator} implementations which should be available for use
     * in the expression
     * @param operators the {@link List} of custom {@link Operator} implementations to add
     * @return the ExpressionBuilder instance
     */
    public ExpressionBuilder operators(List<Operator> operators) {
        for (Operator o : operators) {
            this.operator(o);
        }
        return this;
    }

    /**
     * Build the {@link Expression} instance using the custom operators and functions set.
     * @return an {@link Expression} instance which can be used to evaluate the result of the
     * expression
     */
    public Expression build() {
        return build(false);
    }

    /**
     * Build the {@link Expression} instance using the custom operators and functions set.
     * @param simplify {@code true} if you want to attempt to simplify constants {@code false}
     * otherwise
     * @return an {@link Expression} instance which can be used to evaluate the result of the
     * expression
     */
    public Expression build(boolean simplify) {
        /* Check if there are duplicate vars/functions */
        for (String var : variableNames) {
            if (Functions.getBuiltinFunction(var) != null || userFunctions.containsKey(var)) {
                throw new IllegalArgumentException(l10n(
                    "A variable can not have the same name as a function [%s]", var
                ));
            }
        }

        Token[] tokens = ShuntingYard.convertToRPN(
                simplify,
                expression,
                userFunctions,
                userOperators,
                variableNames,
                useBuiltInFunctions
        );

        return new Expression(tokens, userFunctions.keySet().toArray(new String[0]));
    }

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

    private static void checkVariableName(String variableName) throws IllegalArgumentException {
        if (VAR_NAME_PATTERN.matcher(variableName).matches()) {
            throw new IllegalArgumentException(l10n("Variable names can't contain non ASCII letters"));
        }
    }

}