Letter to GPA Conversion - javascript

I am getting a NaN return when I convert my letter input to number and then pass it to another function that totals and averages them. Is there a problem with my conversion function or the way I am calling the function?
function CalcGPA(a, b, c, d, e, f, g, h, i, j) {
var initial = a + b + c + d + e + f + g + h + i + j;
var total = initial / 10;
return total;
}
function Convert(z) {
var x = z.toString().toUpperCase();
switch (x.value) {
case "A":
return 4.0;
case "A-":
return 3.67;
case "B+":
return 3.33;
case "B":
return 3.0;
case "B-":
return 2.67;
case "C+":
return 2.33;
case "C":
return 2.0;
case "C-":
return 1.7;
case "D+":
return 1.3;
case "D":
return 1.0;
case "F":
return 0;
}
}
$(document).ready(function () {
var input2 = $('[name="grade1"],[name="grade2"],[name="grade3"],[name="grade4"],[name="grade5"],[name="grade6"],[name="grade7"],[name="grade8"],[name="grade9"],[name="grade10"],[name="grade11"]');
input2.keyup(function () {
var total2;
Convert((input2[0]));
Convert((input2[1]));
Convert((input2[2]));
Convert((input2[3]));
Convert((input2[4]));
Convert((input2[5]));
Convert((input2[6]));
Convert((input2[7]));
Convert((input2[8]));
Convert((input2[9]));
total2 = CalcGPA(input2[0], input2[1], input2[2], input2[3], input2[4], input2[5], input2[6], input2[7], input2[8], input2[9]);
total2.toFixed(2);
$(input2[10]).val(total2);
});
});

Call CalcGPA on the value obtained by running Convert() on the array elements. Alternatively, store the 'Converted' values to temporary variables, and then call CalcGPA with them as arguments. Something like this -
function CalcGPA(a, b, c, d, e, f, g, h, i, j) {
var initial = a + b + c + d + e + f + g + h + i + j;
var total = initial / 10;
return total;
}
function Convert(z) {
var x = z.toString().toUpperCase();
switch (x.value) {
case "A":
return 4.0;
case "A-":
return 3.67;
case "B+":
return 3.33;
case "B":
return 3.0;
case "B-":
return 2.67;
case "C+":
return 2.33;
case "C":
return 2.0;
case "C-":
return 1.7;
case "D+":
return 1.3;
case "D":
return 1.0;
case "F":
return 0;
}
}
$(document).ready(function () {
var input2 = $('[name="grade1"],[name="grade2"],[name="grade3"],[name="grade4"],[name="grade5"],[name="grade6"],[name="grade7"],[name="grade8"],[name="grade9"],[name="grade10"],[name="grade11"]');
input2.keyup(function () {
var total2;
total2 = CalcGPA(Convert(input2[0]), Convert(input2[1]), Convert(input2[2]), Convert(input2[3]), Convert(input2[4]), Convert(input2[5]), Convert(input2[6]), Convert(input2[7]), Convert(input2[8]), Convert(input2[9]));
total2.toFixed(2);
$(input2[10]).val(total2);
});
});

