This invincible design can analyze and operate any mathematical expression

Posted by kingman65 on Thu, 18 Nov 2021 08:47:15 +0100

This article is excerpted from "design patterns should be learned this way"

1 parsing mathematical expressions using interpreter patterns

The following uses the interpreter mode to implement a mathematical expression calculator, including addition, subtraction, multiplication and division.

First, define the abstract expression role IArithmeticInterpreter interface.

public interface IArithmeticInterpreter {
    int interpret();
}

Create the Interpreter abstract class for the terminating expression role.

public abstract class Interpreter implements IArithmeticInterpreter {

    protected IArithmeticInterpreter left;
    protected IArithmeticInterpreter right;

    public Interpreter(IArithmeticInterpreter left, IArithmeticInterpreter right) {
        this.left = left;
        this.right = right;
    }
}

Then create non terminator expression role addition, subtraction, multiplication and division interpreters respectively. The code of addition expression AddInterpreter class is as follows.

public class AddInterpreter extends Interpreter {

    public AddInterpreter(IArithmeticInterpreter left, IArithmeticInterpreter right) {
        super(left, right);
    }

    public int interpret() {
        return this.left.interpret() + this.right.interpret();
    }
}

The code of the SubInterpreter class of the subtraction expression is as follows.

public class SubInterpreter extends Interpreter {
    public SubInterpreter(IArithmeticInterpreter left, IArithmeticInterpreter right) {
        super(left, right);
    }

    public int interpret() {
        return this.left.interpret() - this.right.interpret();
    }
}

The code of multiplication expression MultiInterpreter class is as follows.

public class MultiInterpreter extends Interpreter {

    public MultiInterpreter(IArithmeticInterpreter left, IArithmeticInterpreter right){
        super(left,right);
    }

    public int interpret() {
        return this.left.interpret() * this.right.interpret();
    }

}

The code of DivInterpreter class of division expression is as follows.

public class DivInterpreter extends Interpreter {

    public DivInterpreter(IArithmeticInterpreter left, IArithmeticInterpreter right){
        super(left,right);
    }

    public int interpret() {
        return this.left.interpret() / this.right.interpret();
    }

}

The code of NumInterpreter class of numeric expression is as follows.

public class NumInterpreter implements IArithmeticInterpreter {
    private int value;

    public NumInterpreter(int value) {
        this.value = value;
    }


    public int interpret() {
        return this.value;
    }
}

Next, create the calculator GPCalculator class.

public class GPCalculator {
    private Stack<IArithmeticInterpreter> stack = new Stack<IArithmeticInterpreter>();

    public GPCalculator(String expression) {
        this.parse(expression);
    }

    private void parse(String expression) {
        String [] elements = expression.split(" ");
        IArithmeticInterpreter left,right;

        for (int i = 0; i < elements.length ; i++) {
            String operator = elements[i];
            if(OperatorUtil.ifOperator(operator)){
                left = this.stack.pop();
                right = new NumInterpreter(Integer.valueOf(elements[++i]));
                System.out.println("Out of stack" + left.interpret() + "and" + right.interpret());
                this.stack.push(OperatorUtil.getInterpreter(left,right,operator));
                System.out.println("Apply operator:" + operator);
            }else {
                NumInterpreter numInterpreter = new NumInterpreter(Integer.valueOf(elements[i]));
                this.stack.push(numInterpreter);
                System.out.println("Stack:" + numInterpreter.interpret());
            }

        }
    }

    public int calculate() {
        return this.stack.pop().interpret();
    }
}

The code of the tool class OperatorUtil is as follows.

public class OperatorUtil {

    public static boolean isOperator(String symbol) {
        return (symbol.equals("+") || symbol.equals("-") || symbol.equals("*"));
    }

    public static Interpreter getInterpreter(IArithmeticInterpreter left, IArithmeticInterpreter 
    right, String symbol) {
        if (symbol.equals("+")) {
            return new AddInterpreter(left, right);
        } else if (symbol.equals("-")) {
            return new SubInterpreter(left, right);
        } else if (symbol.equals("*")) {
            return new MultiInterpreter(left, right);
        } else if (symbol.equals("/")) {
            return new DivInterpreter(left, right);
        }
        return null;
    }
}

