| index 4a51561..7421ab7 100644 |
| --- a/internal/sheets/calc/translate.go |
| +++ b/internal/sheets/calc/translate.go |
| @@ -45,13 +45,17 @@ func Translate(n Node, dRow, dCol int) Node { |
| // Render is the inverse of Parse for the node shapes Parse can produce: |
| // numbers, strings (re-quoted with `"` doubled), booleans, references and |
| // ranges (with both absolute markers preserved in position), unary minus, |
| -// left-associative binary operators, and upper-cased function calls. It |
| -// does not attempt to reproduce the source's original parenthesisation or |
| -// whitespace — only a re-parseable, semantically equivalent formula. This |
| -// is also what the formula bar shows, so the round trip on absolute |
| -// markers matters beyond the tests: a Render that dropped a "$" would |
| +// binary operators, and upper-cased function calls. It does not reproduce |
| +// the source's original whitespace or its exact parenthesisation — a |
| +// redundant paren the source didn't need is never replayed — but for every |
| +// tree Parse can produce, Parse(Render(n)) reproduces a tree equal to n: |
| +// Render inserts parentheses wherever precedence or associativity would |
| +// otherwise regroup the expression, the mirror of the precedence-climbing |
| +// parser above. This is also what the formula bar shows, so both |
| +// guarantees matter beyond the tests: a Render that dropped a "$" would |
| // silently turn an absolute reference into a relative one the next time it |
| -// was copied. |
| +// was copied, and a Render that dropped a needed paren would silently |
| +// change the value a cell computes. |
| func Render(n Node) string { |
| switch t := n.(type) { |
| case *Num: |
| @@ -68,9 +72,10 @@ func Render(n Node) string { |
| case *Range: |
| return renderRef(t.From) + ":" + renderRef(t.To) |
| case *Unary: |
| - return t.Op + Render(t.X) |
| + return t.Op + renderOperand(t.X, precUnary, false) |
| case *Binary: |
| - return Render(t.Left) + t.Op + Render(t.Right) |
| + p := binaryPrec(t.Op) |
| + return renderOperand(t.Left, p, false) + t.Op + renderOperand(t.Right, p, true) |
| case *Call: |
| args := make([]string, len(t.Args)) |
| for i, a := range t.Args { |
| @@ -82,6 +87,86 @@ func Render(n Node) string { |
| } |
| } |
| |
| +// Precedence levels, loosest to tightest, matching Parse's grammar |
| +// (comparison, concat, additive, term, power, then unary — see the |
| +// precedence note on Parse). Atoms (numbers, strings, refs, ranges, |
| +// calls) never need parenthesising as anyone's child, so they sort above |
| +// even unary. |
| +const ( |
| + precComparison = iota + 1 |
| + precConcat |
| + precAdditive |
| + precTerm |
| + precPower |
| + precUnary |
| + precAtom |
| +) |
| + |
| +// binaryPrec maps a Binary's operator to its precedence level. |
| +func binaryPrec(op string) int { |
| + switch op { |
| + case "=", "<>", "<", "<=", ">", ">=": |
| + return precComparison |
| + case "&": |
| + return precConcat |
| + case "+", "-": |
| + return precAdditive |
| + case "*", "/": |
| + return precTerm |
| + case "^": |
| + return precPower |
| + default: |
| + return precAtom |
| + } |
| +} |
| + |
| +// nodePrec reports the precedence level a node binds at when it appears as |
| +// someone else's operand: a Binary's by its operator, a Unary's fixed at |
| +// precUnary (it parses tighter than any binary operator but looser than |
| +// nothing — see the -2^2 = 4 note on Parse), everything else at precAtom. |
| +func nodePrec(n Node) int { |
| + switch t := n.(type) { |
| + case *Binary: |
| + return binaryPrec(t.Op) |
| + case *Unary: |
| + return precUnary |
| + default: |
| + return precAtom |
| + } |
| +} |
| + |
| +// renderOperand renders n as a child sitting in a slot of precedence |
| +// parentPrec, adding parentheses exactly when omitting them would let the |
| +// child regroup under re-parsing: when it binds strictly looser than the |
| +// parent, or when it binds at exactly the parent's precedence on the side |
| +// associativity doesn't protect (see samePrecedenceNeedsParens). |
| +func renderOperand(n Node, parentPrec int, isRight bool) string { |
| + cp := nodePrec(n) |
| + needParens := cp < parentPrec || (cp == parentPrec && samePrecedenceNeedsParens(parentPrec, isRight)) |
| + s := Render(n) |
| + if needParens { |
| + return "(" + s + ")" |
| + } |
| + return s |
| +} |
| + |
| +// samePrecedenceNeedsParens decides, for a child at exactly its parent's |
| +// precedence, whether it still needs parens to reparse into the same tree. |
| +// Every level Parse builds is left-associative except ^, so a |
| +// left-associative parent needs parens around a same-precedence RIGHT |
| +// child (8-(3-1) must keep its parens: "8-3-1" reparses as (8-3)-1), while |
| +// the right-associative power needs them around a same-precedence LEFT |
| +// child instead ((2^3)^2 must keep its parens: "2^3^2" reparses as |
| +// 2^(3^2)). The unary slot (parentPrec == precUnary, always called with |
| +// isRight == false) falls into the left-associative branch, which is also |
| +// the answer it wants: chained unary minus ("--A1") never needs parens. |
| +func samePrecedenceNeedsParens(parentPrec int, isRight bool) bool { |
| + if parentPrec == precPower { |
| + return !isRight |
| + } |
| + return isRight |
| +} |
| + |
| // renderRef reproduces a single reference's A1 text, including whichever |
| // dollar signs are set: neither ($A1 has one, A$1 has the other, $A$1 has |
| // both), matching how decodeRef reads them back. |