To answer your question:
You are not storing the value of your Convert() calls anywhere, so when you call CalcGPA(), you are just sending the original letter grades into the function.
To make your code better:
I would also strongly recommend that you rethink the way you are collecting values from the various input fields and using them within your calculations.
For example, I would start by applying a common class across the inputs. That way there could be 1 input or 20 inputs, and you could code to handle anything.
Let's say I give each input a class name of gradeLetterInput
Then you might be able to rewrite the $(document).ready() and CalcGPA() functions like this:
$(document).ready(function () {
// get collection of inputs
var $gradeLetterInputs = $('.gradeLetterInput');
var gpaValues = $.map($gradeLetterInputs, function($item, idx) {
return Convert($item.val());
});
var gpaAvg = CalcGPA(gpaValues);
// not shown place avg GPA on DOM somwhere
}
function CalcGPA(arr) {
var total = 0;
var count = arr.length;
for (i=0;i<count;i++) {
total += arr[i];
}
return total/count;
}
Anytime you see yourself relying on id's or input names like something# or adding a bunch of parameters to a function call for a set of similar items, you should recognize this as an anti-pattern and try to think about how you can refactor your code to eliminate such usage.

Related

Chaining Multiple operators in Javascript calculator

I cant seem to get my calculator to chain multiple operators together for example 5x5x5 should equal 125 but it just calculates 5x5 and will no longer calculate decimal numbers. Could anyone point me in the correct direction.
I have tried to assign the result to the firstNum but it seems to break the code.
button.forEach((button) =>
button.addEventListener('click', ()=> displayTotal(button.innerHTML)));
clearBtn.addEventListener('click', clear);
decmialBtn.addEventListener('click', appendDecimal);
functionBtn.forEach((button) =>
button.addEventListener('click', () => setOperator(button.innerHTML)));
equalsBtn.addEventListener('click', calculate)
let firstNum = "";
let secondNum = "";
let operatorSelection = "null";
let result = "";
function add(a, b){
return a + b
}
function subtract(a,b){
return a - b
}
function divide (a,b)
{
return a / b
}
function times (a,b){
return a*b
}
function operator(operator, numa, numb) {
switch(operator){
case '+':
return add(numa, numb);
break;
case '-':
return subtract(numa, numb);
break;
case '/':
return divide(numa,numb);
break;
case '*':
return times(numa,numb)
break;
}
}
function displayTotal (number){
userDisplay.value += number;
}
function clear (){
userDisplay.value = "";
}
function appendDecimal(){
userDisplay.value += ".";
}
function setOperator(operator){
firstNum = parseInt(userDisplay.value);
operatorSelection = operator;
result = firstNum;
clear()
}
function calculate(){
for (i = 0; i < 100; i++)
secondNum = parseInt(userDisplay.value);
result = operator(operatorSelection, firstNum, secondNum);
userDisplay.value = result;
[i];
}
For a simple JS calculator you could use the eval() function and it will handle all the operators for you taking PEMDAS in consideration, but you have to ensure the inpput is valid or it you won't work.
I have one on my Github if want an example:
https://github.com/gustavo-shigueo/calculator
Note: don't worry about the regexes I used, they are there just to prevent the user from typing stuff that would break the calculator
Here is an example of how I would do it:
operations = [
["*", (a, b) => +a * +b],
["/", (a, b) => +a / +b],
["+", (a, b) => +a + +b],
["-", (a, b) => +a - +b],
];
const calc = calcString => {
const split = [...calcString].reduce((prev, cur) => Number.isInteger(+cur) && Number.isInteger(+prev[prev.length - 1]) ? [...prev.slice(0, -1), prev[prev.length - 1] + cur] : [...prev, cur], [""]);
operations.forEach(([operator, funct]) => {
for (let i = 1; i < split.length - 1; i++) {
if (split[i] === operator) {
split[i - 1] = funct(split[i - 1], split[i + 1]);
split.splice(i, 2);
i--;
}
}
})
return split;
};
It doesn't support numbers with a dot/comma in them (e.g. 3.13) and no braces.
some explanation:
const split, I split the given string in numbers and operations, "3+45*4" would result in ["3", "+", "45", "*", "4"].
go over each operation in the given order to first do the "point calculation" and then + and -.
we go over the in 1. created array and look for our operation.
if we find one, we lool at the number before and after the operation and calculate the result given the 2 numbers and our operation type.
we overwrite those 3 elements with the result of our calculation.
we are done if all operations are done
I also have a more advanced version if you are interested

Math operations from string using Javascript

I am trying to find a simple way to perform a set of javascript math operations without using eval() function. Example: 1+2x3x400+32/2+3 and it must follow the PEMDAS math principle. This is what I have, but it doesn't work exactly it should.
function mdas(equation) {
let operations = ["*", "/", "+", "-"];
for (let outerCount = 0; outerCount < operations.length; outerCount++) {
for (let innerCount = 0; innerCount < equation.length; ) {
if (equation[innerCount] == operations[outerCount]) {
let operationResult = runOperation(equation[innerCount - 1], operations[outerCount], equation[innerCount + 1]);
var leftSideOfEquation = equation.substr(0, equation.indexOf(innerCount - 1));
var rightSideOfEquation = equation.substr(equation.indexOf(innerCount), equation.length);
var rightSideOfEquation = rightSideOfEquation.replace(rightSideOfEquation[0],String(operationResult));
equation = leftSideOfEquation + rightSideOfEquation;
innerCount = 0;
}
else {
innerCount++;
}
}
}
return "Here is it: " + equation; //result of the equation
}
If you don't want to use a complete library like mathjs - and you don't want to tackle creating your own script which would involve: lexical analysis, tokenization, syntax analysis, recursive tree parsing, compiling and output...
the simplest banal suggestion: Function
const calc = s => Function(`return(${s})`)();
console.log( calc("1+2*3*400+32/2+3") ); // 2420
console.log( calc("-3*-2") ); // 6
console.log( calc("-3 * + 1") ); // -3
console.log( calc("-3 + -1") ); // -4
console.log( calc("2 * (3 + 1)") ); // 8
My take at a custom MDAS
Here I created a Regex to retrieve operands and operators, accounting for negative values: /(-?[\d.]+)([*\/+-])?/g.
Firstly we need to remove any whitespace from our string using str.replace(/ /g , "")
Using JavaScript's String.prototype.matchAll() we can get a 2D array with all the matches as [[fullMatch, operand, operator], [.. ] we can than further flatten it using Array.prototype.flat()
Having that flattened array, we can now filter it using Array.prototype.filter() to remove the fullMatch -es returned by the regular expression and remove the last undefined value.
Define a calc Object with the needed operation functions
Iterate over the MDAS groups */ and than +- as regular expressions /\/*/ and /+-/
Consume finally the array of matches until only one array key is left
let str = "-1+2 * 3*+400+-32 /2+3.1"; // 2386.1
str = str.replace(/ +/g, ""); // Remove all spaces!
// Get operands and operators as array.
// Remove full matches and undefined values.
const m = [...str.matchAll(/(-?[\d.]+)([*\/+-])?/g)].flat().filter((x, i) => x && i % 3);
const calc = {
"*": (a, b) => a * b,
"/": (a, b) => a / b,
"+": (a, b) => a + b,
"-": (a, b) => a - b,
};
// Iterate by MDAS groups order (first */ and than +-)
[/[*\/]/, /[+-]/].forEach(expr => {
for (let i = 0; i < m.length; i += 2) {
let [a, x, b] = [m[i], m[i + 1], m[i + 2]];
x = expr.exec(x);
if (!x) continue;
m[i] = calc[x.input](parseFloat(a), parseFloat(b)); // calculate and insert
m.splice(i + 1, 2); // remove operator and operand
i -= 2; // rewind loop
}
});
// Get the last standing result
console.log(m[0]); // 2386.1
It's a little hacky, but you can try something like this:
var eqs = [
'1+2*3*4+1+1+3',
'1+2*3*400+32/2+3',
'-5+2',
'3*-2',
];
for(var eq in eqs) { console.log(mdas(eqs[eq])); }
function mdas(equation) {
console.log(equation);
var failsafe = 100;
var num = '(((?<=[*+-])-|^-)?[0-9.]+)';
var reg = new RegExp(num + '([*/])' + num);
while(m = reg.exec(equation)) {
var n = (m[3] == "*") ? m[1]*m[4] : m[1]/m[4];
equation = equation.replace(m[0], n);
if(failsafe--<0) { return 'failsafe'; }
}
var reg = new RegExp(num + '([+-])' + num);
while(m = reg.exec(equation)) {
var n = (m[3] == "+") ? 1*m[1] + 1*m[4] : m[1]-m[4];
equation = equation.replace(m[0], n);
if(failsafe--<0) { return 'failsafe'; }
}
return equation;
}

