In our last article, “Making a Scientific Calculator, Part II,” we implemented a low-spec version of a calculator that only supports four operations, but there are still some problems. This article will solve these problems, and continue to improve the calculator, to achieve a more moderate version. After solving the problem, at least the following objectives should be achieved:

- Supports more mathematical operations in addition to the four operations, such as factorial, exponent, square root, logarithm, etc
- Plus and minus signs are supported
- Support for function calls
- Support expression error check and give the corresponding prompt
- Match parenthesis pairs strictly

To do this, we need to do some analysis:

- In the last implementation version, we only supported four operations. Since they are binary operators, two parameters are passed by default when the calculation result is passed to the processing method. However, the current version of the implementation parameter is variable, so the number of parameters must be variable. We know that
- Support for plus and minus signs means that there should be multiple processing methods for the same plus or minus sign
- Verifying the correctness of the expression means that the previously defined re needs to be refined and the corresponding error should be caught

Now, let’s start by perfecting the calculator step by step.

## Implementation method

**1. Constructor**

(1) Analysis: the previous support four operators, here to support more operations, so need to add the definition of other operators. At the same time, based on the previous analysis, we know that when defining an operator, we should also define the number of parameters that the operator needs to participate in the operation. We’ll leave the implementation of pluses and minuses aside for now and define a set of operators, which we’ll come back to later in the analysis of other features.

(2) Code:

```
constructor() {
this._symbols = {};
this.definedOperator("+".this.add,1.2)
this.definedOperator("-".this.sub,1.2)
this.definedOperator("*".this.multi,2.2)
this.definedOperator("/".this.div,2.2)
this.definedOperator("(")
this.definedOperator(")")
this.definedOperator("%".this.mod,2.2) / / for more
this.definedOperator("!".this.fac,3.1) / / factorial
this.definedOperator("^".Math.pow,2.2) / / index
this.calcReg()
}
Copy the code
```

We temporarily added the remainder, factorial, and exponential operators, as well as a fourth parameter that specifies how many arguments the operator needs to pass in. For now, factorial is the highest priority, exponents are temporarily set to be consistent with multiplication and division (the actual exponents are right-associative operators), and native Math.pow is used as the processing method. We also implemented two functions of factorial and remainder incidentally, the code is as follows:

```
mod(a,b) {
return a % b
}
fac(a) {
if(a % 1| |! (+a >=0)) return NaN
if(a > 170) return Infinity
let b = 1
while( a > 1) b *= a--
return b
}
Copy the code
```

Note: I have tried, in Chrome, when the base of the factorial is 170, the browser has returned infinity, so here for numbers over 170, directly return infinity, which helps improve the performance of the calculation; Also, by the definition of factorial, the cardinality must be a positive integer greater than 1, so unqualified NaN is returned.

**2. Operators define methods definedOperator**

The new definedOperator adds only one argument, and the code looks like this:

```
definedOperator(symbol, handle,precedence, argCount) {
precedence = precedence || 0 // Set the precedence of the parentheses to 0
this._symbols[symbol] = {
symbol, handle, precedence, argCount
}
}
Copy the code
```

At this point, the parse method doesn’t need to be changed if the above three operators are added, but the EVALUATE method needs to be modified a little bit to support the evaluation of undetermined parameters instead of the fixed two. The code is as follows:

```
evaluate(r){
/ / output stack
let result = [], o = [];
// Get the result of the current parse
r = this.parse(r)
for(let i = 0, len = r.length; i < len; i++) {
let c = r[i]
if(isNaN(c)) {
// Extract the argCount number from result and pass in the operator's processing method
let token = this._symbols[c] result.push(token.handle(... result.splice(-token.argCount))) }else {
result.push(c)
}
}
return result.pop();
}
Copy the code
```

In addition, since the added ^ operator is also a metacharacter of the re, it needs to be escaped, and the re results in

```
let regstr = "\\d+(? :\\.\\d+)? |" +
Object.values(this._symbols).map(
val= > val.symbol.replace(/[*+^()]/g.'\ \ $&')
).join("|")
this.pattern = new RegExp(regstr,"g")
Copy the code
```

After that we run the following code:

```
console.log(calc.evaluate("2 times 4-2 over 3 - (2 + 3) times 2"))
console.log(calc.evaluate("2 * (4-2) / 3 - (4%2) + 3! + 4 ^ 2"))
Copy the code
```

