From f7227f4e62b33449f63529a41a8c1aebc0dcd373 Mon Sep 17 00:00:00 2001 From: HadiAyache <55764504+HadiAyache@users.noreply.github.com> Date: Tue, 17 Feb 2026 01:23:15 -0800 Subject: [PATCH] Fix operator precedence grouping for */ and +/- (#6993) * Fix operator precedence grouping for */ and +/- * Add release note for #6993 * Fix exponent associativity and add regression test --------- Co-authored-by: Hadi Ayache --- .../loot-core/src/shared/arithmetic.test.ts | 11 ++++++ packages/loot-core/src/shared/arithmetic.ts | 37 +++++++++++++------ upcoming-release-notes/6993.md | 6 +++ 3 files changed, 42 insertions(+), 12 deletions(-) create mode 100644 upcoming-release-notes/6993.md diff --git a/packages/loot-core/src/shared/arithmetic.test.ts b/packages/loot-core/src/shared/arithmetic.test.ts index 3b41afb368..7cb789f9f7 100644 --- a/packages/loot-core/src/shared/arithmetic.test.ts +++ b/packages/loot-core/src/shared/arithmetic.test.ts @@ -39,6 +39,17 @@ describe('arithmetic', () => { expect(evalArithmetic('20^3 - 5 * (10 / 2)')).toEqual(7975); }); + test('handles exponent as right-associative', () => { + expect(evalArithmetic('2^3^2')).toEqual(512); + }); + + test('handles same-precedence operators left-to-right', () => { + expect(evalArithmetic('24 / 3 * 2')).toEqual(16); + expect(evalArithmetic('24 * 3 / 2')).toEqual(36); + expect(evalArithmetic('10 - 2 + 1')).toEqual(9); + expect(evalArithmetic('10 + 2 - 1')).toEqual(11); + }); + test('respects current number format', () => { expect(evalArithmetic('1,222.45')).toEqual(1222.45); diff --git a/packages/loot-core/src/shared/arithmetic.ts b/packages/loot-core/src/shared/arithmetic.ts index 5e69c79e72..ed26a40135 100644 --- a/packages/loot-core/src/shared/arithmetic.ts +++ b/packages/loot-core/src/shared/arithmetic.ts @@ -91,21 +91,34 @@ function parseParens(state: ParserState): AstNode { return parsePrimary(state); } -function makeOperatorParser(...ops: T) { - type ParserFn = (state: ParserState) => AstNode; - return ops.reduce((prevParser: ParserFn, op: Operator) => { - return (state: ParserState) => { - let node = prevParser(state); - while (nextOperator(state, op)) { - node = { op, left: node, right: prevParser(state) }; - } - return node; - }; - }, parseParens); +function parseExponent(state: ParserState): AstNode { + let node = parseParens(state); + if (nextOperator(state, '^')) { + node = { op: '^', left: node, right: parseExponent(state) }; + } + return node; +} + +function parseMultiplicative(state: ParserState): AstNode { + let node = parseExponent(state); + while (char(state) === '*' || char(state) === '/') { + const op = next(state) as '*' | '/'; + node = { op, left: node, right: parseExponent(state) }; + } + return node; +} + +function parseAdditive(state: ParserState): AstNode { + let node = parseMultiplicative(state); + while (char(state) === '+' || char(state) === '-') { + const op = next(state) as '+' | '-'; + node = { op, left: node, right: parseMultiplicative(state) }; + } + return node; } // These operators go from high to low order of precedence -const parseOperator = makeOperatorParser('^', '/', '*', '-', '+'); +const parseOperator = parseAdditive; function parse(expression: string): AstNode { const state = { str: expression.replace(/\s/g, ''), index: 0 }; diff --git a/upcoming-release-notes/6993.md b/upcoming-release-notes/6993.md new file mode 100644 index 0000000000..8fa5bedd96 --- /dev/null +++ b/upcoming-release-notes/6993.md @@ -0,0 +1,6 @@ +--- +category: Bugfix +authors: [HadiAyache] +--- + +Fix arithmetic expression parsing so operators with the same precedence (`*`/`/`, `+`/`-`) are evaluated left-to-right.