Calculating the subsegments of a line, using logarithmic scaling

I want to calculate the length of the subsegments of a line having a length of totalLineLength, where the subsegments are proportional to the log of the events given.
A visual representation: |---|-----|---------| a line composed of subsegments.
I want the sum of the calculated subsegments to be equal to totalLineLength, and Success must be printed. Log(x+1) must be used too (because it gives always positive results for positive arguments and scales 1 too).
Using linear scaling, I'm doing this, and it works.
var totalLineLength = 20;
var eventTotal;
var calcSubSegment = function(x) {
return totalLineLength * x / eventTotal;
}
var events = [0.1, 1, 22];
eventTotal = events.reduce((a, b) => a + b, 0);
var subSegments = events.map(calcSubSegment);
var segmentLength = subSegments.reduce((a, b) => a + b);
if (segmentLength != totalLineLength) {
console.log("Error:", segmentLength);
} else {
console.log("Success");
}
Using log(x+1) (calcSubSegment is updated), it doesn't work.
var totalLineLength = 20;
var eventTotal;
var calcSubSegment = function(x) {
return totalLineLength * Math.log(x+1) / Math.log(eventTotal+1);
}
var events = [0.1, 1, 22];
eventTotal = events.reduce((a, b) => a + b, 0);
var subSegments = events.map(calcSubSegment);
var segmentLength = subSegments.reduce((a, b) => a + b, 0);
if (segmentLength != totalLineLength) {
console.log("Error:", segmentLength);
} else {
console.log("Success");
}
What's wrong with my code? I suppose the eventTotal calculation, but I'm not sure how to proceed.
How should I use logarithmic scaling? I'd also like a mathematical explanation.
If I understand your task correctly, the simplest solution is first map all events using your log(x+1) functions and then just use linear split you've already implemented. Something like this:
function linearSplit(events, totalLineLength) {
var eventTotal;
var calcSubSegment = function(x) {
return totalLineLength * x / eventTotal;
}
eventTotal = events.reduce((a, b) => a + b, 0);
var subSegments = events.map(calcSubSegment);
return subSegments;
}
function logSplit(events, totalLineLength) {
var logEvents = events.map(function(x) { return Math.log(x+1); } );
return linearSplit(logEvents, totalLineLength);
}
function testSplit(events, totalLineLength, fnSplit, splitName) {
var subSegments = fnSplit(events, totalLineLength);
var segmentLength = subSegments.reduce((a, b) => a + b, 0);
console.log(splitName + ":\n" + events + " =>\n" + subSegments);
if (Math.abs(segmentLength - totalLineLength) > 0.001) {
console.log(splitName + " Error:", segmentLength);
} else {
console.log(splitName + " Success");
}
}
var totalLineLength = 20;
var events = [0.1, 1, 22];
testSplit(events, totalLineLength, linearSplit, "linearSplit");
testSplit(events, totalLineLength, logSplit, "logSplit");
The idea is that the only splitting that
Has parts proportionally to some coefficients
Makes sum of all parts equal to the whole
at the same time is the linear splitting. So if you want to scale coefficients in any way, you should scale them before passing to the linear splitting logic. Otherwise the sum of parts will not equal the whole (in case of any non-linear scaling) as it happens in your code.
P.S. It is not very good idea to compare floating point values by ==. You should use some tolerance for calculation/rounding errors.
Important rule while dealing with logarithms: log(a)+log(b) = log(a*b). Hence for the calculation of the total length, we need to the find the product of the individual numbers instead of their sum.
What you are doing is showing that log(a)+log(b) != log(a+b), so try multiplying the numbers instead of adding them.
Use an error margin as described in SergGr's answer, because of the way floating point operations work.
var totalLineLength = 20;
var eventTotal;
var calcSubSegment = function(x) {
return totalLineLength * Math.log(x+1) / Math.log(eventTotal);
}
var events = [0.1, 1, 22];
eventTotal = events.reduce((a, b) => a * (b+1), 1);
var subSegments = events.map(calcSubSegment);
var segmentLength = subSegments.reduce((a, b) => a + b, 0);
if (segmentLength != totalLineLength) {
console.log("Error:", segmentLength);
} else {
console.log("Success", subSegments);
}