The results are as follows:

The empirical results are correct.

**3. Implementation of function support**

(1) Problem analysis

So let’s say we have the following

`1 + 2 + min (2, 3) + Max (2, four, five, and (3 + 5))`

This formula contains function calls, which have the following characteristics:

- A function name may consist of multiple characters, not necessarily a single character
- Function calls are called with parentheses, which consist of a series of arguments
- Parameters are separated by commas
- The argument can also be an expression
- The number of parameters of the same function may be indeterminate, for example, the number of parameters that can be passed in to obtain the maximum and minimum values is uncertain

This may seem like a tricky problem: what do you do with parentheses for functions and other operations? How do I make the parameters of a function not fixed? What is the priority of the operation of the function?

Let’s first go back to the scheduling field algorithm explained above, and the processing method of the function:

If the character read represents a function, push it into the operator stack. If a delimiter represents a function argument (e.g., a semicolon comma,) : the operator is repeatedly popped from the stack and placed in the output queue until the top element of the stack is an open bracket. If no open parentheses are ever encountered, either the separator is in the wrong place or the parentheses don’t match.

As you can see, when you encounter a function operator, you can push the name of the function directly onto the operator stack without comparing precedence. The main difficulty lies in the parameter separator, so let’s change the formula to understand the above method:

`1 + min (3, 4, Max (2, 2 + 3, 5))`

The entire parse process is as follows (S is the operator stack and Q is the output queue) :

S: [], Q: [1] ->

S:[+],Q:[1] ->

S:[+,min] ,Q:[1] ->