Finally, write the client test code.

public static void main(String[] args) {
        System.out.println("result: " + new GPCalculator("10 + 30").calculate());
        System.out.println("result: " + new GPCalculator("10 + 30 - 20").calculate());
        System.out.println("result: " + new GPCalculator("100 * 2 + 400 * 1 + 66").calculate());
}

The operation results are shown in the figure below.

Of course, the above simple calculator does not consider priority, which operates from left to right. In practical operations, multiplication and division belong to primary operations, and addition and subtraction belong to secondary operations. First level operation requires priority calculation. In addition, we can manually adjust the priority of operations by using parentheses. Let's optimize the code again. First, create an enumeration class.

public enum OperatorEnum {
    LEFT_BRACKET("("),
    RIGHT_BRACKET(")"),
    SUB("-"),
    ADD("+"),
    MULTI("*"),
    DIV("/"),
    ;
    private String operator;

    public String getOperator() {
        return operator;
    }

    OperatorEnum(String operator) {
        this.operator = operator;
    }
}

Then modify the processing logic of OperatorUtil and set two stacks.

public class OperatorUtil {

    public static Interpreter getInterpreter(Stack<IArithmeticInterpreter> numStack, Stack<String> operatorStack) {
        IArithmeticInterpreter right = numStack.pop();
        IArithmeticInterpreter left = numStack.pop();
        String symbol = operatorStack.pop();
        System.out.println("Digital out of stack:" + right.interpret() + "," + left.interpret() + ",Operator out of stack:" + symbol);
        if (symbol.equals("+")) {
            return new AddInterpreter(left, right);
        } else if (symbol.equals("-")) {
            return new SubInterpreter(left, right);
        } else if (symbol.equals("*")) {
            return new MultiInterpreter(left, right);
        } else if (symbol.equals("/")) {
            return new DivInterpreter(left, right);
        }
        return null;
    }
}

Modify the code of GPCalculator.

public class GPCalculator {

    //Digital stack
    private Stack<IArithmeticInterpreter> numStack = new Stack<IArithmeticInterpreter>();
    //Operator stack
    private Stack<String> operatorStack = new Stack<String>();
    /**
     * Analytical expression
     * @param expression
     */
    public GPCalculator(String expression) {
        this.parse(expression);
    }

    private void parse(String input) {
        //Remove empty characters from expressions
        String expression = this.fromat(input);
        System.out.println("Standard expression:" + expression);
        for (String s : expression.split(" ")) {
            if (s.length() == 0){
                //If it is a space, continue the loop and do nothing
                continue;
            }
            //If it is addition and subtraction, because the priority of addition and subtraction is the lowest, as long as the addition and subtraction sign is encountered here, no matter what operator is in the operator stack, it must be operated
            else if (s.equals(OperatorEnum.ADD.getOperator())
                    || s.equals(OperatorEnum.SUB.getOperator())) {
                //When the stack is not empty, and the top element in the stack is any one of addition, subtraction, multiplication and division
                while (!operatorStack.isEmpty()
                        &&(operatorStack.peek().equals(OperatorEnum.SUB.getOperator())
                        || operatorStack.peek().equals(OperatorEnum.ADD.getOperator())
                        || operatorStack.peek().equals(OperatorEnum.MULTI.getOperator())
                        || operatorStack.peek().equals(OperatorEnum.DIV.getOperator()))) {
                    //The results are stored in the stack
                    numStack.push(OperatorUtil.getInterpreter(numStack,operatorStack));
                }
                //After the operation, put the current operator on the stack
                System.out.println("Operator stack:"+s);
                operatorStack.push(s);
            }
            //When the current operator is multiplication and division, it takes precedence over addition and subtraction
           //Therefore, it is necessary to judge whether the uppermost is multiplication and division. If it is multiplication and division, it will be calculated. Otherwise, it will be directly put on the stack
            else if (s.equals(OperatorEnum.MULTI.getOperator())
                    || s.equals(OperatorEnum.DIV.getOperator())) {
                while (!operatorStack.isEmpty()&&(
                        operatorStack.peek().equals(OperatorEnum.MULTI.getOperator())
                        || operatorStack.peek().equals(OperatorEnum.DIV.getOperator()))) {
                    numStack.push(OperatorUtil.getInterpreter(numStack,operatorStack));
                }
                //Stack the current operator
                System.out.println("Operator stack:"+s);
                operatorStack.push(s);
            }
            //If it is an open bracket, it will be directly put on the stack without any operation. The trim() function is used to remove spaces. Due to the above segmentation operation, the operator may contain spaces
            else if (s.equals(OperatorEnum.LEFT_BRACKET.getOperator())) {
                System.out.println("Operator stack:"+s);
                operatorStack.push(OperatorEnum.LEFT_BRACKET.getOperator());
            }
            //If it is a right parenthesis, the operator in the stack is cleared to the left parenthesis
            else if (s.equals(OperatorEnum.RIGHT_BRACKET.getOperator())) {
                while (!OperatorEnum.LEFT_BRACKET.getOperator().equals(operatorStack.peek())) {
                    //Start operation
                    numStack.push(OperatorUtil.getInterpreter(numStack,operatorStack));
                }
                //Clear the left parenthesis after the operation
                String pop = operatorStack.pop();
                System.out.println("After the bracket operation is completed, clear the right bracket in the stack:"+pop);
            }
            //If it is a number, it is directly put into the stack of data
            else {
                //Convert a numeric string into a number and store it on the stack
                NumInterpreter numInterpreter = new NumInterpreter(Integer.valueOf(s));
                System.out.println("Digital stack:"+s);
                numStack.push(numInterpreter);
            }
        }
        //Finally, when the stack is not empty, continue the operation until the stack is empty
        while (!operatorStack.isEmpty()) {
            numStack.push(OperatorUtil.getInterpreter(numStack,operatorStack));
        }
    }