implementing operator precedence in arithmetic operation

I have a code but it is not working for operator precedence. It is normally executing from first to last operator entered in a string.
It gives correct answer while string follow precedence rule(eg."12 * 10 / 5 + 10 - 1" = 33 <- correct answer). But it gives wrong answer while string doesn't follow precedence rule(eg. "15 + 10 * 5 / 10 - 1" = 49 <- wrong answer. actual answer is 19)
code:
var str="12 * 10 / 5 + 10 - 1";
var res=[],n1=[],num=[],op=[],o1=[];
var result;
res= str.split(" ");
console.error("res",res);
document.write(res.join(",")+"<br>");
for(var i=0;i<res.length;i++)
{
if(i%2==0)
{
num.push(parseInt(res[i]));
}
else
{
op.push(res[i]);
}
}
document.write(num+"<br>");
document.write(op+"<br>");
var myFunction = function(num1,num2,oper){
var j;
if(oper=='-'){
j = num1-num2;
return j;
}else if(oper=='+'){
j = num1+num2;
return j;
}else if(oper=='*'){
j = num1*num2;
return j;
}else if(oper=='/'){
j = num1/num2;
return j;
}else{
j = 0;
return j;
}
}
var x=num[0];
for(var i=1; i<num.length; i++){
x = myFunction(x,num[i],op[i-1]);
}
document.write("result of "+str+" is "+x+"<br>");
Here's one way to do it. I like because this is how we craft a parser. There are other approaches as mentioned, usual when you're using a parser generator like lex/yacc.
As you have noticed, there are times we need to save an intermediate result in order to process another part of an expression. This way, called a recursive descent parser, uses the environment's own stack to save the temp results. Another way is to use maintain a stack yourself, but that's not much valuable.
This is also very flexible. It's pretty easy to extend the grammar to different precedence levels. Change parseFactor below to parse negation and parentheses.
function parse(expression) {
return expression.split(' ')
}
function consumeNumber(tokens) {
const token = tokens.shift()
return parseInt(token)
}
function consumeOp(tokens, allowed) {
const token = tokens[0]
if (allowed.indexOf(token) >= 0) {
tokens.shift()
return token
}
}
function parseFactor(tokens) {
return consumeNumber(tokens)
}
function parseTerm(tokens) {
let value = parseFactor(tokens)
let op
while (op = consumeOp(tokens, ['*', '/'])) {
let nextVal = parseFactor(tokens)
switch (op) {
case '*':
value *= nextVal
break
case '/':
value /= nextVal
break
}
}
return value
}
function parseExpression(tokens) {
let value = parseTerm(tokens)
let op
while (op = consumeOp(tokens, ['+', '-'])) {
let nextVal = parseTerm(tokens)
switch (op) {
case '+':
value += nextVal
break
case '-':
value -= nextVal
break
}
}
return value
}
function evaluate(expression) {
const tokens = parse(expression)
return parseExpression(tokens)
}
evaluate('15 + 10 * 5 / 10 - 1')
Here's a hacked together (read: shouldn't actually be used anywhere real without extra work / error checking) object which gets the correct answer for both test cases. I'm sure this could be improved on and there's likely better algorithms for this. As per my comment it just scans for * or / and processes that calculation, then generates a new number/operator set including that result. I mainly just wrote it for fun.
I haven't bothered with the code to try and parse the string, this just takes an array of numbers and an array of operators.
(Also I get 11.5 as the incorrect result for your second test, both manually and running your code)
<script>
myCalc = {
getResult: function(n, o){
this.numbers = n;
this.operators = o;
// keep going until our list of numbers is just the final result
while( this.numbers.length > 1 )
this.findNext();
return this.numbers[0];
},
findNext: function(){
var opIndex = 0;
// find the next * or / operator
for(i=0;i<=this.operators.length;i++){
if( this.operators[i] == '*' || this.operators[i] == '/' ){
opIndex = i;
break;
}
}
var opResult = this.doCalc(this.operators[opIndex], this.numbers[opIndex], this.numbers[opIndex+1]);
// update our main numbers list and splice out the operator we just processed
// replace "x" with the result, and splice "y"
this.numbers[opIndex] = opResult;
this.numbers.splice(opIndex + 1, 1);
this.operators.splice(opIndex, 1);
console.log(this.numbers);
console.log(this.operators);
},
doCalc: function(op,x,y){
switch(op){
case '*':
return x * y;
case '/':
return x / y;
case '+':
return x + y;
case '-':
return x - y;
}
return 0;
}
};
console.log(myCalc.getResult([15,10,5,10,1], ['+','*','/','-']));
console.log(myCalc.getResult([12,10,5,10,1], ['*','/','+','-']));
</script>
Since nobody mentioned it before, you could simply process your string with eval function:
var str="15 + 10 * 5 / 10 - 1";
console.log(eval(str));
// logs 19
If the argument is an expression, eval() evaluates the expression

