An arithmetic expression evaluator turns a math string like "2 + 3 * 4" into the number it represents — 14, not 20 — by honoring operator precedence and parentheses the way a pocket calculator does. You will write arithmeticExpressionEvaluator(expression), which tokenizes the string, parses it, and returns the result as a number. No eval and no new Function: the whole point is to build the parser yourself.
arithmeticExpressionEvaluator(expression: string): number
arithmeticExpressionEvaluator('2 + 3 * 4'); // 14 — '*' binds tighter than '+'
arithmeticExpressionEvaluator('(2 + 3) * 4'); // 20 — parentheses override that
arithmeticExpressionEvaluator('10 - 3 - 2'); // 5 — left to right, not 9
arithmeticExpressionEvaluator('10 / 4'); // 2.5 — real division
arithmeticExpressionEvaluator('3 * -2'); // -6 — unary minus
arithmeticExpressionEvaluator('-(2 + 3)'); // -5 — unary minus on a group
arithmeticExpressionEvaluator('1 +'); // throws — dangling operator
arithmeticExpressionEvaluator('(1 + 2'); // throws — unbalanced parenthesis
3, 3.5). A result can be non-integer: 10 / 4 is 2.5, not 2.* and / bind tighter than + and -, and equal-precedence operators are left-associative, so 8 - 3 - 2 is 3, not 7.(2 + 3) * 4 is 20.- may negate a value or a whole group: -5, 3 * -2, -(2 + 3), and 2 - -3 all work.2+3 and 2 + 3 mean the same thing.Error for unbalanced parentheses, a leading or trailing operator with no operand, an empty string, or an unexpected character, so abc and 2 ** 3 both throw. Division by zero is not malformed: it follows JavaScript and yields Infinity.**), modulo (%), variables, named functions like sqrt, or scientific notation. The four basic operators plus parentheses are the whole surface.You will build a small calculator: one function that reads a math string, respects precedence and parentheses, and returns the number it evaluates to — using a hand-written parser instead of eval.
You type 2 + 3 * 4 into a calculator and expect 14, because multiplication happens before addition. Type (2 + 3) * 4 and you expect 20, because the parentheses go first. A string like this is not a number your program can just add up left to right — it has structure, and that structure is the precedence and grouping rules you learned in school. Your job is to read the string, recover that structure, and compute the result. The tempting shortcut, eval, is off the table: it runs arbitrary code, and it teaches you nothing about how a parser actually works.
Turning a string into a number happens in three stages. First tokenize: scan the characters and group them into meaningful pieces — numbers, operators, parentheses — dropping whitespace. Then parse: arrange those tokens into a structure that captures precedence, so that in 2 + 3 * 4 the 3 * 4 is bound together more tightly than the +. Finally evaluate: walk that structure and fold it down to one number.
The middle stage is where the intelligence lives. 2 + 3 * 4 and (2 + 3) * 4 contain the same five tokens; all that differs is how they group. Get the grouping right and evaluation is trivial arithmetic.
The obvious approach: split the string into pieces and fold them left to right, applying each operator the moment you reach it.
function arithmeticExpressionEvaluator(expression) {
const parts = expression.split(/\s+/).filter(Boolean); // ["2", "+", "3", "*", "4"]
let result = Number(parts[0]);
for (let i = 1; i < parts.length; i += 2) {
const op = parts[i];
const num = Number(parts[i + 1]);
if (op === '+') result += num;
else if (op === '-') result -= num;
else if (op === '*') result *= num;
else if (op === '/') result /= num;
}
return result;
}
This gets 1 + 2 right, but it folds strictly left to right, so 2 + 3 * 4 becomes ((2 + 3) * 4) — it computes 5 * 4 = 20 when the answer is 14. It has no notion that * should reach across the + and grab its operands first. It also cannot handle parentheses at all: a ( becomes Number('('), which is NaN, and the result collapses. And splitting on whitespace means 2+3 with no spaces is one unsplittable token.
What we actually need is to group the tokens by precedence before doing any arithmetic. Drawn as a tree, 2 + 3 * 4 looks like this:
The * sits below the +, so it evaluates first. That one picture is the whole precedence rule made concrete — and building it is exactly what the naive left-to-right loop refuses to do.
The fix is a recursive-descent parser: one small function per precedence level, each calling down into the next. First we tokenize the string into a clean list of { type, value } pieces; then three functions — parseExpression, parseTerm, and parseFactor — consume those tokens and evaluate as they go. Here is the whole evaluator.
function arithmeticExpressionEvaluator(expression) {
const tokens = tokenize(expression);
let pos = 0; // index of the next token to read
const peek = () => tokens[pos]; // current token, or undefined past the end
const consume = () => tokens[pos++]; // read the current token and advance
// expression -> term (('+' | '-') term)* lowest precedence, left-associative
function parseExpression() {
let value = parseTerm();
while (isOperator(peek(), '+', '-')) {
const op = consume().value;
const right = parseTerm();
value = op === '+' ? value + right : value - right;
}
return value;
}
// term -> factor (('*' | '/') factor)* higher precedence, left-associative
function parseTerm() {
let value = parseFactor();
while (isOperator(peek(), '*', '/')) {
const op = consume().value;
const right = parseFactor();
value = op === '*' ? value * right : value / right;
}
return value;
}
// factor -> ('-' | '+') factor | number | '(' expression ')'
function parseFactor() {
const token = peek();
if (!token) {
// Ran out of input while a value was expected — this is the '1 +' case.
throw new Error('Unexpected end of expression: expected a number or "("');
}
// Unary minus/plus: consume the sign, then the factor it applies to. Placing
// it at the factor level makes it bind tighter than + and -, which is why
// "-2 + 3" is 1, not -(2 + 3).
if (isOperator(token, '+', '-')) {
consume();
const operand = parseFactor();
return token.value === '-' ? -operand : operand;
}
if (token.type === 'number') {
consume();
return token.value;
}
if (token.type === 'lparen') {
consume(); // eat '('
const value = parseExpression(); // a parenthesized group is a fresh expression
if (peek() && peek().type === 'rparen') {
consume(); // eat the matching ')'
return value;
}
throw new Error('Unbalanced parentheses: expected a ")"');
}
// A ')' with no matching '(', or an operator where a value belongs.
throw new Error(`Unexpected token "${token.value}"`);
}
const result = parseExpression();
// Any leftover token means the parse stopped early — e.g. "2 3" or "1 + 2)".
if (pos < tokens.length) {
throw new Error(`Unexpected token "${peek().value}"`);
}
return result;
}
// True when `token` is an operator whose value is one of the given symbols.
function isOperator(token, ...symbols) {
return !!token && token.type === 'operator' && symbols.includes(token.value);
}
// Turn the raw string into a flat list of tokens, skipping whitespace.
function tokenize(input) {
const tokens = [];
let i = 0;
while (i < input.length) {
const ch = input[i];
if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
i++; // whitespace separates tokens but is not one — skip it
continue;
}
if (ch === '+' || ch === '-' || ch === '*' || ch === '/') {
tokens.push({ type: 'operator', value: ch });
i++;
} else if (ch === '(') {
tokens.push({ type: 'lparen', value: ch });
i++;
} else if (ch === ')') {
tokens.push({ type: 'rparen', value: ch });
i++;
} else if (isDigit(ch) || ch === '.') {
// A number is a run of digits with at most one decimal point.
let text = '';
let seenDot = false;
while (i < input.length && (isDigit(input[i]) || input[i] === '.')) {
if (input[i] === '.') {
if (seenDot) throw new Error('Malformed number: more than one decimal point');
seenDot = true;
}
text += input[i];
i++;
}
if (text === '.') throw new Error('Malformed number: a lone "."');
tokens.push({ type: 'number', value: Number(text) });
} else {
throw new Error(`Unexpected character "${ch}"`);
}
}
if (tokens.length === 0) {
throw new Error('Cannot evaluate an empty expression');
}
return tokens;
}
function isDigit(ch) {
return ch >= '0' && ch <= '9';
}
module.exports = { arithmeticExpressionEvaluator };
The three functions mirror a grammar, read top to bottom:
expression -> term (('+' | '-') term)*
term -> factor (('*' | '/') factor)*
factor -> ('-' | '+') factor | number | '(' expression ')'
An expression is one or more terms joined by + or -; a term is one or more factors joined by * or /; and a factor is the atom — a number, a unary-signed factor, or a whole expression in parentheses. Because expression is the outermost rule and factor the innermost, the operators named lowest in the grammar bind tightest.
Three ideas replace the naive loop. Tokenizing first frees the parser from character-level worries — it works with a clean list of { type, value } tokens and never sees a space. One function per precedence level encodes the rule that * and / bind tighter than + and -: parseExpression only ever combines whole terms, and each term has already consumed its own * and / chain, so a multiplication can never leak across a +. Recursion handles both parentheses — a ( sends parseFactor back into parseExpression for the inner group — and unary minus, where a - recurses into another factor.
Left-associativity falls out of the while loops. In parseExpression, we compute the first term into value, then each time we see a + or - we fold the next term into value right away: 10 - 3 - 2 runs as (10 - 3), then ... - 2, giving 7 - 2 = 5 — not 10 - (3 - 2) = 9. Folding as we go, rather than collecting every term and combining from the right, is what makes the operators left-associative.
Unary minus lives in parseFactor, the tightest level, on purpose. If it lived higher up, -2 + 3 would parse as -(2 + 3) = -5; because it binds at the factor level, the - grabs only the 2, giving (-2) + 3 = 1. The same branch treats a unary + as a no-op and nests naturally, so 3 * -2 reads -2 as the right factor of the multiply, and -(2 + 3) negates the whole parenthesized group.
The errors are a few small guards. When parseFactor is asked for a value but peek() is undefined, the input ended where an operand was required — a dangling operator like 1 + — so it throws. When a ( opens but no matching ) follows, it throws for unbalanced parentheses. And after the top-level parseExpression returns, any leftover token means the parse stopped early — a stray ) in 1 + 2), or a second number in 2 3 — so we throw rather than silently ignore the tail. The tokenizer adds two more: an unrecognized character, so abc and the second * of 2 ** 3 both fail, and an empty token list for the empty or whitespace-only string.
Division by zero is deliberately not treated as an error. parseTerm divides with JavaScript's own /, so 1 / 0 yields Infinity and 0 / 0 yields NaN, exactly as they would anywhere else in JS. A well-formed expression that happens to divide by zero has a defined IEEE-754 answer; calling it malformed would surprise the caller. If your product needs it to throw instead, that is a one-line guard in parseTerm — see Going further.
Take 2 + 3 * 4. After tokenizing we hold [2] [+] [3] [*] [4] with pos = 0.
parseExpression calls parseTerm, which calls parseFactor. parseFactor sees the number 2, consumes it, returns 2. Back in parseTerm, the next token is +, which is neither * nor /, so the term is just 2. parseExpression now holds value = 2.+, so parseExpression consumes it and calls parseTerm again for the right side. This is the decisive move: the right operand of + is a whole term, so it will swallow the multiplication before the + ever combines anything.parseTerm calls parseFactor, which reads 3. Back in parseTerm, the next token is * — a match. It consumes *, calls parseFactor for 4, and folds 3 * 4 = 12. The next token is undefined, so the term loop ends and parseTerm returns 12.parseExpression, we fold the +: value = 2 + 12 = 14. No tokens remain, the loop ends, and pos is at the end — no leftover-token error. The result is 14.Precedence was never checked explicitly. It is baked into the call order: parseExpression asked parseTerm for the right side of the +, and parseTerm consumed the 3 * 4 as a unit before handing back a single value.
Contrast (2 + 3) * 4. Now parseTerm's first parseFactor sees a (, so it recurses into parseExpression for the inner group, which returns 5, and consumes the ). Back in parseTerm, the next token is *, so it folds 5 * 4 = 20. The parentheses forced the addition to happen first by making it a factor — a self-contained value the multiply then consumes.
2 + 3 * 4 equal 20. Fix: one parse function per precedence level, so a term consumes its whole * and / chain before an outer + can see it.- above the binary + and -, and -2 + 3 wrongly becomes -5. Keep it in parseFactor so it binds tightest and negates only the immediate factor.value = term OP parseExpression()) makes 10 - 3 - 2 compute as 10 - (3 - 2) = 9. Fold in a loop as you go, so it is (10 - 3) - 2 = 5.parseExpression finishes, 1 + 2) and 2 3 silently return 3 and 2. After the top-level parse, confirm every token was consumed and throw if not.peek().value with no if (!token) guard throws Cannot read properties of undefined on 1 +, which points at the wrong thing. Guard the empty peek and throw a clear "unexpected end of expression."power level between factor and the operators above it. ** is right-associative, so 2 ** 3 ** 2 is 2 ** (3 ** 2) = 512; that means parsePower recurses on its right operand instead of looping like parseTerm.factor also be an identifier looked up in a scope, or a call like max(2, 3). The tokenizer grows an identifier rule and parseFactor a lookup, but the precedence skeleton is unchanged.1 / 0 should be rejected rather than returning Infinity, guard it in parseTerm: when the operator is / and the right factor is 0, throw before dividing.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.
An arithmetic expression evaluator turns a math string like "2 + 3 * 4" into the number it represents — 14, not 20 — by honoring operator precedence and parentheses the way a pocket calculator does. You will write arithmeticExpressionEvaluator(expression), which tokenizes the string, parses it, and returns the result as a number. No eval and no new Function: the whole point is to build the parser yourself.
arithmeticExpressionEvaluator(expression: string): number
arithmeticExpressionEvaluator('2 + 3 * 4'); // 14 — '*' binds tighter than '+'
arithmeticExpressionEvaluator('(2 + 3) * 4'); // 20 — parentheses override that
arithmeticExpressionEvaluator('10 - 3 - 2'); // 5 — left to right, not 9
arithmeticExpressionEvaluator('10 / 4'); // 2.5 — real division
arithmeticExpressionEvaluator('3 * -2'); // -6 — unary minus
arithmeticExpressionEvaluator('-(2 + 3)'); // -5 — unary minus on a group
arithmeticExpressionEvaluator('1 +'); // throws — dangling operator
arithmeticExpressionEvaluator('(1 + 2'); // throws — unbalanced parenthesis
3, 3.5). A result can be non-integer: 10 / 4 is 2.5, not 2.* and / bind tighter than + and -, and equal-precedence operators are left-associative, so 8 - 3 - 2 is 3, not 7.(2 + 3) * 4 is 20.- may negate a value or a whole group: -5, 3 * -2, -(2 + 3), and 2 - -3 all work.2+3 and 2 + 3 mean the same thing.Error for unbalanced parentheses, a leading or trailing operator with no operand, an empty string, or an unexpected character, so abc and 2 ** 3 both throw. Division by zero is not malformed: it follows JavaScript and yields Infinity.**), modulo (%), variables, named functions like sqrt, or scientific notation. The four basic operators plus parentheses are the whole surface.You will build a small calculator: one function that reads a math string, respects precedence and parentheses, and returns the number it evaluates to — using a hand-written parser instead of eval.
You type 2 + 3 * 4 into a calculator and expect 14, because multiplication happens before addition. Type (2 + 3) * 4 and you expect 20, because the parentheses go first. A string like this is not a number your program can just add up left to right — it has structure, and that structure is the precedence and grouping rules you learned in school. Your job is to read the string, recover that structure, and compute the result. The tempting shortcut, eval, is off the table: it runs arbitrary code, and it teaches you nothing about how a parser actually works.
Turning a string into a number happens in three stages. First tokenize: scan the characters and group them into meaningful pieces — numbers, operators, parentheses — dropping whitespace. Then parse: arrange those tokens into a structure that captures precedence, so that in 2 + 3 * 4 the 3 * 4 is bound together more tightly than the +. Finally evaluate: walk that structure and fold it down to one number.
The middle stage is where the intelligence lives. 2 + 3 * 4 and (2 + 3) * 4 contain the same five tokens; all that differs is how they group. Get the grouping right and evaluation is trivial arithmetic.
The obvious approach: split the string into pieces and fold them left to right, applying each operator the moment you reach it.
function arithmeticExpressionEvaluator(expression) {
const parts = expression.split(/\s+/).filter(Boolean); // ["2", "+", "3", "*", "4"]
let result = Number(parts[0]);
for (let i = 1; i < parts.length; i += 2) {
const op = parts[i];
const num = Number(parts[i + 1]);
if (op === '+') result += num;
else if (op === '-') result -= num;
else if (op === '*') result *= num;
else if (op === '/') result /= num;
}
return result;
}
This gets 1 + 2 right, but it folds strictly left to right, so 2 + 3 * 4 becomes ((2 + 3) * 4) — it computes 5 * 4 = 20 when the answer is 14. It has no notion that * should reach across the + and grab its operands first. It also cannot handle parentheses at all: a ( becomes Number('('), which is NaN, and the result collapses. And splitting on whitespace means 2+3 with no spaces is one unsplittable token.
What we actually need is to group the tokens by precedence before doing any arithmetic. Drawn as a tree, 2 + 3 * 4 looks like this:
The * sits below the +, so it evaluates first. That one picture is the whole precedence rule made concrete — and building it is exactly what the naive left-to-right loop refuses to do.
The fix is a recursive-descent parser: one small function per precedence level, each calling down into the next. First we tokenize the string into a clean list of { type, value } pieces; then three functions — parseExpression, parseTerm, and parseFactor — consume those tokens and evaluate as they go. Here is the whole evaluator.
function arithmeticExpressionEvaluator(expression) {
const tokens = tokenize(expression);
let pos = 0; // index of the next token to read
const peek = () => tokens[pos]; // current token, or undefined past the end
const consume = () => tokens[pos++]; // read the current token and advance
// expression -> term (('+' | '-') term)* lowest precedence, left-associative
function parseExpression() {
let value = parseTerm();
while (isOperator(peek(), '+', '-')) {
const op = consume().value;
const right = parseTerm();
value = op === '+' ? value + right : value - right;
}
return value;
}
// term -> factor (('*' | '/') factor)* higher precedence, left-associative
function parseTerm() {
let value = parseFactor();
while (isOperator(peek(), '*', '/')) {
const op = consume().value;
const right = parseFactor();
value = op === '*' ? value * right : value / right;
}
return value;
}
// factor -> ('-' | '+') factor | number | '(' expression ')'
function parseFactor() {
const token = peek();
if (!token) {
// Ran out of input while a value was expected — this is the '1 +' case.
throw new Error('Unexpected end of expression: expected a number or "("');
}
// Unary minus/plus: consume the sign, then the factor it applies to. Placing
// it at the factor level makes it bind tighter than + and -, which is why
// "-2 + 3" is 1, not -(2 + 3).
if (isOperator(token, '+', '-')) {
consume();
const operand = parseFactor();
return token.value === '-' ? -operand : operand;
}
if (token.type === 'number') {
consume();
return token.value;
}
if (token.type === 'lparen') {
consume(); // eat '('
const value = parseExpression(); // a parenthesized group is a fresh expression
if (peek() && peek().type === 'rparen') {
consume(); // eat the matching ')'
return value;
}
throw new Error('Unbalanced parentheses: expected a ")"');
}
// A ')' with no matching '(', or an operator where a value belongs.
throw new Error(`Unexpected token "${token.value}"`);
}
const result = parseExpression();
// Any leftover token means the parse stopped early — e.g. "2 3" or "1 + 2)".
if (pos < tokens.length) {
throw new Error(`Unexpected token "${peek().value}"`);
}
return result;
}
// True when `token` is an operator whose value is one of the given symbols.
function isOperator(token, ...symbols) {
return !!token && token.type === 'operator' && symbols.includes(token.value);
}
// Turn the raw string into a flat list of tokens, skipping whitespace.
function tokenize(input) {
const tokens = [];
let i = 0;
while (i < input.length) {
const ch = input[i];
if (ch === ' ' || ch === '\t' || ch === '\n' || ch === '\r') {
i++; // whitespace separates tokens but is not one — skip it
continue;
}
if (ch === '+' || ch === '-' || ch === '*' || ch === '/') {
tokens.push({ type: 'operator', value: ch });
i++;
} else if (ch === '(') {
tokens.push({ type: 'lparen', value: ch });
i++;
} else if (ch === ')') {
tokens.push({ type: 'rparen', value: ch });
i++;
} else if (isDigit(ch) || ch === '.') {
// A number is a run of digits with at most one decimal point.
let text = '';
let seenDot = false;
while (i < input.length && (isDigit(input[i]) || input[i] === '.')) {
if (input[i] === '.') {
if (seenDot) throw new Error('Malformed number: more than one decimal point');
seenDot = true;
}
text += input[i];
i++;
}
if (text === '.') throw new Error('Malformed number: a lone "."');
tokens.push({ type: 'number', value: Number(text) });
} else {
throw new Error(`Unexpected character "${ch}"`);
}
}
if (tokens.length === 0) {
throw new Error('Cannot evaluate an empty expression');
}
return tokens;
}
function isDigit(ch) {
return ch >= '0' && ch <= '9';
}
module.exports = { arithmeticExpressionEvaluator };
The three functions mirror a grammar, read top to bottom:
expression -> term (('+' | '-') term)*
term -> factor (('*' | '/') factor)*
factor -> ('-' | '+') factor | number | '(' expression ')'
An expression is one or more terms joined by + or -; a term is one or more factors joined by * or /; and a factor is the atom — a number, a unary-signed factor, or a whole expression in parentheses. Because expression is the outermost rule and factor the innermost, the operators named lowest in the grammar bind tightest.
Three ideas replace the naive loop. Tokenizing first frees the parser from character-level worries — it works with a clean list of { type, value } tokens and never sees a space. One function per precedence level encodes the rule that * and / bind tighter than + and -: parseExpression only ever combines whole terms, and each term has already consumed its own * and / chain, so a multiplication can never leak across a +. Recursion handles both parentheses — a ( sends parseFactor back into parseExpression for the inner group — and unary minus, where a - recurses into another factor.
Left-associativity falls out of the while loops. In parseExpression, we compute the first term into value, then each time we see a + or - we fold the next term into value right away: 10 - 3 - 2 runs as (10 - 3), then ... - 2, giving 7 - 2 = 5 — not 10 - (3 - 2) = 9. Folding as we go, rather than collecting every term and combining from the right, is what makes the operators left-associative.
Unary minus lives in parseFactor, the tightest level, on purpose. If it lived higher up, -2 + 3 would parse as -(2 + 3) = -5; because it binds at the factor level, the - grabs only the 2, giving (-2) + 3 = 1. The same branch treats a unary + as a no-op and nests naturally, so 3 * -2 reads -2 as the right factor of the multiply, and -(2 + 3) negates the whole parenthesized group.
The errors are a few small guards. When parseFactor is asked for a value but peek() is undefined, the input ended where an operand was required — a dangling operator like 1 + — so it throws. When a ( opens but no matching ) follows, it throws for unbalanced parentheses. And after the top-level parseExpression returns, any leftover token means the parse stopped early — a stray ) in 1 + 2), or a second number in 2 3 — so we throw rather than silently ignore the tail. The tokenizer adds two more: an unrecognized character, so abc and the second * of 2 ** 3 both fail, and an empty token list for the empty or whitespace-only string.
Division by zero is deliberately not treated as an error. parseTerm divides with JavaScript's own /, so 1 / 0 yields Infinity and 0 / 0 yields NaN, exactly as they would anywhere else in JS. A well-formed expression that happens to divide by zero has a defined IEEE-754 answer; calling it malformed would surprise the caller. If your product needs it to throw instead, that is a one-line guard in parseTerm — see Going further.
Take 2 + 3 * 4. After tokenizing we hold [2] [+] [3] [*] [4] with pos = 0.
parseExpression calls parseTerm, which calls parseFactor. parseFactor sees the number 2, consumes it, returns 2. Back in parseTerm, the next token is +, which is neither * nor /, so the term is just 2. parseExpression now holds value = 2.+, so parseExpression consumes it and calls parseTerm again for the right side. This is the decisive move: the right operand of + is a whole term, so it will swallow the multiplication before the + ever combines anything.parseTerm calls parseFactor, which reads 3. Back in parseTerm, the next token is * — a match. It consumes *, calls parseFactor for 4, and folds 3 * 4 = 12. The next token is undefined, so the term loop ends and parseTerm returns 12.parseExpression, we fold the +: value = 2 + 12 = 14. No tokens remain, the loop ends, and pos is at the end — no leftover-token error. The result is 14.Precedence was never checked explicitly. It is baked into the call order: parseExpression asked parseTerm for the right side of the +, and parseTerm consumed the 3 * 4 as a unit before handing back a single value.
Contrast (2 + 3) * 4. Now parseTerm's first parseFactor sees a (, so it recurses into parseExpression for the inner group, which returns 5, and consumes the ). Back in parseTerm, the next token is *, so it folds 5 * 4 = 20. The parentheses forced the addition to happen first by making it a factor — a self-contained value the multiply then consumes.
2 + 3 * 4 equal 20. Fix: one parse function per precedence level, so a term consumes its whole * and / chain before an outer + can see it.- above the binary + and -, and -2 + 3 wrongly becomes -5. Keep it in parseFactor so it binds tightest and negates only the immediate factor.value = term OP parseExpression()) makes 10 - 3 - 2 compute as 10 - (3 - 2) = 9. Fold in a loop as you go, so it is (10 - 3) - 2 = 5.parseExpression finishes, 1 + 2) and 2 3 silently return 3 and 2. After the top-level parse, confirm every token was consumed and throw if not.peek().value with no if (!token) guard throws Cannot read properties of undefined on 1 +, which points at the wrong thing. Guard the empty peek and throw a clear "unexpected end of expression."power level between factor and the operators above it. ** is right-associative, so 2 ** 3 ** 2 is 2 ** (3 ** 2) = 512; that means parsePower recurses on its right operand instead of looping like parseTerm.factor also be an identifier looked up in a scope, or a call like max(2, 3). The tokenizer grows an identifier rule and parseFactor a lookup, but the precedence skeleton is unchanged.1 / 0 should be rejected rather than returning Infinity, guard it in parseTerm: when the operator is / and the right factor is 0, throw before dividing.Keep practising the same patterns with a nearby challenge.
No submissions yet
Share your approach and start the discussion.