    /**
     * Calculation result out of stack
     * @return
     */
    public int calculate() {
        return this.numStack.pop().interpret();
    }

    /**
     * Change to standard form to facilitate segmentation
     * @param expression
     * @return
     */
    private String fromat(String expression) {
        String result = "";
        for (int i = 0; i < expression.length(); i++) {
            if (expression.charAt(i) == '(' || expression.charAt(i) == ')' ||
                expression.charAt(i) == '+' || expression.charAt(i) == '-' ||
                expression.charAt(i) == '*' || expression.charAt(i) == '/')
                //Add a space between the operator and the number
                result += (" " + expression.charAt(i) + " ");
            else
                result += expression.charAt(i);
        }
        return result;
    }
}

At this point, let's look at the client test code.

public static void main(String[] args) {
        System.out.println("result: " + new GPCalculator("10+30/((6-4)*2-2)").calculate());
}

Run to get the expected results, as shown in the figure below.

2 Application of Interpreter pattern in JDK source code

Let's first look at the compilation and parsing of regular expressions by Pattern in the JDK source code.

public final class Pattern implements java.io.Serializable {
    ...
    private Pattern(String p, int f) {
        pattern = p;
        flags = f;

        if ((flags & UNICODE_CHARACTER_CLASS) != 0)
            flags |= UNICODE_CASE;

     
        capturingGroupCount = 1;
        localCount = 0;

        if (pattern.length() > 0) {
            compile();
        } else {
            root = new Start(lastAccept);
            matchRoot = lastAccept;
        }
    }
    ...
    public static Pattern compile(String regex) {
        return new Pattern(regex, 0);
    }
    public static Pattern compile(String regex, int flags) {
        return new Pattern(regex, flags);
    }

    ...
}

Application of Interpreter pattern in Spring source code

Let's look at the ExpressionParser interface in Spring.

public interface ExpressionParser {

    Expression parseExpression(String expressionString) throws ParseException;

    Expression parseExpression(String expressionString, ParserContext context) throws ParseException;

}

Here we do not explain the source code in depth. We can roughly understand its principle through the cases we wrote earlier. You might as well write a piece of client code to verify it. The client test code is as follows.

    public static void main(String[] args) {
        ExpressionParser parser = new SpelExpressionParser();
        Expression expression = parser.parseExpression("100 * 2 + 400 * 1 + 66");
        int result = (Integer) expression.getValue();
        System.out.println("The calculation result is:" + result);
    }
        

The operation results are shown in the figure below.

It can be seen from the above figure that the operation results are consistent with the expected results.