Generate syntax tree for simple math operations

I am trying to generate a syntax tree, for a given string with simple math operators (+, -, *, /, and parenthesis).
Given the string "1 + 2 * 3":
It should return an array like this:
["+",
[1,
["*",
[2,3]
]
]
]
I made a function to transform "1 + 2 * 3" in [1,"+",2,"*",3].
The problem is: I have no idea to give priority to certain operations.
My code is:
function isNumber(ch){
switch (ch) {
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
case '.':
return true;
break;
default:
return false;
break;
}
}
function generateSyntaxTree(text){
if (typeof text != 'string') return [];
var code = text.replace(new RegExp("[ \t\r\n\v\f]", "gm"), "");
var codeArray = [];
var syntaxTree = [];
// Put it in its on scope
(function(){
var lastPos = 0;
var wasNum = false;
for (var i = 0; i < code.length; i++) {
var cChar = code[i];
if (isNumber(cChar)) {
if (!wasNum) {
if (i != 0) {
codeArray.push(code.slice(lastPos, i));
}
lastPos = i;
wasNum = true;
}
} else {
if (wasNum) {
var n = Number(code.slice(lastPos, i));
if (isNaN(n)) {
throw new Error("Invalid Number");
return [];
} else {
codeArray.push(n);
}
wasNum = false;
lastPos = i;
}
}
}
if (wasNum) {
var n = Number(code.slice(lastPos, code.length));
if (isNaN(n)) {
throw new Error("Invalid Number");
return [];
} else {
codeArray.push(n);
}
}
})();
// At this moment, codeArray = [1,"+",2,"*",3]
return syntaxTree;
}
alert('Returned: ' + generateSyntaxTree("1 + 2 * 3"));
The way to do a top down parser, if not using FLEX/BISON or any other similar package is to first write a tokenizer that can parse input and serve tokens.
Basically you need a tokenizer that provides getNextToken, peekNextToken and skipNextToken.
Then you work your way down using this structure.
// parser.js
var input, currToken, pos;
var TOK_OPERATOR = 1;
var TOK_NUMBER = 2;
var TOK_EOF = 3;
function nextToken() {
var c, tok = {};
while(pos < input.length) {
c = input.charAt(pos++);
switch(c) {
case '+':
case '-':
case '*':
case '/':
case '(':
case ')':
tok.op = c;
tok.type = TOK_OPERATOR;
return tok;
case '0':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
tok.value = c;
tok.type = TOK_NUMBER;
return tok;
default:
throw "Unexpected character: " + c;
}
}
tok.type = TOK_EOF;
return tok;
}
function getNextToken() {
var ret;
if(currToken)
ret = currToken;
else
ret = nextToken();
currToken = undefined;
return ret;
}
function peekNextToken() {
if(!currToken)
currToken = nextToken();
return currToken;
}
function skipNextToken() {
if(!currToken)
currToken = nextToken();
currToken = undefined;
}
function parseString(str) {
input = str;
pos = 0;
return expression();
}
function expression() {
return additiveExpression();
}
function additiveExpression() {
var left = multiplicativeExpression();
var tok = peekNextToken();
while(tok.type == TOK_OPERATOR && (tok.op == '+' || tok.op == '-') ) {
skipNextToken();
var node = {};
node.op = tok.op;
node.left = left;
node.right = multiplicativeExpression();
left = node;
tok = peekNextToken();
}
return left;
}
function multiplicativeExpression() {
var left = primaryExpression();
var tok = peekNextToken();
while(tok.type == TOK_OPERATOR && (tok.op == '*' || tok.op == '/') ) {
skipNextToken();
var node = {};
node.op = tok.op;
node.left = left;
node.right = primaryExpression();
left = node;
tok = peekNextToken();
}
return left;
}
function primaryExpression() {
var tok = peekNextToken();
if(tok.type == TOK_NUMBER) {
skipNextToken();
node = {};
node.value = tok.value;
return node;
}
else
if(tok.type == TOK_OPERATOR && tok.op == '(') {
skipNextToken();
var node = expression(); // The beauty of recursion
tok = getNextToken();
if(tok.type != TOK_OPERATOR || tok.op != ')')
throw "Error ) expected";
return node
}
else
throw "Error " + tok + " not exptected";
}
As you can see, you start by requesting the least privileged operation, which requires the next higher privileged operation as its left and right term and so on. Unary operators has a little different structure. The neat thing is the recursion at the end when a parenthesis is encountered.
Here is a demo page that uses the parser and renders the parse-tree (had the code for it laying around...)
<html>
<head>
<title>tree</title>
<script src="parser.js"></script>
</head>
<body onload="testParser()">
<script>
function createTreeNode(x, y, val, color) {
var node = document.createElement("div");
node.style.position = "absolute";
node.style.left = "" + x;
node.style.top = "" + y;
node.style.border= "solid";
node.style.borderWidth= 1;
node.style.backgroundColor= color;
node.appendChild(document.createTextNode(val));
return node;
};
var yStep = 24;
var width = 800;
var height = 600;
var RED = "#ffc0c0";
var BLUE = "#c0c0ff";
container = document.createElement("div");
container.style.width = width;
container.style.height = height;
container.style.border = "solid";
document.body.appendChild(container);
var svgNS = "http://www.w3.org/2000/svg";
function renderLink(x1, y1, x2, y2)
{
var left = Math.min(x1,x2);
var top = Math.min(y1,y2);
var width = 1+Math.abs(x2-x1);
var height = 1+Math.abs(y2-y1);
var svg = document.createElementNS(svgNS, "svg");
svg.setAttribute("x", left);
svg.setAttribute("y", top);
svg.setAttribute("width", width );
svg.setAttribute("height", height );
var line = document.createElementNS(svgNS,"line");
line.setAttribute("x1", (x1 - left) );
line.setAttribute("x2", (x2 - left) );
line.setAttribute("y1", (y1 - top) );
line.setAttribute("y2", (y2 - top) );
line.setAttribute("stroke-width", "1");
line.setAttribute("stroke", "black");
svg.appendChild(line);
var div = document.createElement("div");
div.style.position = "absolute";
div.style.left = left;
div.style.top = top;
div.style.width = width;
div.style.height = height;
div.appendChild(svg);
container.appendChild(div);
}
function getHeight(dom) {
var h = dom.offsetHeight;
return h;
}
function getWidth(dom) {
var w = dom.offsetWidth;
return w;
}
function renderTree(x, y, node, width, height)
{
if(height < 1.5*yStep)
height = 1.5*yStep;
var val;
if(node.op) {
val = node.op;
color = BLUE;
}
else
if(node.value) {
val = node.value;
color = RED;
}
else
val = "?";
var dom = createTreeNode(x, y, val, color);
container.appendChild(dom);
var w = getWidth(dom);
var h = getHeight(dom);
var nx, ny;
var child;
if(node.left) {
nx = x - width/2;
ny = y+height;
var child = renderTree(nx, ny, node.left, width/2, height/2);
renderLink(x+w/2, y+h, nx+getWidth(child)/2, ny);
}
if(node.right) {
nx = x + width/2;
ny = y+height;
child = renderTree(nx, ny, node.right, width/2, height/2);
renderLink(x+w/2, y+h, nx+getWidth(child)/2, ny);
}
return dom;
}
var root;
function testParser()
{
var str = "1+2*5-5*(9+2)";
var exp = document.createElement("div");
exp.appendChild(document.createTextNode(str));
container.appendChild(exp);
var tree = parseString(str);
renderTree(width/2, 20, tree, width/2, 4*yStep);
}
</script>
</body>
</html>
The thing to do is to use a parser generator like flex or ANTLR (searching at google will find one for your language).
But if you are doing this for fun or to learn how parsers work, look up wikipedia for recursive descent parser.
A simple recursive descent parser can be easily made for simple expressions like this. You can define the grammar as:
<expression> ::= <term> | <term> <add_op> <expression>
<term> ::= <factor> | <factor> <mul_op> <term>
<factor> ::= ( <expression> ) | <number>
<add_op> ::= + | -
<mul_op> ::= * | /
Notice that by making the rule for <term> contain the rule for <factor> this grammar makes sure all multiplication/division operations occur lower in the parse tree than any addition/subtraction. This ensures those operations are evaluated first.
Similar to approach in other answers, here is another recursive implementation. It has the following distinctive characteristics:
It produces the nested array structure that is described in the question.
It supports signed numbers, so that -1 (without intermediate space) can be interpreted as a literal, not necessarily as an operator.
It supports unary minus, such as the first minus in this example: -(-1). It would also accept the string - -1 or --1, ...etc.
It supports decimal numbers with a mandatory digit before the decimal point.
It uses a regular expression to identify tokens. This will match number literals as one token, and any other, single non white space character.
Throws an error when there is a syntax validation error, with an indication where in the input string the error occurred.
The supported grammar can be described as:
<literal> ::= [ '-' ] <digit> { <digit> } [ '.' { <digit> } ] ; no white space allowed
<operator2> ::= '*' | '/'
<operator1> ::= '+' | '-'
<factor> ::= '-' <factor> | '(' <expression> ')' | <literal>
<term> ::= [ <term> <operator2> ] <factor>
<expression> ::= [ <expression> <operator1> ] <term>
Precedence is given to match the minus sign as part of a <literal> when possible.
Interactive snippet
function parse(s) {
// Create a closure for the two variables needed to iterate the input:
const
get = ((tokens, match=tokens.next().value) =>
// get: return current token when it is of the required group, and move forward,
// else if it was mandatory, throw an error, otherwise return undefined
(group, mandatory) => {
if (match?.groups[group] !== undefined)
return [match?.groups[group], match = tokens.next().value][0];
if (mandatory)
throw `${s}\n${' '.repeat(match?.index ?? s.length)}^ Expected ${group}`;
}
)( // Get iterator that matches tokens with named capture groups.
s.matchAll(/(?<number>(?:(?<![\d.)]\s*)-)?\d+(?:\.\d*)?)|(?<open>\()|(?<close>\))|(?<add>\+|(?<unary>-))|(?<mul>[*\/])|(?<end>$)|\S/g)
),
// node: Creates a tree node from given operation
node = (operation, ...values) => [operation, values],
// Grammar rules implementation, using names of regex capture groups, returning nodes
factor = (op=get("unary")) =>
op ? node(op, factor()) : get("open") ? expr("close") : +get("number", 1),
term = (arg=factor(), op=get("mul")) =>
op ? term(node(op, arg, factor())) : arg,
expr = (end, arg=term(), op=get("add")) =>
op ? expr(end, node(op, arg, term())) : (get(end, 1), arg);
return expr("end");
}
// I/O Management
const [input, output] = document.querySelectorAll("input, pre");
(input.oninput = () => {
try {
output.textContent = JSON.stringify(parse(input.value), null, 2)
} catch(err) {
output.textContent = err;
}
})();
input { width: 100%; margin-bottom: 10px; }
Math expression: <input value="1 + 2 * 3">
<pre></pre>
Explanations
tokens is an iterator over the input based on a regular expression. The regex has a look-behind assertion to ensure that the minus -- if present -- is not a binary operator, and can be included in the match of the numerical literal. The regex defines named groups, so that the code can rely on names and doesn't have to refer to literal characters.
get uses this iterator to get the next token in a shared variable (match) and return the previous one. get takes an argument to specify which named group is expected to have a match. If this is indeed the case, the next token be read, otherwise get checks whether the match was mandatory. If so, an exception is thrown, otherwise the function returns undefined, so the caller can try another grammar rule.
term, factor and expr implement the grammar rules with the corresponding names. They rely on get (with argument) to decide which way to go in the grammar rules. These functions all return trees (root nodes).
node constructs a node in the output tree, bottom up. If nodes in the tree should be something different than arrays, or some reduction should be performed (merging nodes) then this is the function to change.
Have you read up on the theory behind parsers? Wikipedia (as always) has some good articles to read:
LR parser
Recursive descent parser
I built a fun little calculator once and had the same problem as you, which I solved by
building the syntax tree without keeping the order precedence in mind,firstly. Each node has a precedence value, and when eval'ing non-constants, I'd check the left node: if it has lower precedence, I'd rotate the tree clockwise: bring it into evaluation and evaluate that first, likewise for the right node. then I'd just try to evaluate again. It seemed to work well enough for me.

Categories