| new file mode 100644 |
| index 0000000..4a51561 |
| --- /dev/null |
| +++ b/internal/sheets/calc/translate.go |
| @@ -0,0 +1,124 @@ |
| +package calc |
| + |
| +import "strconv" |
| + |
| +// Translate offsets every relative reference in n by (dRow, dCol) and |
| +// returns a new tree. Absolute components are untouched — that is the whole |
| +// meaning of the dollar sign. |
| +// |
| +// This is the pass stable row and column identity does NOT remove. Ids make |
| +// insert and delete free; copy and fill are positional by definition, so the |
| +// offset has to be applied at paste time. See spec section 4.2. |
| +func Translate(n Node, dRow, dCol int) Node { |
| + switch t := n.(type) { |
| + case *Ref: |
| + out := *t |
| + if !out.AbsRow { |
| + out.RowPos += dRow |
| + } |
| + if !out.AbsCol { |
| + out.ColPos += dCol |
| + } |
| + return &out |
| + case *Range: |
| + return &Range{ |
| + From: Translate(t.From, dRow, dCol).(*Ref), |
| + To: Translate(t.To, dRow, dCol).(*Ref), |
| + } |
| + case *Binary: |
| + return &Binary{Op: t.Op, |
| + Left: Translate(t.Left, dRow, dCol), |
| + Right: Translate(t.Right, dRow, dCol)} |
| + case *Unary: |
| + return &Unary{Op: t.Op, X: Translate(t.X, dRow, dCol)} |
| + case *Call: |
| + args := make([]Node, len(t.Args)) |
| + for i, a := range t.Args { |
| + args[i] = Translate(a, dRow, dCol) |
| + } |
| + return &Call{Name: t.Name, Args: args} |
| + default: |
| + return n // Num, Str, Bool have no references |
| + } |
| +} |
| + |
| +// 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 |
| +// silently turn an absolute reference into a relative one the next time it |
| +// was copied. |
| +func Render(n Node) string { |
| + switch t := n.(type) { |
| + case *Num: |
| + return strconv.FormatFloat(t.V, 'g', -1, 64) |
| + case *Str: |
| + return `"` + quoteStr(t.V) + `"` |
| + case *Bool: |
| + if t.V { |
| + return "TRUE" |
| + } |
| + return "FALSE" |
| + case *Ref: |
| + return renderRef(t) |
| + case *Range: |
| + return renderRef(t.From) + ":" + renderRef(t.To) |
| + case *Unary: |
| + return t.Op + Render(t.X) |
| + case *Binary: |
| + return Render(t.Left) + t.Op + Render(t.Right) |
| + case *Call: |
| + args := make([]string, len(t.Args)) |
| + for i, a := range t.Args { |
| + args[i] = Render(a) |
| + } |
| + return t.Name + "(" + joinComma(args) + ")" |
| + default: |
| + return "" |
| + } |
| +} |
| + |
| +// 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. |
| +func renderRef(r *Ref) string { |
| + s := "" |
| + if r.AbsCol { |
| + s += "$" |
| + } |
| + s += ColLabel(r.ColPos) |
| + if r.AbsRow { |
| + s += "$" |
| + } |
| + s += strconv.Itoa(r.RowPos + 1) |
| + return s |
| +} |
| + |
| +// quoteStr doubles embedded double quotes, the inverse of lexString's |
| +// unescaping. |
| +func quoteStr(s string) string { |
| + out := make([]byte, 0, len(s)) |
| + for i := 0; i < len(s); i++ { |
| + if s[i] == '"' { |
| + out = append(out, '"', '"') |
| + continue |
| + } |
| + out = append(out, s[i]) |
| + } |
| + return string(out) |
| +} |
| + |
| +func joinComma(parts []string) string { |
| + out := "" |
| + for i, p := range parts { |
| + if i > 0 { |
| + out += "," |
| + } |
| + out += p |
| + } |
| + return out |
| +} |