All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.creativewidgetworks.expressionparser.Parser Maven / Gradle / Ivy

The newest version!
package com.creativewidgetworks.expressionparser;

import java.math.BigDecimal;
import java.math.MathContext;
import java.math.RoundingMode;
import java.text.ParseException;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class Parser {
    // Internal VO class to hold a function's argument count
    public class ArgCount { public boolean haveArgs; public int count; }

    // Default numeric precision (number of decimal places)
    public static final int DEFAULT_PRECISION = 5;

    // Maximum size of arrays that can be created by DIM
    public static int MAX_DIM_ROWS = 10000;
    public static int MAX_DIM_COLS = 256;

    // By default, disable access to system and environment properties
    private boolean allowProperties = false;

    // By default, use the JVM's timezone
    private TimeZone localTimeZone = TimeZone.getDefault();

    private static final String DEFAULT_SPLIT_CHARACTER = ";";
    private static final String SPLIT_REGEX = "(?=([^\\\"\\']*[\\\"\\'][^\\\"\\']*[\\\"\\'])*[^\\\"\\']*$)";

    // Number of digits of precision for math operations
    private int precision = DEFAULT_PRECISION;

    // RegEx tokenizer - package level for testing
    private boolean caseSensitive;
    private Pattern combinedPattern;
    private String expressionDelimiter;
    final Map> tokenizedExpressions = new HashMap<>();

    // Status
    private ParserException lastException;
    private String lastExpression;

    // Containers for constants, functions, and variables
    private Map constants = new HashMap<>();
    private Map functions = new HashMap<>();
    private Map globals = new TreeMap<>();
    private Map variables = new TreeMap<>();
    
    private FieldInterface fieldInterface;

    private boolean suppressParseExceptions;

    public Parser() {
        caseSensitive = false;
        expressionDelimiter = DEFAULT_SPLIT_CHARACTER;
        clearConstants();
        clearFunctions();
    }

    public Parser(Parser parser) {
        this();
        allowProperties = parser.allowProperties;
        expressionDelimiter = parser.expressionDelimiter;
        fieldInterface = parser.fieldInterface;
        localTimeZone = parser.localTimeZone;
        precision = parser.precision;
        suppressParseExceptions = parser.suppressParseExceptions;
        caseSensitive = parser.getCaseSensitive();
        constants = parser.getConstants();
        functions = parser.getFunctions();
        globals = parser.getGlobalVariables();
        variables = parser.getVariables();
    }

    /*----------------------------------------------------------------------------*/

    public boolean setAllowProperties(boolean allowProperties) {
        boolean orgAllowProperties = this.allowProperties;
        this.allowProperties = allowProperties;
        return orgAllowProperties;
    }

    /*----------------------------------------------------------------------------*/

    public TimeZone getTimeZone() {
        return localTimeZone;
    }

    public TimeZone setTimeZone(TimeZone timezone) {
        TimeZone orgTimeZone = this.localTimeZone;
        this.localTimeZone = timezone;
        return orgTimeZone;
    }

    /*----------------------------------------------------------------------------*/

    /**
     * Examine the stack (list of function parameters) looking for any whose value
     * is null. Returns null if all parameters are non-null or a comma-delimited
     * list of parameter numbers that are null.
     */
    public String listOfNullParameters(Stack stack) {
        return listOfNullParameters(stack, 0);
    }

    public String listOfNullParameters(Stack stack, int argCount) {
        StringBuilder sb = new StringBuilder();

        if (stack != null) {
            int offset = argCount == 0 ? 0 : stack.size() - argCount;
            for (int i = offset, p = 1; i < stack.size(); i++, p++) {
                if (stack.elementAt(i).getValue().asObject() == null) {
                    if (sb.length() > 0) {
                        sb.append(", ");
                    }
                    sb.append(p);
                }
            }
        }

        return sb.length() == 0 ? null : sb.toString();
    }

    /*----------------------------------------------------------------------------*/

    public void addConstant(String name, BigDecimal value) {
        if (name != null) {
            constants.put(caseSensitive ? name : name.toUpperCase(), value);
            invalidatePattern();
        }
    }

    public void clearConstant(String name) {
        constants.remove(caseSensitive ? name : name.toUpperCase());
        invalidatePattern();
    }

    public void clearConstants() {
        constants.clear();
        addConstant("null", null);
        addConstant("pi", BigDecimal.valueOf(Math.PI));
        invalidatePattern();
    }

    public BigDecimal getConstant(String name) {
        return name == null ? null : constants.get(caseSensitive ? name : name.toUpperCase());
    }

    public Map getConstants() {
        return constants;
    }

    public String getConstantRegex() {
        List names = new ArrayList<>();
        names.addAll(constants.keySet());

        // Sort in descending order to insure proper matching
        Collections.sort(names, Collections.reverseOrder());

        StringBuilder sb = new StringBuilder();
        for (String name : names) {
            if (sb.length() > 0) {
                sb.append("|");
            }
            sb.append(name);
        }

        if (sb.length() == 0) {
            sb.append("~~no-constants-defined~~");
        }

        return sb.toString();
    }

    /*----------------------------------------------------------------------------*/

    public Value getField(String name) {
        if (fieldInterface != null) {
            return fieldInterface.getField(name, getCaseSensitive());
        } else {
            return null;
        }
    }

    public FieldInterface getFieldInterface() {
        return fieldInterface;
    }

    public FieldInterface setFieldInterface(FieldInterface fieldInterface) {
        FieldInterface oldValue = fieldInterface;
        this.fieldInterface = fieldInterface;
        return oldValue;
    }

    /*----------------------------------------------------------------------------*/

    public void addFunction(Function function) {
        if (function != null) {
            functions.put(caseSensitive ? function.getName() : function.getName().toUpperCase(), function);
            invalidatePattern();
        }
    }

    public void clearFunction(String name) {
        functions.remove(caseSensitive ? name : name.toUpperCase());
        invalidatePattern();
    }

    public void clearFunctions() {
        functions.clear();
        addFunction(new Function("clearGlobal", this, "_CLEARGLOBAL", 1, 1));
        addFunction(new Function("clearGlobals", this, "_CLEARGLOBALS", 0, 0));
        addFunction(new Function("dim", this, "_DIM", 2, 3, ValueType.UNDEFINED, ValueType.NUMBER, ValueType.NUMBER));
        addFunction(new Function("getGlobal", this, "_GETGLOBAL", 1, 1, ValueType.STRING));
        addFunction(new Function("setGlobal", this, "_SETGLOBAL", 2, 2, ValueType.STRING));
        addFunction(new Function("now", this, "_NOW", 0, 1));
        addFunction(new Function("precision", this, "_PRECISION", 1, 1, ValueType.NUMBER));
        invalidatePattern();
    }

    /*---------------------------------------------------------------------------------*/

    /**
     * Returns a regex that will be used to parse OPERATOR tokens
     * @param parser instance using the TokenType
     * @return String, regex expression
     */
    public Pattern getPattern(Parser parser) {
        if (combinedPattern == null) {
            StringBuilder sb = new StringBuilder();
            for (TokenType tokenType : TokenType.values()) {
                sb.append(String.format("|(?<%s>%s)", tokenType.name(), tokenType.getRegex(parser)));
            }

            int options = parser.getCaseSensitive() ? Pattern.UNICODE_CASE : Pattern.UNICODE_CASE | Pattern.CASE_INSENSITIVE;
            options |= Pattern.UNICODE_CHARACTER_CLASS;
            combinedPattern = Pattern.compile(sb.substring(1), options);
        }
        return combinedPattern;
    }


    public void invalidatePattern() {
        combinedPattern = null;
    }

    /*---------------------------------------------------------------------------------*/

    public Function getFunction(String functionName) {
        return functionName == null ? null : functions.get(caseSensitive ? functionName : functionName.toUpperCase());
    }

    public Map getFunctions() {
        return functions;
    }

    public String getFunctionRegex() {
        List regexs = new ArrayList<>();
        for (Function function : functions.values()) {
            regexs.add(function.getName());
        }

        // Sort in descending order to insure proper matching
        Collections.sort(regexs, Collections.reverseOrder());

        StringBuilder sb = new StringBuilder();
        for (String regex : regexs) {
            if (sb.length() > 0) {
                sb.append("|");
            }
            sb.append(regex);
        }

        if (sb.length() == 0) {
            sb.append("~~no-functions-defined~~");
        }

        return sb.toString();
    }

    /*----------------------------------------------------------------------------*/

    private Object getProperty(String name) {
        if (allowProperties) {
            Object obj = System.getenv(name);
            return obj == null ? System.getProperties().get(name) : obj;
        } else {
            return null;
        }
    }

    /*----------------------------------------------------------------------------*/

    public void addGlobalVariable(String name, Value value) {
        if (name != null && value != null) {
            globals.put(caseSensitive ? name : name.toUpperCase(), value);
        }
    }

    public void clearGlobalVariable(String name) {
        if (name != null) {
            globals.remove(caseSensitive ? name : name.toUpperCase());
        }
    }

    public void clearGlobalVariables() {
        globals.clear();
    }

    public Value getGlobalVariable(String name) {
        return name == null ? null : globals.get(caseSensitive ? name : name.toUpperCase());
    }

    public Map getGlobalVariables() {
        return globals;
    }

    /*----------------------------------------------------------------------------*/

    public void addVariable(String name, Value value) {
        if (name != null && value != null) {
            variables.put(caseSensitive ? name : name.toUpperCase(), value);
        }
    }

    public void clearVariable(String name) {
        variables.remove(caseSensitive ? name : name.toUpperCase());
    }

    public void clearVariables() {
        variables.clear();
    }

    public Value getVariable(String name) {
        return name == null ? null : variables.get(caseSensitive ? name : name.toUpperCase());
    }

    public Map getVariables() {
        return variables;
    }


    /*----------------------------------------------------------------------------*/
    /*----------------------------------------------------------------------------*/
    /*----------------------------------------------------------------------------*/

    private int compareTokens(Token token1, Token token2, boolean caseSensitive) throws ParserException {
        if (!token1.isOperator()) {
            throw new ParserException(ParserException.formatMessage("error.operator_expected", token1.getType().name()));
        } else if (!token2.isOperator()) {
            throw new ParserException(ParserException.formatMessage("error.operator_expected_at_top", token1.getType().name()));
        }

        Operator o1 =  Operator.find(token1, caseSensitive);
        if (o1 == null) {
            throw new ParserException(ParserException.formatMessage("error.operator_not_found", token1.getText()));
        }

        Operator o2 =  Operator.find(token2, caseSensitive);
        if (o2 == null) {
            throw new ParserException(ParserException.formatMessage("error.operator_not_found", token2.getText()));
        }

        return  o1.getPrecedence() - o2.getPrecedence();
    }

    private boolean isType(Token token, int type, boolean caseSensitive) throws ParserException {
        if (!token.isOperator()) {
            throw new ParserException(ParserException.formatMessage("error.expected_operator_token", token.getText(), token.getType().name()));
        }

        Operator o = Operator.find(token, caseSensitive);
        if (o == null) {
            throw new ParserException(ParserException.formatMessage("error.operator_not_found", token.getText()));
        }

        return o.getAssociation() == type;
    }

    private boolean shouldPopToken(Token token, Token topOfStack, boolean caseSensitive) throws ParserException {
        // Unary minus/plus are handled differently if the top token is the exponentiation operator
        Operator op = Operator.find(token, caseSensitive);
        if (op.inSet(Operator.UNARY_MINUS, Operator.UNARY_PLUS) && Operator.EXP.getText().equals(topOfStack.getText())) {
            return false;
        } else {
            return
                    (isType(token, Operator.LEFT_ASSOCIATIVE, caseSensitive) && compareTokens(token, topOfStack, caseSensitive) >= 0) ||
                            (isType(token, Operator.RIGHT_ASSOCIATIVE, caseSensitive) && compareTokens(token, topOfStack, caseSensitive) > 0);
        }
    }

    /*----------------------------------------------------------------------------*/

    public void clearCache() {
        tokenizedExpressions.clear();
    }

    public ParserException getLastException() {
        return lastException;
    }

    public String getLastExpression() {
        return lastExpression;
    }

    /*----------------------------------------------------------------------------*/

    public boolean getCaseSensitive() {
        return caseSensitive;
    }

    public boolean setCaseSensitive(boolean caseSensitive) {
        boolean oldValue = this.caseSensitive;
        this.caseSensitive = caseSensitive;
        return oldValue;
    }

    /*----------------------------------------------------------------------------*/

    public int getPrecision() {
        return precision;
    }

    public int setPrecision(int decimals) {
        int oldValue = this.precision;
        this.precision = decimals;
        return oldValue;
    }

    /*----------------------------------------------------------------------------*/

    private void setStatusAndFail(Token currentToken, String message, Object... parameters) throws ParserException {
        int errorAtRow = currentToken == null ? -1 : currentToken.getRow();
        int errorAtCol = currentToken == null ? -1 : currentToken.getColumn();
        String errorMessage = ParserException.formatMessage(message, parameters);
        throw new ParserException(errorMessage, errorAtRow, errorAtCol);
    }

    /*----------------------------------------------------------------------------*/

    public Value eval(String source) {
        // Source statements cannot be null
        if (source == null) {
            source = "";
        }

        source += ";";

        // Clear results of last parse
        lastException = null;
        Value value =  new Value("ERROR: EMPTY EXPRESSION");;

        try {
            String[] expressions = source.split(expressionDelimiter + SPLIT_REGEX);

            for (String expression : expressions) {
                if (expression.trim().length() > 0) {
                    List tokens = tokenizedExpressions.get(expression);
                    if (tokens == null) {
                        lastExpression = expression;
                        tokens = new ArrayList();
                        List list = tokenize(expression, false);
                        if (list.size() > 0) {
                            tokens.addAll(infixToRPN(list));
                            tokenizedExpressions.put(expression, tokens);
                        }
                    }

                    // Restore any token values that may have been updated so cached expressions will continue to work
                    for (Token token : tokens) {
                        token.restoreOrgValue();
                    }

                    // Evaluate the expression
                    value = (tokens.size() > 0) ? RPNtoValue(tokens) : new Value("ERROR: EMPTY EXPRESSION");
                }
            }
        } catch (ParserException ex) {
            lastException = ex;
            value = new Value().setValue(lastException);
        }

        return value;
    }

    /*----------------------------------------------------------------------------*/

    public List tokenize(String input, boolean wantWhitespace) throws ParserException {
        int offset = 0;
        int row = 1;

        List tokens = new ArrayList<>();

        Matcher matcher = getPattern(this).matcher(input);
        while (matcher.find()) {
            if (wantWhitespace || matcher.group(TokenType.WHITESPACE.name()) == null) {
                for (TokenType tokenType : TokenType.values()) {
                    if (matcher.group(tokenType.name()) != null) {
                        String text = tokenType.resolve(matcher.group(tokenType.name()));
                        Token token = new Token(tokenType, text, row, matcher.start() + 1 - offset);
                        token.saveOrgValue();
                        tokens.add(token);
                        break;
                    }
                }
            }

            if (matcher.group(TokenType.NEWLINE.name()) != null) {
                offset = matcher.start() + 1;
                row++;
            }
        }

        // Remove the NOMATCH signifying end-of-expression
        if (tokens.size() > 1) {
            int last = tokens.size() - 1;
            Token lastToken = tokens.get(last);
            if (TokenType.NOMATCH.equals(lastToken.getType())) {
                tokens.remove(last);
            }

            // Check for invalid tokens in the expression
            for (Token token : tokens) {
                if (TokenType.NOMATCH.equals(token.getType())) {
                    setStatusAndFail(token, "error.invalid_token");
                }
            }
        }

        return tokens;
    }

    /*----------------------------------------------------------------------------*/

    /*
     * Convert a list of infix tokens to Reverse Polish Notation (RPN) form. Dijkstra's
     * shunting-yard algorithm is used to process the tokens.
     * @param inputTokens list of tokens to process
     * @return List outputTokens in RPN form
     */
    protected List infixToRPN(List inputTokens) throws ParserException {
        List outputTokens = new ArrayList<>();

        Token lastToken = null;
        Stack stack = new Stack<>();
        Stack argStack = new Stack<>();

        // To simplify processing later, checks for matching brackets and parenthesis are performed now
        int bcount = 0;
        int pcount = 0;
        for (Token token : inputTokens) {
            if (token.opEquals(Operator.LBRACKET)) {
                bcount++;
            } else if (token.opEquals(Operator.RBRACKET)) {
                bcount--;
                if (bcount < 0) {
                    setStatusAndFail(token, "error.missing_bracket", Operator.LBRACKET.getText());
                }
            }

            if (token.opEquals(Operator.LPAREN)) {
                pcount++;
            } else if (token.opEquals(Operator.RPAREN)) {
                pcount--;
                if (pcount < 0) {
                    setStatusAndFail(token, "error.missing_parens", Operator.LPAREN.getText());
                }
            }
        }

        if (bcount != 0) {
            Token token = inputTokens.get(inputTokens.size() - 1);
            setStatusAndFail(token, "error.missing_bracket", Operator.RBRACKET.getText());
        }

        if (pcount != 0) {
            Token token = inputTokens.get(inputTokens.size() - 1);
            setStatusAndFail(token, "error.missing_parens", Operator.RPAREN.getText());
        }

        for (Token token : inputTokens) {
            // Touch up token if a unary minus or plus is encountered
            if ((token.opEquals(Operator.MINUS) || token.opEquals(Operator.PLUS))) {
                boolean isUnary = lastToken == null || lastToken.isOperator() || lastToken.isParen();
                if (isUnary) {
                    if (token.opEquals(Operator.MINUS)) {
                        token.setText(Operator.UNARY_MINUS.getText());  // unary minus, parsers 1 - -1.0
                    } else {
                        token.setText(Operator.UNARY_PLUS.getText()); // unary plus, parses  1 + +1.0
                    }
                }
            }

            // Addresses where a function name is mistyped and is recognized as an identifier
            if (lastToken != null && lastToken.isIdentifer() && token.getText().equalsIgnoreCase(Operator.LPAREN.getText())) {
                setStatusAndFail(lastToken, "error.expected_function", lastToken.getText());
            }

            lastToken = token;

            // Suppress immediately throwing ParserExceptions when ternaries are involved
            if (token.opEquals(Operator.TIF)) {
                outputTokens.add(new Token(TokenType.NOTHROW, "?", token.getRow(), token.getColumn()));
            }

            if (!token.isOperator() &&
                (token.isNumber() || token.isString() || token.isConstant() || token.isField() || token.isIdentifer() || token.isProperty())) {
                outputTokens.add(token);
                if (!argStack.isEmpty()) {
                    argStack.peek().haveArgs = true;
                }
            } else if (token.isFunction()) {
                stack.push(token);
                if (!argStack.isEmpty()) {
                    argStack.peek().haveArgs = true;
                }
                argStack.push(new ArgCount());

            } else if (token.opEquals(Operator.COMMA)) {
                // Ignore commas if inside brackets
                if (bcount == 0) {
                    // Copy tokens to output and bump argument count for function
                    while (!stack.empty() && !stack.peek().opEquals(Operator.LPAREN)) {
                        outputTokens.add(stack.pop());
                    }
                    if (argStack.size() > 0 && argStack.peek().haveArgs) {
                        argStack.peek().count++;
                    }
                }

            } else if (token.opEquals(Operator.LBRACKET)) {
                bcount++;
                stack.push(token);
            } else if (token.opEquals(Operator.LPAREN)) {
                stack.push(token);
            } else if (token.opEquals(Operator.RBRACKET)) {
                bcount--;
                while (!stack.empty() && !stack.peek().opEquals(Operator.LBRACKET)) {
                    outputTokens.add(stack.pop());
                }
                outputTokens.add(stack.pop());
            } else if (token.opEquals(Operator.RPAREN)) {
                while (!stack.empty() && !stack.peek().opEquals(Operator.LPAREN)) {
                    outputTokens.add(stack.pop());
                }

                stack.pop();

                if (!stack.empty() && stack.peek().isFunction()) {
                    Token function = stack.pop();
                    ArgCount argc = argStack.pop();
                    function.setArgc(argc.haveArgs ? argc.count + 1 : argc.count);
                    outputTokens.add(function);
                }

            } else if (token.isOperator()) {
                // While stack not empty and stack top element is an operator add it to the output
                while (!stack.empty() && stack.peek().isOperator()) {
                    if (shouldPopToken(token, stack.peek(), caseSensitive)) {
                        outputTokens.add(stack.pop());
                        continue;
                    }
                    break;
                }
                stack.push(token);
            }
        }

        // Copy the rest of the stack to the output
        while (!stack.empty()) {
            outputTokens.add(stack.pop());
        }

        return outputTokens;
    }

    /*----------------------------------------------------------------------------*/

    private boolean haveString(Token lhs, Token rhs) {
        return lhs.getValue().getType() == ValueType.STRING || rhs.getValue().getType() == ValueType.STRING;
    }

    private void assertBothNumbers(Token lhs, Token rhs) throws ParserException {
        if (lhs.getValue().getType() != ValueType.NUMBER || rhs.getValue().getType() != ValueType.NUMBER ) {
            setStatusAndFail(rhs, "error.both_must_be_numeric", lhs.asString(), rhs.asString());
        }
    }

    private void assertSufficientStack(Token token, Stack stack, int requiredSize) throws ParserException {
        if (stack.size() < requiredSize) {
            setStatusAndFail(token, "error.syntax");
        }
    }

    private Token processOperators(Token token, Stack stack) throws ParserException {
        Token result = null;

        // Unary: percentage
        Operator op = Operator.find(token, caseSensitive);
        if (op.equals(Operator.PERCENT)) {
            assertSufficientStack(token, stack, 1);
            BigDecimal bd = stack.pop().asNumber().divide(new BigDecimal(100), getPrecision(), RoundingMode.HALF_UP).stripTrailingZeros();
            return new Token(TokenType.NUMBER, bd.toPlainString(), token.getRow(), token.getColumn());
        }

        // Ternary
        if (op.equals(Operator.TIF)) {
            assertSufficientStack(token, stack, 4);
            if (!Operator.TELSE.equals(Operator.find(stack.pop(), caseSensitive))) {
                setStatusAndFail(token, "error.expected_telse", Operator.TELSE.getText());
            }

            Token falseValue = stack.pop();
            Token trueValue = stack.pop();
            Token booleanValue =  stack.pop();
            if (booleanValue.getValue().getType() != ValueType.BOOLEAN) {
                setStatusAndFail(booleanValue, "error.boolean_expected", booleanValue.getType());
            }

            suppressParseExceptions = false;
            Token tValue = booleanValue.asBoolean() ? trueValue : falseValue;
            if (tValue.asObject() instanceof ParserException) {
                throw (ParserException)tValue.asObject();
            }

            return tValue;
        }

        assertSufficientStack(token, stack, 2);
        Token rhs = stack.pop();
        Token lhs = stack.pop();

        try {
            if (op.equals(Operator.PLUS)) {
                // Addition/concantenation
                if (haveString(lhs, rhs)) {
                    String strL = lhs.asString() == null ? "" : lhs.asString();
                    String strR = rhs.asString() == null ? "" : rhs.asString();
                    result = new Token(TokenType.STRING, strL + strR, token.getRow(), token.getColumn());
                } else {
                    assertBothNumbers(lhs, rhs);
                    BigDecimal bd = lhs.asNumber().add(rhs.asNumber());
                    bd = bd.setScale(getPrecision(), BigDecimal.ROUND_HALF_UP).stripTrailingZeros();
                    result = new Token(TokenType.NUMBER, bd.toPlainString(), token.getRow(), token.getColumn());
                }
            } else if (op.equals(Operator.MINUS)) {
                // Subtraction
                assertBothNumbers(lhs, rhs);
                BigDecimal bd = lhs.asNumber().subtract(rhs.asNumber());
                bd = bd.setScale(getPrecision(), BigDecimal.ROUND_HALF_UP).stripTrailingZeros();
                result = new Token(TokenType.NUMBER, bd.toPlainString(), token.getRow(), token.getColumn());
            } else if (op.equals(Operator.MULT)) {
                // Multiplication
                assertBothNumbers(lhs, rhs);
                BigDecimal bd = lhs.asNumber().multiply(rhs.asNumber());
                bd = bd.setScale(getPrecision(), BigDecimal.ROUND_HALF_UP).stripTrailingZeros();
                result = new Token(TokenType.NUMBER, bd.toPlainString(), token.getRow(), token.getColumn());
            } else if (op.equals(Operator.DIV)) {
                // Division
                assertBothNumbers(lhs, rhs);
                int divisorScale = rhs.asNumber().scale();
                int scale = lhs.asNumber().equals(BigDecimal.ZERO) ? divisorScale : getPrecision();
                BigDecimal bd = lhs.asNumber().divide(rhs.asNumber(), scale, BigDecimal.ROUND_HALF_UP).stripTrailingZeros();
                result = new Token(TokenType.NUMBER, bd.toPlainString(), token.getRow(), token.getColumn());
            } else if (op.equals(Operator.IDIV)) {
                // Integer division
                assertBothNumbers(lhs, rhs);
                BigDecimal bd = lhs.asNumber().divideToIntegralValue(rhs.asNumber());
                result = new Token(TokenType.NUMBER, bd.toPlainString(), token.getRow(), token.getColumn());
            } else if (op.equals(Operator.MODULUS)) {
                // Modulus
                assertBothNumbers(lhs, rhs);
                BigDecimal bd = lhs.asNumber().remainder(rhs.asNumber());
                result = new Token(TokenType.NUMBER, bd.toPlainString(), token.getRow(), token.getColumn());
            } else if (op.equals(Operator.EXP)) {
                // Exponentiation x^y
                assertBothNumbers(lhs, rhs);
                MathContext mc = rhs.asNumber().compareTo(BigDecimal.ZERO) < 0 ? MathContext.DECIMAL128 : MathContext.UNLIMITED;
                BigDecimal bd = lhs.asNumber().pow(rhs.asNumber().intValue(), mc);
                bd = bd.setScale(getPrecision(), BigDecimal.ROUND_HALF_UP).stripTrailingZeros();
                result = new Token(TokenType.NUMBER, bd.toPlainString(), token.getRow(), token.getColumn());
            } else if (op.equals(Operator.ASSIGNMENT)) {
                // Assignment
                if (lhs.isIdentifer()) {
                    // Trying to assign an uninitialized variable -- could also get here
                    // if user is trying to call a function that doesn't exist.
                    if (rhs.getValue().getType().equals(ValueType.UNDEFINED)) {
                        setStatusAndFail(rhs, "error.expected_initialized", rhs.getText());
                    }

                    // Identifier should always be found as it would have been created when parsing the RPN
                    // stack. Setting one and two dimensional array values is handled here as well.
                    String[] varName = lhs.getText().split("[\\[,\\]]");
                    Value val = variables.get(varName[0].toUpperCase());
                    if (varName.length > 1) {
                        val = val.getArray().get(Integer.valueOf(varName[1]).intValue());
                        if (varName.length > 2) {
                            val = val.getArray().get(Integer.valueOf(varName[2]).intValue());
                        }
                    }

                    val.set(rhs.getValue());
                } else {
                    setStatusAndFail(lhs, "error.expected_identifier", lhs.getText());
                }
            } else {
                result = processRelationalOperators(lhs, token, rhs);
            }
        } catch (ArithmeticException ex) {
            throw new ParserException(ex.getMessage(), ex, token.getRow(), token.getColumn());
        }

        return result;
    }

    private Token processRelationalOperators(Token lhs, Token operator, Token rhs) throws ParserException {
        boolean isTrue = false;

        Operator op = Operator.find(operator, caseSensitive);

        if (lhs.getValue().getType() == ValueType.BOOLEAN) {
            if (op.inSet(Operator.EQU, Operator.NEQ, Operator.AND, Operator.OR)) {
                isTrue = performComparison(lhs.getValue().asBoolean(), rhs.getValue().asBoolean(), op);
            } else {
                setStatusAndFail(operator, "error.invalid_operator_boolean", op.getText());
            }
        } else if (lhs.getValue().getType() == ValueType.NUMBER) {
            if (!op.inSet(Operator.AND, Operator.OR)) {
                isTrue = performComparison(lhs.getValue().asNumber(), rhs.getValue().asNumber(), op);
            } else {
                setStatusAndFail(rhs, "error.invalid_operator", op.getText());
            }
        } else if (lhs.getValue().getType() == ValueType.STRING) {
            if (!op.inSet(Operator.AND, Operator.OR)) {
                isTrue = performComparison(lhs.getValue().asString(), rhs.getValue().asString(), op);
            } else {
                setStatusAndFail(rhs, "error.invalid_operator", op.getText());
            }
        } else if (lhs.getValue().getType() == ValueType.DATE) {
            if (!op.inSet(Operator.AND, Operator.OR)) {
                isTrue = performComparison(lhs.getValue().asDate(), rhs.getValue().asDate(), op);
            } else {
                setStatusAndFail(rhs, "error.invalid_operator", op.getText());
            }
        }

        return new Token(TokenType.VALUE, new Value("VALUE", isTrue ? Boolean.TRUE : Boolean.FALSE), rhs.getRow(), rhs.getColumn() + 1);
    }

    @SuppressWarnings("unchecked")
    private boolean performComparison(Comparable o1, Comparable o2, Operator op) throws ParserException {
        boolean isTrue = false;
        boolean haveValues = o1 != null && o2 != null;
        if (Operator.AND.equals(op)) {
            isTrue = o1 instanceof Boolean && o2 instanceof Boolean && ((Boolean) o1 && (Boolean) o2);
        } else if (Operator.OR.equals(op)) {
            isTrue = o1 instanceof Boolean && o2 instanceof Boolean && ((Boolean)o1 || (Boolean)o2);
        } else if (Operator.LT.equals(op)) {
            isTrue = haveValues && o1.compareTo(o2) < 0;
        } else if (Operator.LTE.equals(op)) {
            isTrue = haveValues && o1.compareTo(o2) <= 0;
        } else if (Operator.EQU.equals(op)) {
            isTrue = haveValues && o1.compareTo(o2) == 0;
        } else if (Operator.NEQ.equals(op)) {
            isTrue = haveValues && o1.compareTo(o2) != 0;
        } else if (Operator.GTE.equals(op)) {
            isTrue = haveValues && o1.compareTo(o2) >= 0;
        } else if (Operator.GT.equals(op)) {
            isTrue = haveValues && o1.compareTo(o2) > 0;
        }
        return isTrue;
    }

    private Token processField(Token field, Stack stack) throws ParserException {
        return new Token(TokenType.VALUE, getField(field.getText()), field.getRow(), field.getColumn());
    }

    private Token processFunction(Token function, Stack stack) throws ParserException {
        Value value = null;
        String name = function.getText();
        int orgStackSize = stack.size();

        Function f = getFunction(name);
        if (f != null) {
            try {
                value = f.execute(function, stack);
            } catch (ParserException ex) {
                // Clean stack
                int toRemove = function.getArgc() - (orgStackSize - stack.size());
                for (int i = 0; i < toRemove; i++) {
                    stack.pop();
                }
                if (!suppressParseExceptions) {
                    throw ex;
                }
                return new Token(TokenType.VALUE, new Value().setValue(ex), function.getRow(), function.getColumn());
            }
        } else {
            setStatusAndFail(function, "error.no_handler", name);
        }

        return new Token(TokenType.VALUE, value, function.getRow(), function.getColumn());
    }

    protected Value RPNtoValue(List tokens) throws ParserException {
        int tcount = 0;
        Token last_telse = null;

        Stack stack = new Stack<>();

        for (Token token : tokens) {
            // Trigger suppression of parser exceptions when processing ternaries.
            if (TokenType.NOTHROW.equals(token.getType())) {
                suppressParseExceptions = true;
                continue;
            }

            if (token.isProperty()) {
                Object obj = getProperty(token.getText());
                Value value = new Value();
                if (obj instanceof Boolean) {
                    value.setValue((Boolean) obj);
                } else if (obj instanceof BigDecimal) {
                    value.setValue((BigDecimal) obj);
                } else if (obj instanceof Date) {
                    value.setValue((Date) obj);
                } else if (obj != null) {
                    value.setValue(obj.toString());
                }
                token.setValue(value);
                stack.push(token);
            } else if (token.isField()) {
                stack.push(processField(token, stack));
            } else if (token.isFunction()) {
                stack.push(processFunction(token, stack));
            } else if (token.isConstant()) {
                BigDecimal bd = getConstant(token.getText());
                token.getValue().setValue(bd);
                stack.push(token);
            } else if (token.isIdentifer()) {
                // Retrieve the value referenced by the identifier or create an empty placeholder
                Value value = variables.get(caseSensitive ? token.getText() : token.getText().toUpperCase());
                if (value == null) {
                    value = new Value();
                    variables.put(caseSensitive ? token.getText() : token.getText().toUpperCase(), value);
                }
                token.getValue().set(value);
                stack.push(token);
            } else if (token.isOperator()) {
                // Handle unary minus (negation) and plus
                Operator op = Operator.find(token, caseSensitive);
                if (Operator.UNARY_MINUS.equals(op) || Operator.NOT.equals(op)) {
                    Value value = stack.pop().getValue();
                    switch (value.getType()) {
                        case NUMBER:
                            value.setValue(value.asNumber().negate());
                            stack.push(new Token(TokenType.NUMBER, value.asString(), token.getRow(), token.getColumn()));
                            break;
                        case BOOLEAN:
                            value.setValue(value.asBoolean() ? Boolean.FALSE : Boolean.TRUE);
                            stack.push(new Token(TokenType.VALUE, value, token.getRow(), token.getColumn()));
                            break;
                        default:
                            setStatusAndFail(token, "error.type_mismatch", value.getType().name());
                    }
                    continue;
                } else if (op.equals(Operator.TIF)) {
                    tcount--;
                } else if (op.equals(Operator.TELSE)) {
                    tcount++;
                    last_telse = token;
                    stack.push(token);
                    continue;
                } else if (op.equals(Operator.UNARY_PLUS)) {
                    continue;
                }

                // Test for TIF without corresponding TELSE
                if (tcount % 2 != 0) {
                    setStatusAndFail(token, "error.missing_telse", Operator.TIF.getText(), Operator.TELSE.getText());
                }

                // If an assignment has occurred, the result should not be pushed on the stack
                Token result = processOperators(token, stack);
                if (result != null) {
                    stack.push(result);
                }




            } else if (token.getText().equals(Operator.LBRACKET.getText())) {
                Token index = null;
                Token subIndex = null;
                if (stack.peek().getType().equals(TokenType.NUMBER)) {
                    index = stack.pop();
                    if (stack.size() == 0) {
                        setStatusAndFail(index, "error.syntax");
                    }
                    if (stack.peek().getType().equals(TokenType.NUMBER)) {
                        subIndex = index;
                        index = stack.pop();
                        if (!stack.peek().getType().equals(TokenType.IDENTIFIER)) {
                            setStatusAndFail(subIndex, "Only one or two dimensional arrays are supported");
                        }
                    }
                }

                Token var = stack.pop();

                if (!ValueType.ARRAY.equals(var.getValue().getType())) {
                    setStatusAndFail(var, "error.expected_array", var.getValue().getType());
                }

                String strIdx = "";
                if (index != null) {
                    strIdx = subIndex == null ? index.asString() : index.asString() + "," + subIndex.asString();
                    strIdx = "[" + strIdx + "]";
                }
                String valName = var.getText() + strIdx;

                int idx = 0;
                Value val = null;
                if (index != null) {
                    List array = var.getValue().getArray();
                    int len = (array == null) ? 0 : array.size() - 1;

                    // Don't throw exceptions when processing tenaries
                    if (len >=0 || !suppressParseExceptions) {
                        idx = index.getValue().asNumber().intValue();
                        if (idx < 0 || idx > len) {
                            setStatusAndFail(index, "error.index_out_of_range", String.valueOf(idx), String.valueOf(len));
                        }

                        val = new Value();
                        val.set(var.getValue().getArray().get(idx));
                        val.setName(valName);

                        if (subIndex != null) {
                            if (!ValueType.ARRAY.equals(val.getType())) {
                                setStatusAndFail(var, "error.expected_array", val.getType());
                            }

                            array = val.getArray();
                            len = (array == null) ? 0 : array.size() - 1;
                            idx = subIndex.getValue().asNumber().intValue();
                            if (idx < 0 || idx > len) {
                                setStatusAndFail(subIndex, "error.index_out_of_range", String.valueOf(idx), String.valueOf(len));
                            }
                            val = val.getArray().get(idx);
                        }
                    }
                }

                // V[] is the same as V[0]
                if (val == null) {
                    int len = var.getValue().getArray().size();
                    val = len > 0 ? var.getValue().getArray().get(0) : var.getValue();
                }

                stack.push(new Token(TokenType.IDENTIFIER, valName, val, var.getRow(), var.getColumn()));
           } else {
                stack.push(token);
            }
        }

        // Test for uncaught TELSE without corresponding TIF
        if (tcount != 0) {
            setStatusAndFail(last_telse, "error.missing_tif", Operator.TELSE.getText(), Operator.TIF.getText());
        }

        // Stack should have been consumed except for the final result
        if (stack.size() > 1) {
            setStatusAndFail(stack.get(0), "error.syntax");
        }

        // For variable assignment-only expressions, return Boolean.TRUE
        return stack.size() == 0 ? new Value("empty result", Boolean.TRUE) : stack.pop().getValue();
    }

    /*----------------------------------------------------------------------------*/

    /*
     * Pops the arguments off of the stack and returns an array in the order
     * that they were pushed. This ensures optional arguments in function calls
     * appear at the end of the array and not the beginning.
     */
    public Token[] popArguments(Token function, Stack stack) {
        Token[] args = null;
        if (function.getArgc() > 0 && stack.size() > 0) {
            args = new Token[function.getArgc()];
            for (int i = function.getArgc() - 1; i >= 0; i--) {
                args[i] = stack.pop();
            }
        } else {
            args = new Token[0];
        }
        return args;
    }

    /*----------------------------------------------------------------------------*/
    /*----------------------------------------------------------------------------*/
    /*----------------------------------------------------------------------------*/

    /*
     * Clears a global variable
     *  clearGlobal("DOW") -> Boolean.TRUE and "DOW" is removed, if present
     */
    public Value _CLEARGLOBAL(Token function, Stack stack) throws ParserException {
        boolean haveParameters = listOfNullParameters(stack) == null;
        if (haveParameters) {
            clearGlobalVariable(stack.pop().asString());
        }
        return new Value(function.getText()).setValue(haveParameters ? Boolean.TRUE : Boolean.FALSE);
    }

    /*
     * Clears all global variables
     *  clearGlobals() -> Boolean.TRUE and global variables cleared
     */
    public Value _CLEARGLOBALS(Token function, Stack stack) throws ParserException {
        clearGlobalVariables();
        return new Value(function.getText()).setValue(Boolean.TRUE);
    }

    /*
    * Creates a one or two dimension array for use
    *  DIM(V, 10) -> One dimensional array of a size of 10 rows is assigned to V
    *  DIM(V, 10, 5) -> Two dimensional array of a size of 10 rows, each row containing 5 elements is assigned to V
    */
    public Value _DIM(Token function, Stack stack) throws ParserException {
        // Skip first parameter (variable) because its value will be null and that is okay
        String nullParams = listOfNullParameters(stack, function.getArgc() - 1);
        if (nullParams != null) {
            setStatusAndFail(function, "error.null_parameters", nullParams);
        }

        Token[] args = popArguments(function, stack);

        if (!TokenType.IDENTIFIER.equals(args[0].getType())) {
            setStatusAndFail(args[0], "error.expected_identifier", args[0].getType().name());
        }

        int numRows = args[1].asNumber().intValue();
        if (numRows < 1 || numRows > MAX_DIM_ROWS) {
            setStatusAndFail(args[1], "error.function_value_out_of_range", "DIM", "numRows", "1", "10000", String.valueOf(numRows));
        }

        int numCols = 0;
        if (args.length > 2) {
            numCols = args[2].asNumber().intValue();
            if (numCols < 1 || numCols > MAX_DIM_COLS) {
                setStatusAndFail(args[2], "error.function_value_out_of_range", "DIM", "numCols", "1", "256", String.valueOf(numCols));
            }
        }

        Value value = new Value("ARRAY", ValueType.ARRAY);
        for (int i = 0; i < numRows; i++) {
            Value newRow = new Value();
            value.addValueToArray(newRow);
            for (int j = 0; j < numCols; j++) {
                newRow.addValueToArray(new Value());
            }
        }

        args[0].setValue(value);
        variables.put(args[0].getText().toUpperCase(), args[0].getValue());

        return value;
    }

    /*
     * Returns a global variable or null if not found
     * parser.eval("SetGlobal('DOW', 1)");
     * getGlobal("DOW") ->  1 (NUMBER)
     * getGlobal("NotFound") -> null
     */
    public Value _GETGLOBAL(Token function, Stack stack) throws ParserException {
        String name = stack.pop().asString();
        Value value = new Value(function.getText());
        value.set(getGlobalVariable(name == null ? "~nofind~" : name));
        return value;
    }

    /*
     * Sets a global variable or null if not found
     * setGlobal("DOW", "Wednesday") ->  Boolean.TRUE and global "DOW" is (created and) set to "Wednesday"
     */
    public Value _SETGLOBAL(Token function, Stack stack) throws ParserException {
        Value value = stack.pop().getValue();
        String name = stack.pop().asString();

        boolean haveValues = value.asString() != null && name != null;
        if (haveValues) {
            addGlobalVariable(name, value);
        }

        return new Value(function.getText()).setValue(haveValues ? Boolean.TRUE : Boolean.FALSE);
    }

    /*
     * Returns the current date and time
     * parameter_1: (no parameter = actual time)
     *              0 = actual time
     *              1 = beginning of today
     *              2 = end of today
     * date() ->   2009-08-31 13:32:02
     * date(1) ->  2009-08-31 00:00:00
     * date(2) ->  2009-08-31 23:59:59
     * date("kdkdkd") -> expected number exception
     *
     */
    public Value _NOW(Token function, Stack stack) throws ParserException {
        String nullParams = listOfNullParameters(stack, function.getArgc());
        if (nullParams != null) {
            setStatusAndFail(function, "error.null_parameters", nullParams);
        }

        Calendar calendar = Calendar.getInstance();
        calendar.setTimeZone(getTimeZone());
        if (function.getArgc() > 0) {
            Token token = stack.pop();
            int mode = token.asNumber().intValue();
            switch (mode) {
                case 0:
                    break; // current time

                case 1:    // beginning of day
                    calendar.set(Calendar.HOUR_OF_DAY, 0);
                    calendar.set(Calendar.MINUTE, 0);
                    calendar.set(Calendar.SECOND, 0);
                    calendar.set(Calendar.MILLISECOND, 0);
                    break;

                case 2:    // end of day
                    calendar.set(Calendar.HOUR_OF_DAY, 23);
                    calendar.set(Calendar.MINUTE, 59);
                    calendar.set(Calendar.SECOND, 59);
                    calendar.set(Calendar.MILLISECOND, 0);
                    break;

                default:
                    String msg = ParserException.formatMessage("error.function_value_out_of_range",
                            function.getText(), "1", "0", "2", String.valueOf(mode));
                    throw new ParserException(msg, token.getRow(), token.getColumn() - 1);
            }
        }

        return new Value(function.getText()).setValue(new Date(calendar.getTimeInMillis()));
    }

    /*----------------------------------------------------------------------------*/

    /*
     * Sets the number of decimal places when working with numeric values
     * parameter_1: number of decimal places (0>)
     * returns previous precision value
     */
    public Value _PRECISION(Token function, Stack stack) throws ParserException {
        String nullParams = listOfNullParameters(stack, function.getArgc());
        if (nullParams != null) {
            setStatusAndFail(function, "error.null_parameters", nullParams);
        }

        Value value = new Value(function.getText()).setValue(BigDecimal.valueOf(precision));

        Token token = stack.pop();
        int decimals = token.asNumber().intValue();
        if (decimals >= 0 && decimals <= 100) {
            precision = decimals;
        } else {
            String msg = ParserException.formatMessage("error.function_value_out_of_range",
                    function.getText(), "1", "0", "100", String.valueOf(decimals));
            throw new ParserException(msg, token.getRow(), token.getColumn() - 1);
        }

        return value;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy