The parser keeps $ markers on references and ranges, and a printer turns a formula back into text (ranges as A1:B3, functions without @), so rewritten formulas are stored in Sheets' spelling. Percent is now its own node so it prints back as typed, and #REF! parses. On top of that the engine can: - copy and paste a range, shifting relative references by the paste offset and leaving $absolute parts alone; references pushed off the sheet become #REF!. Pastes tile when the destination is an exact multiple of the copied size, and can paste values only; - move a range (cut and paste): references to moved cells follow them, references to overwritten cells become #REF!; - insert and delete rows and columns, rewriting every reference: ranges grow and shrink as in Sheets, references to deleted cells become #REF!, and column widths shift along; - fill down, fill right, and fill one entry into a range. Each is one undo step. FuzzPrint checks that printing a parsed formula parses back to the same expression.
- commit
- 14fe69c2c37dd02aea07b6b235b6efe28ae218e1
- parent
- 0d885ce92063f9f2cc04e16a73640ae13ef27ddc
- author
- Dami <dami.etoile@gmail.com>
| M | internal/ | 50 | ++++- |
| M | internal/ | 8 | + |
| A | internal/ | 295 | +++++++++++++++++++++++++ |
| M | internal/ | 62 | ++++- |
| A | internal/ | 100 | ++++++++ |
| A | internal/ | 196 | ++++++++++++++++ |
| A | internal/ | 361 | ++++++++++++++++++++++++++++++ |
| M | internal/ | 2 | +- |
@@ -53,21 +53,57 @@ func ParseCol(s string) (int, bool) {
5353 // ParseAddr parses an A1-style reference. Absolute markers ($A$1) are5454 // accepted and ignored.5555 func ParseAddr(s string) (Addr, bool) {56- s = strings.ReplaceAll(s, "$", "")56+ a, _, ok := parseRef(s)57+ return a, ok58+}59+60+// absFlags records which parts of a reference are absolute ($A$1). Copying61+// a formula shifts only the relative parts.62+type absFlags uint863+64+const (65+ absCol absFlags = 1 << iota66+ absRow67+)68+69+// parseRef parses a reference written as A1, $A1, A$1 or $A$1.70+func parseRef(s string) (Addr, absFlags, bool) {71+ var abs absFlags72+ if rest, ok := strings.CutPrefix(s, "$"); ok {73+ s, abs = rest, absCol74+ }5775 i := 05876 for i < len(s) && isLetter(s[i]) {5977 i++6078 }6179 col, ok := ParseCol(s[:i])62- if !ok || i == len(s) {63- return Addr{}, false80+ if !ok {81+ return Addr{}, 0, false82+ }83+ digits := s[i:]84+ if rest, ok := strings.CutPrefix(digits, "$"); ok {85+ digits, abs = rest, abs|absRow6486 }65- row, err := strconv.Atoi(s[i:])66- if err != nil || s[i] == '+' || s[i] == '-' {67- return Addr{}, false87+ if digits == "" || strings.ContainsFunc(digits, func(r rune) bool { return r < '0' || r > '9' }) {88+ return Addr{}, 0, false6889 }90+ row, err := strconv.Atoi(digits)6991 a := Addr{Col: col, Row: row - 1}70- return a, a.Valid()92+ return a, abs, err == nil && a.Valid()93+}94+95+// refString writes a with its absolute markers, e.g. $A1.96+func refString(a Addr, abs absFlags) string {97+ var b strings.Builder98+ if abs&absCol != 0 {99+ b.WriteByte('$')100+ }101+ b.WriteString(ColName(a.Col))102+ if abs&absRow != 0 {103+ b.WriteByte('$')104+ }105+ b.WriteString(strconv.Itoa(a.Row + 1))106+ return b.String()71107 }72108 73109 // Rect is an inclusive rectangular range of cells.@@ -94,6 +94,8 @@ func eval(n Node, get lookup) Value {
9494 return get(n.a)9595 case nameNode:9696 return ErrName97+ case refErrNode:98+ return ErrRef9799 case rangeNode:98100 // A range outside a function: Sheets uses the top-left cell here99101 // for single-cell ranges and #VALUE! otherwise.@@ -119,6 +121,12 @@ func eval(n Node, get lookup) Value {
119121 return *err120122 }121123 return boolean(f == 0)124+ case "%":125+ f, err := toNum(x)126+ if err != nil {127+ return *err128+ }129+ return num(f / 100)122130 }123131 return x // unary + is identity124132 case binaryNode:@@ -0,0 +1,295 @@
1+package sheet2+3+import (4+ "errors"5+ "strconv"6+)7+8+// Errors returned by operations that would lose data or don't fit.9+var (10+ ErrPushedOff = errors.New("There's data at the edge of the sheet that would be pushed off")11+ ErrPasteEdge = errors.New("The paste doesn't fit: it would go past the edge of the sheet")12+ ErrFillTooBig = errors.New("The selection is too large to fill")13+)14+15+// maxFill caps how many cells a paste or fill writes at once, so an16+// accidental whole-sheet selection can't stall the program.17+const maxFill = 1 << 2018+19+// remap moves every stored cell with cell (dropping those it rejects) and20+// rewrites every formula with rw, as one undo step.21+func (s *Sheet) remap(focus Rect, cell func(Addr) (Addr, bool), rw refRewrite) {22+ next := make(map[Addr]*Cell, len(s.cells))23+ for a, c := range s.cells {24+ if to, ok := cell(a); ok {25+ next[to] = c.rewritten(rw)26+ }27+ }28+ s.change(focus, func() {29+ for a, c := range s.cells {30+ if next[a] != c {31+ s.place(a, next[a])32+ }33+ }34+ for a, c := range next {35+ if s.cells[a] != c {36+ s.place(a, c)37+ }38+ }39+ })40+}41+42+// InsertRows inserts n blank rows before row at, shifting the rows below43+// down and adjusting every reference to them.44+func (s *Sheet) InsertRows(at, n int) error {45+ return s.insert(true, at, n)46+}47+48+// InsertCols inserts n blank columns before column at.49+func (s *Sheet) InsertCols(at, n int) error {50+ return s.insert(false, at, n)51+}52+53+// DeleteRows deletes n rows starting at row at. References to deleted54+// cells become #REF!; ranges that lose some of their rows shrink.55+func (s *Sheet) DeleteRows(at, n int) {56+ s.restructure(true, span{at, -n, MaxRows})57+}58+59+// DeleteCols deletes n columns starting at column at.60+func (s *Sheet) DeleteCols(at, n int) {61+ s.restructure(false, span{at, -n, MaxCols})62+}63+64+func (s *Sheet) insert(rows bool, at, n int) error {65+ size := MaxCols66+ if rows {67+ size = MaxRows68+ }69+ for a := range s.cells {70+ line := a.Col71+ if rows {72+ line = a.Row73+ }74+ if line >= at && line >= size-n {75+ return ErrPushedOff76+ }77+ }78+ s.restructure(rows, span{at, n, size})79+ return nil80+}81+82+func (s *Sheet) restructure(rows bool, sp span) {83+ cell, rng := axisRewrite(rows, sp)84+ last := sp.at + max(sp.n, -sp.n) - 185+ focus := colRect(sp.at, min(last, MaxCols-1))86+ if rows {87+ focus = rowRect(sp.at, min(last, MaxRows-1))88+ }89+ s.change(focus, func() {90+ s.remap(focus, cell, relocate(cell, rng))91+ if !rows {92+ s.shiftWidths(sp)93+ }94+ })95+}96+97+// shiftWidths moves column widths along with inserted or deleted columns.98+func (s *Sheet) shiftWidths(sp span) {99+ next := map[int]int{}100+ for c, w := range s.widths {101+ if to, ok := sp.point(c); ok {102+ next[to] = w103+ }104+ }105+ for c := range s.widths {106+ if _, ok := next[c]; !ok {107+ s.setWidth(c, 0)108+ }109+ }110+ for c, w := range next {111+ if s.widths[c] != w {112+ s.setWidth(c, w)113+ }114+ }115+}116+117+// Move moves the cells in src so its top-left corner lands on to, as118+// cut and paste does in Sheets. Formulas anywhere that referred to the119+// moved cells follow them; references to cells the move overwrote become120+// #REF!. It returns the destination range.121+func (s *Sheet) Move(src Rect, to Addr) (Rect, error) {122+ dc, dr := to.Col-src.From.Col, to.Row-src.From.Row123+ dst := Rect{to, Addr{Col: src.To.Col + dc, Row: src.To.Row + dr}}124+ if !dst.To.Valid() {125+ return Rect{}, ErrPasteEdge126+ }127+ shift := func(a Addr) Addr { return Addr{Col: a.Col + dc, Row: a.Row + dr} }128+ cell := func(a Addr) (Addr, bool) {129+ switch {130+ case src.Contains(a):131+ return shift(a), true132+ case dst.Contains(a):133+ return a, false134+ }135+ return a, true136+ }137+ rng := func(r Rect) (Rect, bool) {138+ if src.Contains(r.From) && src.Contains(r.To) {139+ return Rect{shift(r.From), shift(r.To)}, true140+ }141+ return r, true142+ }143+ s.remap(dst, cell, relocate(cell, rng))144+ return dst, nil145+}146+147+// Clip is a copied range: snapshots of its cells taken at copy time.148+type Clip struct {149+ Src Rect150+ cells map[Addr]*Cell // by offset from Src.From151+}152+153+// Copy snapshots the cells in r for pasting.154+func (s *Sheet) Copy(r Rect) *Clip {155+ c := &Clip{Src: r, cells: map[Addr]*Cell{}}156+ for _, a := range s.cellsIn(r) {157+ c.cells[Addr{Col: a.Col - r.From.Col, Row: a.Row - r.From.Row}] = s.cells[a].clone()158+ }159+ return c160+}161+162+// Size returns the clip's width and height in cells.163+func (c *Clip) Size() (cols, rows int) {164+ return c.Src.To.Col - c.Src.From.Col + 1, c.Src.To.Row - c.Src.From.Row + 1165+}166+167+// Text returns the clip's values as displayed in General format, row by168+// row, for the system clipboard.169+func (c *Clip) Text() [][]string {170+ cols, rows := c.Size()171+ out := make([][]string, rows)172+ for r := range out {173+ out[r] = make([]string, cols)174+ for col := range out[r] {175+ if cell := c.cells[Addr{Col: col, Row: r}]; cell != nil {176+ out[r][col] = cell.Value.String()177+ }178+ }179+ }180+ return out181+}182+183+// Paste writes the clip into dst and returns the range written. Formulas184+// shift their relative references by the distance pasted, as in Sheets;185+// with values set, only the computed values are pasted. Like Sheets, a186+// destination that is an exact multiple of the clip's size is tiled;187+// otherwise the clip is pasted once at dst's top-left corner.188+func (s *Sheet) Paste(c *Clip, dst Rect, values bool) (Rect, error) {189+ cols, rows := c.Size()190+ w, h := dst.To.Col-dst.From.Col+1, dst.To.Row-dst.From.Row+1191+ if w%cols != 0 || h%rows != 0 {192+ dst.To = Addr{Col: dst.From.Col + cols - 1, Row: dst.From.Row + rows - 1}193+ w, h = cols, rows194+ }195+ if !dst.To.Valid() {196+ return Rect{}, ErrPasteEdge197+ }198+ if w*h > maxFill {199+ return Rect{}, ErrFillTooBig200+ }201+ s.change(dst, func() {202+ for r := range h {203+ for col := range w {204+ off := Addr{Col: col % cols, Row: r % rows}205+ to := Addr{Col: dst.From.Col + col, Row: dst.From.Row + r}206+ from := Addr{Col: c.Src.From.Col + off.Col, Row: c.Src.From.Row + off.Row}207+ s.pasteCell(to, c.cells[off], to.Col-from.Col, to.Row-from.Row, values)208+ }209+ }210+ })211+ return dst, nil212+}213+214+func (s *Sheet) pasteCell(a Addr, c *Cell, dc, dr int, values bool) {215+ switch {216+ case c == nil:217+ if s.cells[a] != nil {218+ s.place(a, nil)219+ }220+ case values:221+ s.put(a, valueInput(c.Value))222+ default:223+ s.place(a, c.rewritten(shiftRefs(dc, dr)).clone())224+ }225+}226+227+// valueInput is the entry that stores v as a constant. Text that would228+// read as a number, boolean or formula gets a leading ' to stay text.229+func valueInput(v Value) string {230+ switch v.Kind {231+ case Number:232+ return strconv.FormatFloat(v.Num, 'f', -1, 64)233+ case Text:234+ if n, _ := classify(v.Str); n != nil || (len(v.Str) > 0 && v.Str[0] == '\'') {235+ return "'" + v.Str236+ }237+ }238+ return v.String()239+}240+241+// FillDown copies the top row of r into the rest of it, adjusting242+// references (Ctrl+D). A single-row range fills from the row above.243+func (s *Sheet) FillDown(r Rect) (Rect, error) {244+ src := Rect{r.From, Addr{Col: r.To.Col, Row: r.From.Row}}245+ if r.From.Row == r.To.Row {246+ if r.From.Row == 0 {247+ return r, nil248+ }249+ src.From.Row, src.To.Row = r.From.Row-1, r.From.Row-1250+ } else {251+ r.From.Row++252+ }253+ return s.Paste(s.Copy(src), r, false)254+}255+256+// FillRight copies the left column of r into the rest of it (Ctrl+R). A257+// single-column range fills from the column to the left.258+func (s *Sheet) FillRight(r Rect) (Rect, error) {259+ src := Rect{r.From, Addr{Col: r.From.Col, Row: r.To.Row}}260+ if r.From.Col == r.To.Col {261+ if r.From.Col == 0 {262+ return r, nil263+ }264+ src.From.Col, src.To.Col = r.From.Col-1, r.From.Col-1265+ } else {266+ r.From.Col++267+ }268+ return s.Paste(s.Copy(src), r, false)269+}270+271+// FillEntry stores input in every cell of r as if it had been typed at272+// origin and copied to each cell, adjusting references (Ctrl+Enter).273+func (s *Sheet) FillEntry(r Rect, origin Addr, input string) error {274+ w, h := r.To.Col-r.From.Col+1, r.To.Row-r.From.Row+1275+ if w*h > maxFill {276+ return ErrFillTooBig277+ }278+ if _, err := classify(input); err != nil {279+ return err280+ }281+ return s.Batch(r, func() error {282+ if err := s.put(origin, input); err != nil {283+ return err284+ }285+ c := s.cells[origin].clone()286+ for row := r.From.Row; row <= r.To.Row; row++ {287+ for col := r.From.Col; col <= r.To.Col; col++ {288+ if a := (Addr{Col: col, Row: row}); a != origin {289+ s.pasteCell(a, c, a.Col-origin.Col, a.Row-origin.Row, false)290+ }291+ }292+ }293+ return nil294+ })295+}@@ -10,13 +10,22 @@ import (
1010 type Node any1111 1212 type (13- numLit struct{ v float64 }14- strLit struct{ v string }15- boolLit struct{ v bool }16- refNode struct{ a Addr }17- rangeNode struct{ r Rect }18- nameNode struct{ name string } // an identifier that isn't a cell; #NAME? until names exist19- unaryNode struct {13+ numLit struct{ v float64 }14+ strLit struct{ v string }15+ boolLit struct{ v bool }16+ refNode struct {17+ a Addr18+ abs absFlags19+ }20+ // rangeNode is kept normalized (r.From is the top-left corner); abs21+ // holds the absolute markers of r.From and r.To.22+ rangeNode struct {23+ r Rect24+ abs [2]absFlags25+ }26+ refErrNode struct{} // a reference to deleted cells: #REF!27+ nameNode struct{ name string } // an identifier that isn't a cell; #NAME? until names exist28+ unaryNode struct {2029 op string2130 x Node2231 }@@ -36,9 +45,10 @@ const (
3645 tokEOF tokKind = iota3746 tokNum3847 tokStr39- tokIdent // cell reference, TRUE/FALSE or a name40- tokFunc // identifier directly followed by "("41- tokOp48+ tokIdent // cell reference, TRUE/FALSE or a name49+ tokFunc // identifier directly followed by "("50+ tokOp // operators and punctuation51+ tokRefErr // #REF!, left behind when a referenced cell is deleted4252 )4353 4454 type token struct {@@ -131,6 +141,9 @@ func lex(src string) ([]token, error) {
131141 kind = tokFunc // @PI without parentheses132142 }133143 toks = append(toks, token{kind, name, start})144+ case c == '#' && strings.HasPrefix(strings.ToUpper(src[i:]), "#REF!"):145+ i += len("#REF!")146+ toks = append(toks, token{tokRefErr, "#REF!", start})134147 case c == '#':135148 end := strings.IndexByte(src[i+1:], '#')136149 if end < 0 {@@ -227,7 +240,7 @@ func (p *parser) expr(minPower int) (Node, error) {
227240 for {228241 if p.isOp("%") && percentPower > minPower {229242 p.next()230- left = binaryNode{op: "/", l: left, r: numLit{100}}243+ left = unaryNode{op: "%", x: left}231244 continue232245 }233246 t := p.peek()@@ -259,6 +272,8 @@ func (p *parser) prefix() (Node, error) {
259272 return p.ident(t)260273 case tokFunc:261274 return p.call(t)275+ case tokRefErr:276+ return refErrNode{}, nil262277 case tokOp:263278 switch t.text {264279 case "(":@@ -291,7 +306,7 @@ func (p *parser) prefix() (Node, error) {
291306 }292307 293308 func (p *parser) ident(t token) (Node, error) {294- a, isRef := ParseAddr(t.text)309+ a, abs, isRef := parseRef(t.text)295310 if !isRef {296311 switch t.text {297312 case "TRUE":@@ -304,13 +319,30 @@ func (p *parser) ident(t token) (Node, error) {
304319 if p.isOp(":") || p.isOp("..") {305320 sep := p.next()306321 end := p.next()307- b, ok := ParseAddr(end.text)322+ b, bAbs, ok := parseRef(end.text)308323 if end.kind != tokIdent || !ok {309324 return nil, &ParseError{end.pos, "Expected a cell after " + sep.text}310325 }311- return rangeNode{NewRect(a, b)}, nil326+ return newRange(a, b, abs, bAbs), nil327+ }328+ return refNode{a, abs}, nil329+}330+331+// newRange builds a normalized range from two corners as written. Each332+// absolute marker stays with its column or row, so $B1:A$2 becomes A1:$B$2.333+func newRange(a, b Addr, aAbs, bAbs absFlags) rangeNode {334+ swap := func(bit absFlags) {335+ aAbs, bAbs = aAbs&^bit|bAbs&bit, bAbs&^bit|aAbs&bit336+ }337+ if a.Col > b.Col {338+ a.Col, b.Col = b.Col, a.Col339+ swap(absCol)340+ }341+ if a.Row > b.Row {342+ a.Row, b.Row = b.Row, a.Row343+ swap(absRow)312344 }313- return refNode{a}, nil345+ return rangeNode{Rect{a, b}, [2]absFlags{aAbs, bAbs}}314346 }315347 316348 func (p *parser) call(t token) (Node, error) {@@ -0,0 +1,100 @@
1+package sheet2+3+import (4+ "math"5+ "strconv"6+ "strings"7+)8+9+// formulaText prints a parsed formula back to text, with a leading "=".10+// Rewritten formulas (after a paste or an inserted row) are stored this11+// way, in Sheets' spelling: ranges as A1:B3, functions without @.12+func formulaText(n Node) string {13+ var b strings.Builder14+ b.WriteByte('=')15+ printNode(&b, n)16+ return b.String()17+}18+19+// atomPower is the binding power of nodes that never need parentheses.20+const atomPower = 10021+22+// power returns how tightly n binds, using the parser's binding powers, so23+// the printer adds parentheses exactly where the parser needs them.24+func power(n Node) int {25+ switch n := n.(type) {26+ case binaryNode:27+ return infixPower[n.op]28+ case unaryNode:29+ switch n.op {30+ case "%":31+ return percentPower32+ case "#NOT#":33+ return notPower34+ }35+ return unaryPower36+ }37+ return atomPower38+}39+40+func printNode(b *strings.Builder, n Node) {41+ switch n := n.(type) {42+ case numLit:43+ b.WriteString(numText(n.v))44+ case strLit:45+ b.WriteString(`"` + strings.ReplaceAll(n.v, `"`, `""`) + `"`)46+ case boolLit:47+ b.WriteString(strings.ToUpper(strconv.FormatBool(n.v)))48+ case refNode:49+ b.WriteString(refString(n.a, n.abs))50+ case rangeNode:51+ b.WriteString(refString(n.r.From, n.abs[0]) + ":" + refString(n.r.To, n.abs[1]))52+ case refErrNode:53+ b.WriteString("#REF!")54+ case nameNode:55+ b.WriteString(n.name)56+ case unaryNode:57+ if n.op == "%" {58+ printChild(b, n.x, power(n.x) < percentPower)59+ b.WriteByte('%')60+ return61+ }62+ b.WriteString(n.op)63+ printChild(b, n.x, power(n.x) < power(n))64+ case binaryNode:65+ p := infixPower[n.op]66+ // Operators associate to the left, so a right operand of equal67+ // power needs parentheses: 1-(2-3).68+ printChild(b, n.l, power(n.l) < p)69+ b.WriteString(n.op)70+ printChild(b, n.r, power(n.r) <= p)71+ case callNode:72+ b.WriteString(n.fn.Name + "(")73+ for i, a := range n.args {74+ if i > 0 {75+ b.WriteByte(',')76+ }77+ printNode(b, a)78+ }79+ b.WriteByte(')')80+ }81+}82+83+func printChild(b *strings.Builder, n Node, paren bool) {84+ if paren {85+ b.WriteByte('(')86+ }87+ printNode(b, n)88+ if paren {89+ b.WriteByte(')')90+ }91+}92+93+// numText writes a number literal so it parses back to the same float:94+// plainly in everyday ranges, in E notation for very large or small ones.95+func numText(v float64) string {96+ if a := math.Abs(v); a == 0 || (a >= 1e-6 && a < 1e21) {97+ return strconv.FormatFloat(v, 'f', -1, 64)98+ }99+ return strconv.FormatFloat(v, 'E', -1, 64)100+}@@ -0,0 +1,196 @@
1+package sheet2+3+// Reference rewriting: copying a formula shifts its relative references,4+// and moving or inserting and deleting cells updates every reference to5+// the cells that moved, as Sheets does. A reference that can't be kept6+// becomes #REF!.7+8+// refRewrite maps the references in a formula. Either function may return9+// refErrNode.10+type refRewrite struct {11+ ref func(refNode) Node12+ rng func(rangeNode) Node13+}14+15+// rewrite returns n with its references mapped, and whether anything16+// changed. Unchanged subtrees are shared.17+func rewrite(n Node, rw refRewrite) (Node, bool) {18+ switch n := n.(type) {19+ case refNode:20+ out := rw.ref(n)21+ return out, out != Node(n)22+ case rangeNode:23+ out := rw.rng(n)24+ return out, out != Node(n)25+ case unaryNode:26+ x, ok := rewrite(n.x, rw)27+ if ok {28+ n.x = x29+ }30+ return n, ok31+ case binaryNode:32+ l, okL := rewrite(n.l, rw)33+ r, okR := rewrite(n.r, rw)34+ if okL || okR {35+ n.l, n.r = l, r36+ }37+ return n, okL || okR38+ case callNode:39+ var args []Node40+ for i, a := range n.args {41+ if b, ok := rewrite(a, rw); ok {42+ if args == nil {43+ args = append([]Node(nil), n.args...)44+ }45+ args[i] = b46+ }47+ }48+ if args == nil {49+ return n, false50+ }51+ n.args = args52+ return n, true53+ }54+ return n, false55+}56+57+// shiftRefs is the rewrite for copying a formula by (dc, dr): relative58+// parts move, $absolute parts stay, and references pushed off the sheet59+// become #REF!.60+func shiftRefs(dc, dr int) refRewrite {61+ move := func(a Addr, abs absFlags) (Addr, bool) {62+ if abs&absCol == 0 {63+ a.Col += dc64+ }65+ if abs&absRow == 0 {66+ a.Row += dr67+ }68+ return a, a.Valid()69+ }70+ return refRewrite{71+ ref: func(n refNode) Node {72+ a, ok := move(n.a, n.abs)73+ if !ok {74+ return refErrNode{}75+ }76+ return refNode{a, n.abs}77+ },78+ rng: func(n rangeNode) Node {79+ from, ok1 := move(n.r.From, n.abs[0])80+ to, ok2 := move(n.r.To, n.abs[1])81+ if !ok1 || !ok2 {82+ return refErrNode{}83+ }84+ return newRange(from, to, n.abs[0], n.abs[1])85+ },86+ }87+}88+89+// relocate is the rewrite for cells that move: cell maps where each cell90+// went (false if it's gone), and rng maps whole ranges.91+func relocate(cell func(Addr) (Addr, bool), rng func(Rect) (Rect, bool)) refRewrite {92+ return refRewrite{93+ ref: func(n refNode) Node {94+ a, ok := cell(n.a)95+ if !ok {96+ return refErrNode{}97+ }98+ return refNode{a, n.abs}99+ },100+ rng: func(n rangeNode) Node {101+ r, ok := rng(n.r)102+ if !ok {103+ return refErrNode{}104+ }105+ return rangeNode{r, n.abs}106+ },107+ }108+}109+110+// withFormula returns a copy of c holding expression n, printed as its111+// new input. Other fields (formats, say) come along unchanged.112+func (c *Cell) withFormula(n Node) *Cell {113+ cp := c.clone()114+ cp.Input = formulaText(n)115+ cp.setExpr(n)116+ return cp117+}118+119+// rewritten returns c with its references rewritten, or c itself if none120+// changed.121+func (c *Cell) rewritten(rw refRewrite) *Cell {122+ if c.expr == nil || (len(c.refs) == 0 && len(c.ranges) == 0) {123+ return c124+ }125+ n, changed := rewrite(c.expr, rw)126+ if !changed {127+ return c128+ }129+ return c.withFormula(n)130+}131+132+// span inserts (n > 0) or deletes (n < 0) lines starting at index at,133+// along an axis with size lines.134+type span struct{ at, n, size int }135+136+// point maps a line index; false if the line was deleted or pushed off137+// the end.138+func (sp span) point(v int) (int, bool) {139+ switch {140+ case v < sp.at:141+ return v, true142+ case sp.n < 0 && v < sp.at-sp.n:143+ return 0, false144+ }145+ v += sp.n146+ return v, v < sp.size147+}148+149+// interval maps a range's lines lo..hi as Sheets does: inserting inside150+// the range grows it, deleting part of it shrinks it, and deleting all of151+// it leaves #REF!.152+func (sp span) interval(lo, hi int) (int, int, bool) {153+ if sp.n > 0 {154+ lo, okLo := sp.point(lo)155+ if !okLo {156+ return 0, 0, false157+ }158+ hi, _ := sp.point(hi)159+ return lo, min(hi, sp.size-1), true160+ }161+ end := sp.at - sp.n // first line after the deletion162+ if lo >= sp.at && hi < end {163+ return 0, 0, false164+ }165+ if lo >= sp.at {166+ lo = max(lo, end) + sp.n167+ }168+ if hi >= sp.at {169+ hi = max(hi+sp.n, sp.at-1)170+ }171+ return lo, hi, true172+}173+174+// axisRewrite is the cell and range mapping for inserting or deleting rows175+// (rows true) or columns.176+func axisRewrite(rows bool, sp span) (func(Addr) (Addr, bool), func(Rect) (Rect, bool)) {177+ cell := func(a Addr) (Addr, bool) {178+ var ok bool179+ if rows {180+ a.Row, ok = sp.point(a.Row)181+ } else {182+ a.Col, ok = sp.point(a.Col)183+ }184+ return a, ok185+ }186+ rng := func(r Rect) (Rect, bool) {187+ var ok bool188+ if rows {189+ r.From.Row, r.To.Row, ok = sp.interval(r.From.Row, r.To.Row)190+ } else {191+ r.From.Col, r.To.Col, ok = sp.interval(r.From.Col, r.To.Col)192+ }193+ return r, ok194+ }195+ return cell, rng196+}@@ -0,0 +1,361 @@
1+package sheet2+3+import (4+ "maps"5+ "reflect"6+ "strings"7+ "testing"8+)9+10+func TestParseAbsoluteRefs(t *testing.T) {11+ tests := []struct {12+ in string13+ want Node14+ }{15+ {"=A1", refNode{at("A1"), 0}},16+ {"=$A1", refNode{at("A1"), absCol}},17+ {"=A$1", refNode{at("A1"), absRow}},18+ {"=$a$1", refNode{at("A1"), absCol | absRow}},19+ {"=$B$3:A1", rangeNode{NewRect(at("A1"), at("B3")), [2]absFlags{0, absCol | absRow}}},20+ // Each marker stays with its column or row when corners swap.21+ {"=$B1:A$2", rangeNode{NewRect(at("A1"), at("B2")), [2]absFlags{0, absCol | absRow}}},22+ {"=A1..$B2", rangeNode{NewRect(at("A1"), at("B2")), [2]absFlags{0, absCol}}},23+ {"=A$$1", nameNode{"A$$1"}},24+ {"=A1$", nameNode{"A1$"}},25+ {"=#REF!+1", binaryNode{"+", refErrNode{}, numLit{1}}},26+ }27+ for _, tt := range tests {28+ got, err := Parse(tt.in)29+ if err != nil || !reflect.DeepEqual(got, tt.want) {30+ t.Errorf("Parse(%q) = %#v, %v; want %#v", tt.in, got, err, tt.want)31+ }32+ }33+}34+35+func TestPrintFormula(t *testing.T) {36+ tests := []struct{ in, want string }{37+ {"=a1+$b$2*C$3", "=A1+$B$2*C$3"},38+ {"=@SUM(A1..B3; 2)", "=SUM(A1:B3,2)"},39+ {"=(1+2)*3", "=(1+2)*3"},40+ {"=1-(2-3)", "=1-(2-3)"},41+ {"=(1-2)-3", "=1-2-3"},42+ {"=2^3^2", "=2^3^2"},43+ {"=2^(3^2)", "=2^(3^2)"},44+ {"=-2^2", "=-2^2"},45+ {"=(-2)^2", "=(-2)^2"},46+ {"=-(A1+1)", "=-(A1+1)"},47+ {"=50%", "=50%"},48+ {"=(A1+1)%", "=(A1+1)%"},49+ {"=#NOT#(A1=1) #AND# B1", "=#NOT#A1=1#AND#B1"},50+ {"=1+(#NOT#A1)", "=1+(#NOT#A1)"},51+ {`="say ""hi"""&A1`, `="say ""hi"""&A1`},52+ {"=1.5E3+1e-9+2E21", "=1500+1E-09+2E+21"},53+ {"=@PI", "=PI()"},54+ {"=IF(A1>=2,TRUE,profit)", "=IF(A1>=2,TRUE,PROFIT)"},55+ {"=SUM(#REF!)", "=SUM(#REF!)"},56+ {"+A1", "=+A1"},57+ }58+ for _, tt := range tests {59+ n, err := Parse(tt.in)60+ if err != nil {61+ t.Fatalf("Parse(%q): %v", tt.in, err)62+ }63+ if got := formulaText(n); got != tt.want {64+ t.Errorf("print(%q) = %q, want %q", tt.in, got, tt.want)65+ }66+ }67+}68+69+func TestShiftRefs(t *testing.T) {70+ tests := []struct {71+ in string72+ dc, dr int73+ want string74+ }{75+ {"=A1+B2", 1, 2, "=B3+C4"},76+ {"=$A$1+A1", 1, 1, "=$A$1+B2"},77+ {"=$A1+A$1", 2, 3, "=$A4+C$1"},78+ {"=SUM(A1:B2)", 0, 5, "=SUM(A6:B7)"},79+ {"=SUM($A$1:B2)", 1, 1, "=SUM($A$1:C3)"},80+ {"=A2", 0, -1, "=A1"},81+ {"=A2", 0, -2, "=#REF!"},82+ {"=SUM(A1:B2)+1", -1, 0, "=SUM(#REF!)+1"},83+ {"=$A2", -1, 0, "=$A2"},84+ {"=IV1", 1, 0, "=#REF!"},85+ // A mixed range can flip when one corner is anchored.86+ {"=SUM($C1:D1)", -2, 0, "=SUM(B1:$C1)"},87+ }88+ for _, tt := range tests {89+ n, _ := Parse(tt.in)90+ out, _ := rewrite(n, shiftRefs(tt.dc, tt.dr))91+ if got := formulaText(out); got != tt.want {92+ t.Errorf("shift(%q, %d, %d) = %q, want %q", tt.in, tt.dc, tt.dr, got, tt.want)93+ }94+ }95+}96+97+func TestSpanInterval(t *testing.T) {98+ tests := []struct {99+ sp span100+ lo, hi int101+ wLo, wHi int102+ wOK bool103+ name string104+ checkPoint bool105+ }{106+ {span{2, 2, 100}, 0, 1, 0, 1, true, "insert below", false},107+ {span{2, 2, 100}, 1, 3, 1, 5, true, "insert inside grows", false},108+ {span{2, 2, 100}, 2, 3, 4, 5, true, "insert at start shifts", false},109+ {span{2, 2, 100}, 50, 99, 52, 99, true, "insert clamps at the edge", false},110+ {span{2, -2, 100}, 1, 4, 1, 2, true, "delete inside shrinks", false},111+ {span{2, -2, 100}, 2, 4, 2, 2, true, "delete top part", false},112+ {span{2, -2, 100}, 1, 2, 1, 1, true, "delete bottom part", false},113+ {span{2, -2, 100}, 3, 6, 2, 4, true, "delete overlapping start", false},114+ {span{2, -2, 100}, 5, 6, 3, 4, true, "delete above shifts", false},115+ {span{2, -2, 100}, 2, 3, 0, 0, false, "delete all of it", false},116+ }117+ for _, tt := range tests {118+ lo, hi, ok := tt.sp.interval(tt.lo, tt.hi)119+ if ok != tt.wOK || (ok && (lo != tt.wLo || hi != tt.wHi)) {120+ t.Errorf("%s: interval(%d, %d) = %d, %d, %v", tt.name, tt.lo, tt.hi, lo, hi, ok)121+ }122+ }123+}124+125+func TestInsertDeleteRows(t *testing.T) {126+ base := map[string]string{127+ "A1": "1", "A2": "2", "A3": "3", "A4": "4",128+ "B1": "=SUM(A1:A4)", "B2": "=A3*2", "B3": "=$A$4", "B4": "=SUM(A3:A4)",129+ }130+ tests := []struct {131+ name string132+ op func(s *Sheet)133+ want map[string]string134+ vals map[string]float64135+ }{136+ {"insert inside a range", func(s *Sheet) { s.InsertRows(2, 1) }, map[string]string{137+ "A1": "1", "A2": "2", "A4": "3", "A5": "4",138+ "B1": "=SUM(A1:A5)", "B2": "=A4*2", "B4": "=$A$5", "B5": "=SUM(A4:A5)",139+ }, map[string]float64{"B1": 10, "B2": 6, "B4": 4}},140+ {"delete a referenced row", func(s *Sheet) { s.DeleteRows(2, 1) }, map[string]string{141+ "A1": "1", "A2": "2", "A3": "4",142+ "B1": "=SUM(A1:A3)", "B2": "=#REF!*2", "B3": "=SUM(A3:A3)",143+ }, map[string]float64{"B1": 7, "B3": 4}},144+ {"delete every row of a range", func(s *Sheet) { s.DeleteRows(2, 2) }, map[string]string{145+ "A1": "1", "A2": "2", "B1": "=SUM(A1:A2)", "B2": "=#REF!*2",146+ }, map[string]float64{"B1": 3}},147+ }148+ for _, tt := range tests {149+ s := sheetOf(t, base)150+ tt.op(s)151+ if got := inputs(s); !maps.Equal(got, tt.want) {152+ t.Errorf("%s:\n got %v\nwant %v", tt.name, got, tt.want)153+ }154+ for a, v := range tt.vals {155+ if got := s.Value(at(a)); got.Kind != Number || got.Num != v {156+ t.Errorf("%s: %s = %+v, want %v", tt.name, a, got, v)157+ }158+ }159+ for a, in := range tt.want {160+ if strings.Contains(in, "#REF!") && s.Value(at(a)) != ErrRef {161+ t.Errorf("%s: %s = %+v, want #REF!", tt.name, a, s.Value(at(a)))162+ }163+ }164+ // Recalc still follows the rewritten references.165+ s.Set(at("A1"), "101")166+ if got := s.Value(at("B1")).Num; got != tt.vals["B1"]+100 {167+ t.Errorf("%s: B1 after edit = %v", tt.name, got)168+ }169+ // Undo restores the sheet exactly.170+ s.Undo()171+ s.Undo()172+ if got := inputs(s); !maps.Equal(got, base) {173+ t.Errorf("%s: undo left %v", tt.name, got)174+ }175+ }176+}177+178+func TestInsertDeleteColsShiftWidths(t *testing.T) {179+ s := sheetOf(t, map[string]string{"A1": "1", "B1": "2", "C1": "=A1+B1"})180+ s.SetColWidth(1, 15)181+ if err := s.InsertCols(1, 2); err != nil {182+ t.Fatal(err)183+ }184+ if got := inputs(s); got["E1"] != "=A1+D1" || got["D1"] != "2" || s.ColWidth(3) != 15 || s.ColWidth(1) != DefaultWidth {185+ t.Errorf("after insert: %v, width D %d", got, s.ColWidth(3))186+ }187+ s.DeleteCols(0, 1)188+ if got := inputs(s)["D1"]; got != "=#REF!+C1" || s.ColWidth(2) != 15 {189+ t.Errorf("after delete: D1 %q, width C %d", got, s.ColWidth(2))190+ }191+ s.Undo()192+ s.Undo()193+ if got := inputs(s)["C1"]; got != "=A1+B1" || s.ColWidth(1) != 15 {194+ t.Errorf("after undo: C1 %q width B %d", got, s.ColWidth(1))195+ }196+}197+198+func TestInsertRefusesToPushDataOff(t *testing.T) {199+ s := sheetOf(t, map[string]string{"A8192": "last"})200+ if err := s.InsertRows(0, 1); err != ErrPushedOff {201+ t.Errorf("err = %v", err)202+ }203+ if err := s.InsertRows(8191, 1); err != ErrPushedOff {204+ t.Errorf("err = %v", err)205+ }206+ if err := s.InsertRows(8192-3, 1); err != ErrPushedOff {207+ t.Errorf("err = %v", err)208+ }209+ s = sheetOf(t, map[string]string{"A8190": "x", "B1": "=A8192"})210+ if err := s.InsertRows(0, 1); err != nil {211+ t.Fatal(err)212+ }213+ if got := inputs(s)["B2"]; got != "=#REF!" {214+ t.Errorf("reference pushed off the sheet = %q", got)215+ }216+}217+218+func TestCopyPaste(t *testing.T) {219+ tests := []struct {220+ name string221+ src string222+ dst string223+ values bool224+ want map[string]string225+ wrote string226+ }{227+ {"relative shift", "C1", "D3", false, map[string]string{"D3": "=C3+$A$1"}, "D3"},228+ {"off the sheet", "C1", "A1", false, map[string]string{"A1": "=#REF!+$A$1"}, "A1"},229+ {"tiles an exact multiple", "A1:A2", "E1:F4", false, map[string]string{230+ "E1": "1", "E2": "2", "E3": "1", "E4": "2", "F1": "1", "F2": "2", "F3": "1", "F4": "2",231+ }, "E1:F4"},232+ {"pastes once otherwise", "A1:A2", "E1:E3", false, map[string]string{"E1": "1", "E2": "2"}, "E1:E2"},233+ {"values only", "B1:C1", "E5", true, map[string]string{"E5": "'12", "F5": "13"}, "E5:F5"},234+ }235+ for _, tt := range tests {236+ s := sheetOf(t, map[string]string{"A1": "1", "A2": "2", "B1": "'12", "C1": "=B1+$A$1"})237+ src, _ := ParseRange(tt.src)238+ dst, _ := ParseRange(tt.dst)239+ before := inputs(s)240+ wrote, err := s.Paste(s.Copy(src), dst, tt.values)241+ if err != nil || wrote.String() != tt.wrote {242+ t.Errorf("%s: wrote %v, %v", tt.name, wrote, err)243+ }244+ got := inputs(s)245+ for a, in := range tt.want {246+ if got[a] != in {247+ t.Errorf("%s: %s = %q, want %q", tt.name, a, got[a], in)248+ }249+ }250+ if len(got) != len(uniqueKeys(before, tt.want)) {251+ t.Errorf("%s: unexpected cells %v", tt.name, got)252+ }253+ s.Undo()254+ if !maps.Equal(inputs(s), before) {255+ t.Errorf("%s: undo left %v", tt.name, inputs(s))256+ }257+ }258+}259+260+func uniqueKeys(ms ...map[string]string) map[string]bool {261+ out := map[string]bool{}262+ for _, m := range ms {263+ for k := range m {264+ out[k] = true265+ }266+ }267+ return out268+}269+270+func TestPasteBlanksAndEdge(t *testing.T) {271+ s := sheetOf(t, map[string]string{"A1": "1", "C1": "keep", "D1": "gone"})272+ s.Paste(s.Copy(NewRect(at("A1"), at("B1"))), NewRect(at("C1"), at("C1")), false)273+ if got := inputs(s); got["C1"] != "1" || got["D1"] != "" {274+ t.Errorf("blank source cells should clear the destination: %v", got)275+ }276+ if _, err := s.Paste(s.Copy(NewRect(at("A1"), at("B1"))), NewRect(at("IV1"), at("IV1")), false); err != ErrPasteEdge {277+ t.Errorf("err = %v", err)278+ }279+}280+281+func TestMove(t *testing.T) {282+ s := sheetOf(t, map[string]string{283+ "A1": "5", "A2": "=A1*2", "B1": "=A1+A2", "C1": "=SUM(A1:A2)", "C2": "=SUM(A1:A3)", "D1": "old", "E1": "=D1",284+ })285+ dst, err := s.Move(NewRect(at("A1"), at("A2")), at("D1"))286+ if err != nil || dst.String() != "D1:D2" {287+ t.Fatalf("Move = %v, %v", dst, err)288+ }289+ want := map[string]string{290+ "D1": "5", "D2": "=D1*2", "B1": "=D1+D2", "C1": "=SUM(D1:D2)",291+ "C2": "=SUM(A1:A3)", // not wholly inside the moved cells: unchanged292+ "E1": "=#REF!", // pointed at a cell the move overwrote293+ }294+ if got := inputs(s); !maps.Equal(got, want) {295+ t.Errorf("got %v\nwant %v", got, want)296+ }297+ if got := s.Value(at("B1")).Num; got != 15 {298+ t.Errorf("B1 = %v", got)299+ }300+ s.Undo()301+ if got := inputs(s); got["A2"] != "=A1*2" || got["D1"] != "old" || got["E1"] != "=D1" {302+ t.Errorf("undo: %v", got)303+ }304+}305+306+func TestFill(t *testing.T) {307+ s := sheetOf(t, map[string]string{"A1": "1", "B1": "=A1*2", "A2": "2", "A3": "3"})308+ if _, err := s.FillDown(NewRect(at("B1"), at("B3"))); err != nil {309+ t.Fatal(err)310+ }311+ if got := inputs(s); got["B2"] != "=A2*2" || got["B3"] != "=A3*2" || s.Value(at("B3")).Num != 6 {312+ t.Errorf("fill down: %v", got)313+ }314+ s.FillRight(NewRect(at("B1"), at("C2")))315+ if got := inputs(s); got["C1"] != "=B1*2" || got["C2"] != "=B2*2" {316+ t.Errorf("fill right: %v", got)317+ }318+ // A single cell fills from its neighbor.319+ s.FillDown(NewRect(at("B4"), at("B4")))320+ if got := inputs(s)["B4"]; got != "=A4*2" {321+ t.Errorf("single-cell fill down: %q", got)322+ }323+ if err := s.FillEntry(NewRect(at("D1"), at("E2")), at("D1"), "=A1+$A$1"); err != nil {324+ t.Fatal(err)325+ }326+ want := map[string]string{"D1": "=A1+$A$1", "E1": "=B1+$A$1", "D2": "=A2+$A$1", "E2": "=B2+$A$1"}327+ for a, in := range want {328+ if got := inputs(s)[a]; got != in {329+ t.Errorf("fill entry %s = %q, want %q", a, got, in)330+ }331+ }332+ s.Undo()333+ if got := inputs(s)["D1"]; got != "" {334+ t.Errorf("fill entry is one undo step, D1 = %q", got)335+ }336+}337+338+// FuzzPrint checks that printing a parsed formula gives text that parses339+// back to the same expression.340+func FuzzPrint(f *testing.F) {341+ for _, s := range []string{342+ "=A1*2", "=$A$1+A$2-$B3", "=SUM($A1:B$3)", "=-2^-2%", "=1-(2-3)", "=#NOT#A1=1#OR#B1",343+ "=2^(3^2)", `=IF(A1>2,"x""y",#REF!)`, "=1.5E-3^2", "=@SUM(A1..B3)", "=(1+2)%*-A1",344+ } {345+ f.Add(s)346+ }347+ f.Fuzz(func(t *testing.T, in string) {348+ n, err := Parse(in)349+ if err != nil {350+ return351+ }352+ text := formulaText(n)353+ back, err := Parse(text)354+ if err != nil {355+ t.Fatalf("%q printed as %q, which doesn't parse: %v", in, text, err)356+ }357+ if !reflect.DeepEqual(n, back) {358+ t.Fatalf("%q printed as %q parses differently:\n%#v\n%#v", in, text, n, back)359+ }360+ })361+}@@ -269,7 +269,7 @@ func TestParseNumber(t *testing.T) {
269269 }270270 271271 func FuzzParse(f *testing.F) {272- for _, s := range []string{"=A1*2", "=SUM(A1:B3)", "=IF(AND(A1>2,B1),1,NA())", `="a"&"b"`, "=1.5E-3^2", "$1,200", "12%", "@SUM(A1..B3)"} {272+ for _, s := range []string{"=A1*2", "=SUM(A1:B3)", "=IF(AND(A1>2,B1),1,NA())", `="a"&"b"`, "=1.5E-3^2", "$1,200", "12%", "@SUM(A1..B3)", "=$A$1+A$2*$B3", "=SUM($A1:B$3)", "=#REF!+1", "=A$$1"} {273273 f.Add(s)274274 }275275 f.Fuzz(func(t *testing.T, in string) {