S: [+,min,(], Q: [1] ->

S: [+,min,(], Q: [1,3] ->

**S: [+,min,(], Q: [1,3] ->**

S: [+,min,(], Q: [1,3,4] ->

S: [+,min,(], Q: [1,3,4] ->

S: [+,min,(, Max], Q: [1,3,4] ->

S: [+,min,(, Max,(], Q: [1,3,4] ->

S: [+,min,(, Max,(], Q: [1,3,4,2] ->

S: [+,min,(, Max,(], Q: [1,3,4,2] ->

S: [+,min,(, Max,(], Q: [1,3,4,2,2] ->

S: [+,min,(, Max,(,+], Q: [1,3,4,2,2] ->

S: [+,min,(, Max,(,+], Q: [1,3,4,2,2,3] ->

S: [+, min, and Max, [], Q: [1,3,4,2,2,3, +] (met a comma, to press the left parenthesis before operators into output queue) – >

S: [+,min,(, Max,(], Q: [1,3,4,2,2,3,+,5] ->

S: [+,min,(, Max], Q: [1,3,4,2,2,3,+,5] ->

S: [+,min], Q: [1,3,4,2,2,3,+,5, Max] ->

S: [+, min], Q: [1,3,4,2,2,3, + 5, Max, min, +]

The final result is 1, 3, 4, 2, 2, 3 + 5 Max min +. When we try to evaluate this, we run into the problem that Max and min are still called without knowing how many arguments to pass in. But these results are based on my personal understanding of the scheduling field algorithm, because the comma does not end up on the stack as an operator, as described by the scheduling field algorithm. I am not sure whether I have a wrong understanding. If there is a correct way to interpret it, you can share it with me and I will improve the content of this part. Now, we might as well put the scheduling algorithm of the words * * “said if a function parameter of the separator (one and a half Angle of commas, for example) : constantly from the stack pop-up operator and, in the output queue until the top of the stack elements for a left parenthesis” interpreted as if said a function parameter separator… The result is 1, 3, 4, 2, 2, 3 + 5, Max, min + after the function parameter separator is pressed into the operator queue. After we get this formula, we try to evaluate it again. At this time, we can find that every time we encounter a parameter separator means that the subsequent function needs to add another parameter. This process can be realized by seeing the number of parameters passed by the function as 1, and then expanding the array of parameters passed. The argument separator is used to convert multiple arguments into an Array (for example, array.of). In this way, we can solve the above problems with parsing functions.

With the above ideas in mind, we can realize the function contained in the expression, mainly need to do the following processing:

- The function is defined to take an argument
- The function delimiter concatenates the first and second arguments into an array
`Array.of`

Processing) into the output queue - Like the open parenthesis, the function is pushed directly onto the stack without comparing precedence, but since it is an operator participating in the operation, it needs to be distinguished from the normal operator, so a new type parameter is added to the function definition to indicate that it is of type function

(1) Constructor adds the definition of supported functions

```
this.definedOperator("min".Math.min,0.1.'func')
this.definedOperator("max".Math.max,0.1.'func')
this.definedOperator(",".Array.of,0.2)
Copy the code
```

(2) Operators define functions

Add a type parameter

```
The /** * operator defines method *@param Symbol operator *@param Handle *@param Precedence Precedence *@param Precedence Specifies the number of parameters@param Type Operator type **/
definedOperator(symbol, handle,precedence, argCount,type) {
precedence = precedence || 0 // Set the precedence of the parentheses to 0
this._symbols[symbol] = {
symbol, handle, precedence, argCount,type
}
}
Copy the code
```

(3) The perfection of regularity

What should be the re of the function name? What if one function name contains another function name? Min (1,2,3) + m(4,5). In this equation, there are two functions m and min. Min contains m. If proper processing is not done, m may be resolved instead of the complete min.) In fact, both ordinary mathematical operators and functions should be considered as a symbolic whole. Re matches are greedy by default, and pipe matches are sequential from left to right. Here’s an example:

So, in this case, we just need to put min before m (sort by length) when generating the re, that is, long length comes after short length. So, in the end, the rule for regex generation is as follows:

```
let regstr = "\\d+(? :\\.\\d+)? |" +
Object.values(this._symbols)
.sort((a,b) = > b.symbol.length - a.symbol.length)
.map(
val= > val.symbol.replace(/[*+^()]/g.'\ \ $&')
).join("|")
this.pattern = new RegExp(regstr,"g")
console.log(this.pattern)
Copy the code
```

(4) Parse

Based on the above analysis, the analytical method needs to be improved as follows:

- Parsing to a function is the same as parsing to an open parenthesis, pushing it directly onto the operator stack
- Encountered a parameter separator (
`.`

) ‘, repeatedly popping the operator and pressing into the output queue until an open parenthesis is encountered

The improved parts are as follows:

```
if(isNaN(+token)) { // If it is not a number
if(token === "(" || this._symbols[token].type== 'func') {// If it is an open parenthesis or a function, it goes directly to the operator stack
operators.push(token)
} else if(token === ') ') {// When matching a close parenthesis
/ /...
} else if(! operators.length) {// The operator is empty and goes directly to the operator stack
operators.push(token)
} else if(token === ', ') {// If it is a parameter separator, press the pop-up operator into the output queue
let prev = operators[operators.length - 1]
while(prev ! = ='(') {
result.push(operators.pop())
prev = operators[operators.length - 1]}// Push the parameter separator onto the stack
operators.push(token)
} else {// Operator stack is not empty, need to compare priority
// ...}}Copy the code
```

(5) Evaluate

After modifying the parse method, I tried to parse min(1,2,3) and got 1,2,3, min, but the result was NaN. This is because the evaluate method performs the evaluate method when calling the operator’s handler method pass parameter

`result.push(token.handle(... result.splice(-token.argCount)))Copy the code`

When,, the array. of method is called. After the first, [1,2] is obtained, and the second call is [[1,2],3]. Therefore, math.min ([1,2],3) is executed, resulting in NaN. To solve this problem, you need to convert the parameters to a one-dimensional array, such as [1,2,3], on each call. How to convert [[1,2],3] into [1,2,3]? It is natural to think that this can be handled using the concat method, [].concat([1,2],3). Therefore, evaluate is improved as follows:

```
evaluate(r){
/ / output stack
let result = [], o = [];
// Get the result of the current parse
r = this.parse(r)
for(let i = 0, len = r.length; i < len; i++) {
let c = r[i]
if(isNaN(c)) {
// Extract the argCount number from result and pass in the operator's processing method
let token = this._symbols[c] result.push(token.handle(... [].concat(... result.splice(-token.argCount)))) }else {
result.push(c)
}
}
return result.pop();
}
Copy the code
```

After modification in the test to get the correct result 1. You can also get the correct result by testing the following examples.

```
console.log(calc.evaluate("Three! 2 + 4 ^ 2 + min (filling - 3)"))
console.log("-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -")
console.log(calc.evaluate("max(2! , Max (3, 3-3), min (Max) (2 ^ 2, 3))"))
Copy the code
```

The results and tracking process are as follows:

**4. Support for plus and minus signs**

(1) Operator definition method defineOperator

Plus and minus is a prefix operator, and to support plus and minus means that the same operators + and – have dual roles. Where, as a sign, it is a unary operator, and as a addition and subtraction operator, it is a binary operator. After the sign, the notation support type for a mathematical expression should be basically satisfied. Let’s revisit some of the properties that operators have.

- Operators include prefix operators, infix operators, postfix operators and functions
- To perform an operation, all but two arguments are required for the infix operator (a function argument is an array argument composed of several arguments by a function delimiter)
- The operator has a compute priority attribute
- The same operator symbol can be multiple types of operators (such as plus and minus and addition and subtraction)

Then look at the final data structure of the operators we have defined so far:

As you can see, an operator corresponds to an object that contains each parameter. If you need to change the symbol to support multiple types, you need to wrap multiple hidden layers around it to distinguish between different types. Such as:

```
+ : {type1: {
argCount: 2
handle: function(a,b) {}
precedence: 1
symbol: "+"
type: undefined
},
type2: {
argCount: 2
handle: function(a) {}
precedence: 1
symbol: "+"
type: undefined}}Copy the code
```

Therefore, if we want to support the plus and minus sign operation, the first thing we need to improve is the operator definition method defineOperator. The code after improvement is as follows:

```
The /** * operator defines method *@param Symbol operator *@param Handle *@param The type operator type [prefix, infix, postfix, func] *@param Precedence Precedence **/
definedOperator(symbol, handle, type = 'func', precedence = 0) {
// The function type enforces the priority to 0
if(type === 'func') precedence = 0
let argCount = type === 'infix' ? 2 : 1
this._symbols[symbol] = Object.assign({}, this._symbols[symbol], {
[type]:{symbol,handle,precedence,argCount,type},
symbol
})
}
Copy the code
```

Code interpretation:

- Only binary operators (infix operators) need to pass two arguments, so the original
`argCount`

This parameter can be removed and determined by the type of the operator - Operator type increment
`prefix,infix,postfix`

Three types, merging the ones defined above`func`

Type There are four types. The default value is no`func`

- Symbol definition of multiple types in a nested manner to
`type`

As a key name

(2) Constructor

After defineOperator is modified, the constructor definition of symbols should be changed as well, plus and minus signs should be added as follows:

```
this.definedOperator("+".this.last, "prefix".3)
this.definedOperator("-".this.negation, "prefix".3)
this.definedOperator("+".this.add, 'infix'.2)
this.definedOperator("-".this.sub, 'infix'.2)
this.definedOperator("*".this.multi, 'infix'.4)
this.definedOperator("/".this.div, 'infix'.4)
this.definedOperator("(")
this.definedOperator(")")
this.definedOperator("%".this.mod, 'infix'.4)
this.definedOperator("!".this.fac, 'postfix'.5)
this.definedOperator("^".Math.pow, 'infix'.4)
this.definedOperator("min".Math.min)
this.definedOperator("max".Math.max)
this.definedOperator(",".Array.of,'infix'.1)
Copy the code
```

For positive and negative signs, additional handling is required, as follows:

```
last(a){
return a
}
negation(a){
return -a
}
Copy the code
```

(3) Parse

After adding plus and minus signs, one problem to solve is how to distinguish between plus and minus signs when matching. By observing a formula such as 3*-3++6– 4, it can be found that, for a correct formula, when the last matching is an operator, when the next matching +,- should belong to the prefix operator; Conversely, when the last match was a number, the current +,- is the infix operator. Therefore, to solve this problem, it is only necessary to add a flag when reading to indicate whether the last match was a number. In the end, the parse method was refined as follows:

```
parse(s) {
Operator stack, output stack
let operators = [],result = [],
// The result of the re match, the symbol currently matched, and whether the last match was a number
match,token, lastIsNumber = false
// remove whitespace processing
s = s.replace(/\s/g.' ')
// Resets the current position of the re
this.pattern.lastIndex = 0
do{
match = this.pattern.exec(s)
token = match ? match[0] : null
// If no match is found, the match ends
//console.log(token,lastIsNumber)
if(! token)break
const curr = this._symbols[token]
if(lastIsNumber) {// The last number matched
// The last match was a number. The current match can only be an infix or suffix operator
lastIsNumber = false;
const currSymbol = curr.postfix || curr.infix
if(token === "(" || this._symbols[token].type== 'func') {// If it is an open parenthesis or a function, it goes directly to the operator stack
operators.push(curr.func || curr.prefix)
} else if(token === ') ') {// When matching a close parenthesis
// loop up the operator stack and push it into the output stack,
// until an open parenthesis is encountered
let o = operators.pop()
while(o.symbol ! = ="(") {
result.push(o)
o = operators.pop()
}
// Set lastIsNumber to true
lastIsNumber = true;
} else if(! operators.length && currSymbol.type ! = ='postfix') {// The operator is empty and goes directly to the operator stack
operators.push(this._symbols[token].infix)
} else {// Operator stack is not empty, need to compare priority
// Get the previous operator
let prev = operators[operators.length - 1]
/** If the priority of the current operator is not higher than that of the top of the stack, the top of the stack is ejected and pushed onto the output stack, and the top of the stack is cyclically compared with the rest of the stack until the priority of the current element is higher than the top of the stack **/
while(prev && prev.symbol ! = ="(" && currSymbol.symbol === ', ' || prev && currSymbol.precedence <= prev.precedence) {
result.push(operators.pop())
prev = operators[operators.length - 1]}if(currSymbol.type === 'postfix') {// The postfix operator needs to be pressed into the output queue and lastIsNumber set to true, as in 3! + 4
result.push(currSymbol)
lastIsNumber = true;
} else {
// push the current operator onto the operator stack
operators.push(currSymbol)
}
}
} else if(isNaN(+token)) {// The last match was not a number, and the current match is not a number, which means only prefix operators or functions
operators.push(curr.prefix || curr.func)
} else {// Indicates that the current value is a number
lastIsNumber = true / / tag
result.push(+token)
}
} while(match)
// Pop all remaining operators on the stack and push them onto the output stackresult.push(... operators.reverse())// Get the output stack, which is a postfix expression when converted to a string
console.log('Parse result:',result)
return result
}
Copy the code
```

Code interpretation:

- A new tag has been added
`lastIsNumber`

Used to indicate whether the last match was a number - Instead of just the operator’s symbol being pushed onto the stack, an entire operator object is pushed onto the stack
- Remove the logic for matching the conditional branch of parameter dividers. Because we know that the parameter separator is in
`defineOperator`

In the transformation, it has been treated as a binary operator. According to the scheduling field algorithm, when encountering a parameter separator, the operator after the preceding left sign needs to be pressed into the output queue. Therefore, the parameter separator can be regarded as the operator with the lowest operation priority, so its priority is 1 when defined. - For postfix expressions, adjustments have been made to push them to the output queue first when they match, and to set the
`lastIsNumber`

In order to buy`true`

. Why do you do that? When we looked at the pluses and minuses, we said that the last time we encountered an operator, we treated the pluses and minuses as pluses and minuses, which is actually a bit of a problem. Consider the formula`3! + 4`

This formula is correct and allows two consecutive operators to occur. But according to the above logic, when parsing to`+`

Because the last match was`!`

, then at this time`+`

It’s going to be treated as a plus sign, so you get an incorrect result. One solution for postfix operators is to compute the value directly, then press it into the output queue and treat it as a number. That is, the postfix operator is pushed into the output queue when it is encountered, so that it can participate in the operation first. At the same time will`lastIsNumber`

Set to`true`

That solved the problem

At this point, support for plus and minus signs is complete for the time being. Let’s test out the following formulas

```
console.log(calc.evaluate("-max(2! , Max (3, 3-3), min (Max) (2 ^ 2, 3))"))
console.log("-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -")
console.log(calc.evaluate("- 3 + + + 2"))
console.log("-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -")
console.log(calc.evaluate("3^2 + 2 * min(max(-2,-3++9))"))
Copy the code
```

The results are as follows:

## conclusion

In view of the problems left in the last article, this paper has done a detailed explanation. Up to this article, has basically realized a complete support function, four operations, positive and negative signs, exponential operations and other calculations of the mathematical calculator. Two questions remain:

- Support expression error check and give the corresponding prompt
- Match parenthesis pairs strictly

As the content of this article is enough, we will put these two problems to the next article to deal with and improve. In addition, the next article should also achieve the following goals:

- Optimize existing code by incorporating expression evaluation into the parsing process
- Encapsulate the entire calculator and expose configurable apis for the outside world
- Complete test
- Make it support multiple specifications (AMD, CMD, UMD)
- Open source the project

Next: Making a Scientific Calculator (Part 4)