References and ranges, sheet names in references, the lexer, parser, syntax tree, printer and reference rewriting need nothing from cells or values, so they become their own package. The engine tells the parser which functions exist through a small Func interface that FuncDef implements, and marks operators it computes in decimal with its own node types rather than a flag in the tree. sheet keeps Addr, Rect, Parse and the sheet-name helpers as aliases and wrappers, so its API is unchanged. The lexer is a small type with a method per kind of token, bringing it and parser.call and Rewrite under the cognitive complexity limit. Strings without escapes are no longer copied, and #REF! is matched without upper-casing the rest of the formula, so parsing allocates a third less.
- commit
- e1e9e30b972d7f2ba5c904119dd664e487154d06
- parent
- 462d874a57e6451c3e0cabc367b7944ce82a2ff2
- author
- Dami <dami.etoile@gmail.com>
@@ -0,0 +1,103 @@
1+// Package formula is the formula language: A1 references and ranges,2+// sheet names in references, the lexer and Pratt parser, the syntax tree,3+// the printer that writes trees back in Sheets' spelling, and the4+// rewriting of references when cells are copied, moved, inserted or5+// deleted. It knows nothing of cells or values: the engine evaluates the6+// trees, and tells the parser which functions exist.7+package formula8+9+// Node is a parsed formula expression: one of the types below.10+type Node any11+12+type (13+ Num struct{ V float64 }14+ Str struct{ V string }15+ Bool struct{ V bool }16+ // Ref is a cell reference. Sheet is the sheet name as written before17+ // the "!" (Sheet2!A1), or "" for the formula's own sheet.18+ Ref struct {19+ Addr Addr20+ Abs Abs21+ Sheet string22+ }23+ // Range is kept normalized (Rect.From is the top-left corner); Abs24+ // holds the absolute markers of Rect.From and Rect.To.25+ Range struct {26+ Rect Rect27+ Abs [2]Abs28+ Sheet string29+ }30+ RefErr struct{} // a reference to deleted cells: #REF!31+ Name struct{ Name string } // a named range as spelled in the formula32+ Empty struct{} // an omitted argument, as in XLOOKUP(a, b, c, , 1)33+ Unary struct {34+ Op string // "-", "+", "%" (postfix) or "#NOT#"35+ X Node36+ }37+ Binary struct {38+ Op string39+ L, R Node40+ }41+ Call struct {42+ Fn Func43+ Args []Node44+ }45+)46+47+// Func is a function a formula can call. The parser checks calls against48+// its signature; what a call computes is up to the engine, which49+// recovers its own type from Call.Fn.50+type Func interface {51+ Signature() Signature52+}53+54+// Signature is how a function is called.55+type Signature struct {56+ Name string // canonical, upper case57+ Args string // shown to users, e.g. "value1, [value2, ...]"58+ Min int59+ Max int // -1 for variadic60+ // Step > 0 means arguments after Min come in groups of Step, like61+ // SUMIFS' (range, criterion) pairs.62+ Step int63+}64+65+// Funcs finds a function by its upper-case name or alias.66+type Funcs func(name string) (Func, bool)67+68+// WalkNames calls fn for every name in n.69+func WalkNames(n Node, fn func(Name)) {70+ switch n := n.(type) {71+ case Name:72+ fn(n)73+ case Unary:74+ WalkNames(n.X, fn)75+ case Binary:76+ WalkNames(n.L, fn)77+ WalkNames(n.R, fn)78+ case Call:79+ for _, a := range n.Args {80+ WalkNames(a, fn)81+ }82+ }83+}84+85+// WalkRefs calls fn for every single-cell reference and range in n, with86+// the sheet it was qualified with ("" for the formula's own sheet).87+func WalkRefs(n Node, ref func(string, Addr), rng func(string, Rect)) {88+ switch n := n.(type) {89+ case Ref:90+ ref(n.Sheet, n.Addr)91+ case Range:92+ rng(n.Sheet, n.Rect)93+ case Unary:94+ WalkRefs(n.X, ref, rng)95+ case Binary:96+ WalkRefs(n.L, ref, rng)97+ WalkRefs(n.R, ref, rng)98+ case Call:99+ for _, a := range n.Args {100+ WalkRefs(a, ref, rng)101+ }102+ }103+}@@ -0,0 +1,326 @@
1+package formula2+3+import (4+ "reflect"5+ "testing"6+)7+8+// testFunc is a function for the parser, with its signature only.9+type testFunc Signature10+11+func (f testFunc) Signature() Signature { return Signature(f) }12+13+// testFuncs is a small function table: what the parser needs to know.14+func testFuncs(name string) (Func, bool) {15+ sigs := map[string]Signature{16+ "SUM": {Name: "SUM", Args: "value1, [value2, ...]", Min: 1, Max: -1},17+ "IF": {Name: "IF", Args: "condition, value_if_true, [value_if_false]", Min: 2, Max: 3},18+ "PI": {Name: "PI", Max: 0},19+ "ROUND": {Name: "ROUND", Args: "value, [places]", Min: 1, Max: 2},20+ "SUMIFS": {Name: "SUMIFS", Args: "sum_range, criteria_range1, criterion1, ...", Min: 3, Max: -1, Step: 2},21+ }22+ sig, ok := sigs[name]23+ return testFunc(sig), ok24+}25+26+func at(s string) Addr {27+ a, ok := ParseAddr(s)28+ if !ok {29+ panic("bad address " + s)30+ }31+ return a32+}33+34+func TestAddr(t *testing.T) {35+ tests := []struct {36+ in string37+ want Addr38+ ok bool39+ }{40+ {"A1", Addr{0, 0}, true},41+ {"z10", Addr{25, 9}, true},42+ {"AA1", Addr{26, 0}, true},43+ {"IV8192", Addr{255, 8191}, true},44+ {"$B$2", Addr{1, 1}, true},45+ {"IW1", Addr{}, false},46+ {"A0", Addr{}, false},47+ {"A8193", Addr{}, false},48+ {"ABC1", Addr{}, false},49+ {"A+1", Addr{}, false},50+ {"1A", Addr{}, false},51+ }52+ for _, tt := range tests {53+ got, ok := ParseAddr(tt.in)54+ if ok != tt.ok || (ok && got != tt.want) {55+ t.Errorf("ParseAddr(%q) = %v, %v; want %v, %v", tt.in, got, ok, tt.want, tt.ok)56+ }57+ }58+ for c := range MaxCols {59+ if got, ok := ParseCol(ColName(c)); !ok || got != c {60+ t.Fatalf("ParseCol(ColName(%d)) = %d, %v", c, got, ok)61+ }62+ }63+}64+65+func TestParseRange(t *testing.T) {66+ for _, in := range []string{"B3..A1", "a1:b3", "$A$1:$B$3"} {67+ r, ok := ParseRange(in)68+ if !ok || r.String() != "A1:B3" {69+ t.Errorf("ParseRange(%q) = %v, %v", in, r, ok)70+ }71+ }72+}73+74+func TestParseAbsoluteRefs(t *testing.T) {75+ tests := []struct {76+ in string77+ want Node78+ }{79+ {"=A1", Ref{at("A1"), 0, ""}},80+ {"=$A1", Ref{at("A1"), AbsCol, ""}},81+ {"=A$1", Ref{at("A1"), AbsRow, ""}},82+ {"=$a$1", Ref{at("A1"), AbsCol | AbsRow, ""}},83+ {"=$B$3:A1", Range{NewRect(at("A1"), at("B3")), [2]Abs{0, AbsCol | AbsRow}, ""}},84+ // Each marker stays with its column or row when corners swap.85+ {"=$B1:A$2", Range{NewRect(at("A1"), at("B2")), [2]Abs{0, AbsCol | AbsRow}, ""}},86+ {"=A1..$B2", Range{NewRect(at("A1"), at("B2")), [2]Abs{0, AbsCol}, ""}},87+ {"=A$$1", Name{"A$$1"}},88+ {"=A1$", Name{"A1$"}},89+ {"=#REF!+1", Binary{Op: "+", L: RefErr{}, R: Num{1}}},90+ }91+ for _, tt := range tests {92+ got, err := Parse(tt.in, testFuncs)93+ if err != nil || !reflect.DeepEqual(got, tt.want) {94+ t.Errorf("Parse(%q) = %#v, %v; want %#v", tt.in, got, err, tt.want)95+ }96+ }97+}98+99+func TestPrintFormula(t *testing.T) {100+ tests := []struct{ in, want string }{101+ {"=a1+$b$2*C$3", "=A1+$B$2*C$3"},102+ {"=@SUM(A1..B3; 2)", "=SUM(A1:B3,2)"},103+ {"=(1+2)*3", "=(1+2)*3"},104+ {"=1-(2-3)", "=1-(2-3)"},105+ {"=(1-2)-3", "=1-2-3"},106+ {"=2^3^2", "=2^3^2"},107+ {"=2^(3^2)", "=2^(3^2)"},108+ {"=-2^2", "=-2^2"},109+ {"=(-2)^2", "=-2^2"}, // negation binds tighter than ^110+ {"=-(2^2)", "=-(2^2)"},111+ {"=-(A1+1)", "=-(A1+1)"},112+ {"=50%", "=50%"},113+ {"=(A1+1)%", "=(A1+1)%"},114+ {"=#NOT#(A1=1) #AND# B1", "=#NOT#A1=1#AND#B1"},115+ {"=1+(#NOT#A1)", "=1+(#NOT#A1)"},116+ {`="say ""hi"""&A1`, `="say ""hi"""&A1`},117+ {"=1.5E3+1e-9+2E21", "=1500+1E-09+2E+21"},118+ {"=@PI", "=PI()"},119+ {"=IF(A1>=2,TRUE,profit)", "=IF(A1>=2,TRUE,profit)"},120+ {"=SUM(#REF!)", "=SUM(#REF!)"},121+ {"+A1", "=+A1"},122+ }123+ for _, tt := range tests {124+ n, err := Parse(tt.in, testFuncs)125+ if err != nil {126+ t.Fatalf("Parse(%q): %v", tt.in, err)127+ }128+ if got := Text(n); got != tt.want {129+ t.Errorf("print(%q) = %q, want %q", tt.in, got, tt.want)130+ }131+ }132+}133+134+func TestShiftRefs(t *testing.T) {135+ tests := []struct {136+ in string137+ dc, dr int138+ want string139+ }{140+ {"=A1+B2", 1, 2, "=B3+C4"},141+ {"=$A$1+A1", 1, 1, "=$A$1+B2"},142+ {"=$A1+A$1", 2, 3, "=$A4+C$1"},143+ {"=SUM(A1:B2)", 0, 5, "=SUM(A6:B7)"},144+ {"=SUM($A$1:B2)", 1, 1, "=SUM($A$1:C3)"},145+ {"=A2", 0, -1, "=A1"},146+ {"=A2", 0, -2, "=#REF!"},147+ {"=SUM(A1:B2)+1", -1, 0, "=SUM(#REF!)+1"},148+ {"=$A2", -1, 0, "=$A2"},149+ {"=IV1", 1, 0, "=#REF!"},150+ // A mixed range can flip when one corner is anchored.151+ {"=SUM($C1:D1)", -2, 0, "=SUM(B1:$C1)"},152+ }153+ for _, tt := range tests {154+ n, _ := Parse(tt.in, testFuncs)155+ out, _ := Rewrite(n, Shift(tt.dc, tt.dr))156+ if got := Text(out); got != tt.want {157+ t.Errorf("shift(%q, %d, %d) = %q, want %q", tt.in, tt.dc, tt.dr, got, tt.want)158+ }159+ }160+}161+162+func TestSpanInterval(t *testing.T) {163+ tests := []struct {164+ sp Span165+ lo, hi int166+ wLo, wHi int167+ wOK bool168+ name string169+ checkPoint bool170+ }{171+ {Span{2, 2, 100}, 0, 1, 0, 1, true, "insert below", false},172+ {Span{2, 2, 100}, 1, 3, 1, 5, true, "insert inside grows", false},173+ {Span{2, 2, 100}, 2, 3, 4, 5, true, "insert at start shifts", false},174+ {Span{2, 2, 100}, 50, 99, 52, 99, true, "insert clamps at the edge", false},175+ {Span{2, -2, 100}, 1, 4, 1, 2, true, "delete inside shrinks", false},176+ {Span{2, -2, 100}, 2, 4, 2, 2, true, "delete top part", false},177+ {Span{2, -2, 100}, 1, 2, 1, 1, true, "delete bottom part", false},178+ {Span{2, -2, 100}, 3, 6, 2, 4, true, "delete overlapping start", false},179+ {Span{2, -2, 100}, 5, 6, 3, 4, true, "delete above shifts", false},180+ {Span{2, -2, 100}, 2, 3, 0, 0, false, "delete all of it", false},181+ }182+ for _, tt := range tests {183+ lo, hi, ok := tt.sp.Interval(tt.lo, tt.hi)184+ if ok != tt.wOK || (ok && (lo != tt.wLo || hi != tt.wHi)) {185+ t.Errorf("%s: interval(%d, %d) = %d, %d, %v", tt.name, tt.lo, tt.hi, lo, hi, ok)186+ }187+ }188+}189+190+func TestParseSheetRefs(t *testing.T) {191+ tests := []struct{ in, out, err string }{192+ {in: "=Sheet2!A1", out: "=Sheet2!A1"},193+ {in: "=sheet2!$a$1+1", out: "=sheet2!$A$1+1"},194+ {in: "='My Sheet'!B2:C9", out: "='My Sheet'!B2:C9"},195+ {in: "=SUM('Bob''s'!A1:A3)", out: "=SUM('Bob''s'!A1:A3)"},196+ {in: "=Data!B3:Data!A1", out: "=Data!A1:B3"},197+ {in: "='2026'!A1", out: "='2026'!A1"},198+ {in: "='A1'!A1", out: "='A1'!A1"},199+ {in: "=Q3.plan!A1..B2", out: "=Q3.plan!A1:B2"},200+ {in: "=Sheet2!Sales", err: "Expected a cell after Sheet2!"},201+ {in: "=Sheet2!A1:Other!B2", err: "A range can't span sheets"},202+ {in: "='Open!A1", err: "Expected ! after a quoted sheet name"},203+ {in: "='x'A1", err: "Expected ! after a quoted sheet name"},204+ }205+ for _, tt := range tests {206+ n, err := Parse(tt.in, testFuncs)207+ switch {208+ case tt.err != "":209+ if err == nil || err.Error() != tt.err {210+ t.Errorf("%s: error %v, want %q", tt.in, err, tt.err)211+ }212+ case err != nil:213+ t.Errorf("%s: %v", tt.in, err)214+ case Text(n) != tt.out:215+ t.Errorf("%s prints as %s, want %s", tt.in, Text(n), tt.out)216+ }217+ }218+}219+220+func TestQuoteSheet(t *testing.T) {221+ for name, want := range map[string]string{222+ "Sheet1": "Sheet1", "Q3_plan": "Q3_plan", "My Sheet": "'My Sheet'", "Bob's": "'Bob''s'",223+ "2026": "'2026'", "AB12": "'AB12'", "R1C1": "'R1C1'", "Café": "'Café'",224+ } {225+ if got := QuoteSheet(name); got != want {226+ t.Errorf("QuoteSheet(%q) = %s, want %s", name, got, want)227+ }228+ }229+}230+231+func TestSplitSheet(t *testing.T) {232+ tests := []struct{ in, sheet, rest string }{233+ {"A1", "", "A1"},234+ {"Sheet2!A1:B3", "Sheet2", "A1:B3"},235+ {"'Q3 plan'!B2", "Q3 plan", "B2"},236+ {"'Bob''s'!C1", "Bob's", "C1"},237+ {" Data!Z9 ", "Data", "Z9"},238+ }239+ for _, tt := range tests {240+ if sheet, rest := SplitSheet(tt.in); sheet != tt.sheet || rest != tt.rest {241+ t.Errorf("SplitSheet(%q) = %q, %q", tt.in, sheet, rest)242+ }243+ }244+}245+246+// FuzzPrint checks that printing a parsed formula gives text that parses247+// back to the same expression.248+func FuzzPrint(f *testing.F) {249+ for _, s := range []string{250+ "=A1*2", "=$A$1+A$2-$B3", "=SUM($A1:B$3)", "=-2^-2%", "=1-(2-3)", "=#NOT#A1=1#OR#B1",251+ "=2^(3^2)", `=IF(A1>2,"x""y",#REF!)`, "=1.5E-3^2", "=@SUM(A1..B3)", "=(1+2)%*-A1",252+ } {253+ f.Add(s)254+ }255+ f.Fuzz(func(t *testing.T, in string) {256+ n, err := Parse(in, testFuncs)257+ if err != nil {258+ return259+ }260+ text := Text(n)261+ back, err := Parse(text, testFuncs)262+ if err != nil {263+ t.Fatalf("%q printed as %q, which doesn't parse: %v", in, text, err)264+ }265+ if !reflect.DeepEqual(n, back) {266+ t.Fatalf("%q printed as %q parses differently:\n%#v\n%#v", in, text, n, back)267+ }268+ })269+}270+271+func TestParseErrors(t *testing.T) {272+ tests := []struct {273+ in string274+ pos int275+ msg string276+ }{277+ {"=A1+", 4, "Formula is incomplete"},278+ {"=SUM(A1", 7, "Expected , or ) in SUM"},279+ {"=NOPE(1)", 1, "Unknown function NOPE"},280+ {"=(1", 3, "Missing )"},281+ {"=A1:", 4, "Expected a cell after :"},282+ {"=1 2", 3, "Unexpected 2"},283+ {`="abc`, 1, "Missing closing quote"},284+ {"=ROUND()", 1, "Wrong number of arguments to ROUND(value, [places])"},285+ {"=PI(1)", 1, "Wrong number of arguments to PI()"},286+ {"=SUMIFS(A1:A3,B1:B3,1,C1:C3)", 1, "Wrong number of arguments to SUMIFS(sum_range, criteria_range1, criterion1, ...)"},287+ {"=1#XOR#2", 2, "Unknown operator #XOR#"},288+ {"=1#2", 2, "Unexpected #"},289+ {"=@(1)", 1, "Missing function name after @"},290+ {"=1!", 2, `Unexpected '!'`},291+ }292+ for _, tt := range tests {293+ _, err := Parse(tt.in, testFuncs)294+ pe, ok := err.(*ParseError)295+ if !ok || pe.Msg != tt.msg || pe.Pos != tt.pos {296+ t.Errorf("Parse(%q) error = %#v, want %q at %d", tt.in, err, tt.msg, tt.pos)297+ }298+ }299+}300+301+func TestLexLiterals(t *testing.T) {302+ tests := []struct {303+ in string304+ want Node305+ }{306+ {`="say ""hi"""`, Str{`say "hi"`}},307+ {`=""""`, Str{`"`}},308+ {`="plain"`, Str{"plain"}},309+ {"=.5", Num{0.5}},310+ {"=1.5e+3", Num{1500}},311+ {"=#ref!", RefErr{}},312+ {"=SUMIFS(A1:A3,B1:B3,1)", nil},313+ }314+ for _, tt := range tests {315+ got, err := Parse(tt.in, testFuncs)316+ if tt.want == nil {317+ if err != nil {318+ t.Errorf("Parse(%q): %v", tt.in, err)319+ }320+ continue321+ }322+ if err != nil || !reflect.DeepEqual(got, tt.want) {323+ t.Errorf("Parse(%q) = %#v, %v; want %#v", tt.in, got, err, tt.want)324+ }325+ }326+}@@ -0,0 +1,242 @@
1+package formula2+3+import (4+ "fmt"5+ "strings"6+)7+8+type tokKind int9+10+const (11+ tokEOF tokKind = iota12+ tokNum13+ tokStr14+ tokIdent // cell reference, TRUE/FALSE or a name15+ tokFunc // identifier directly followed by "("16+ tokOp // operators and punctuation17+ tokRefErr // #REF!, left behind when a referenced cell is deleted18+ tokSheet // a sheet name and its "!", as in Sheet2!A1 or 'My Sheet'!A1; text is the name19+)20+21+type token struct {22+ kind tokKind23+ text string24+ pos int25+}26+27+// ParseError describes a formula that could not be parsed. Pos is the byte28+// offset in the entry where the problem was found, used to place the edit29+// cursor.30+type ParseError struct {31+ Pos int32+ Msg string33+}34+35+func (e *ParseError) Error() string { return e.Msg }36+37+// Operators: two-character ones are matched first, then single characters.38+// #AND#, #OR# and #NOT# are 1-2-3's logical operators.39+var (40+ twoCharOps = [...]string{"..", "<=", ">=", "<>"}41+ hashOps = [...]string{"#AND#", "#OR#", "#NOT#"}42+ oneCharOps = "+-*/^=<>&(),;:%"43+ refErrorText = "#REF!"44+)45+46+func isIdentStart(c byte) bool { return isLetter(c) || c == '_' || c == '$' }47+48+func isIdentPart(c byte) bool { return isIdentStart(c) || isDigit(c) || c == '.' }49+50+// lexer splits a formula (without its "=") into tokens.51+type lexer struct {52+ src string53+ i int54+ toks []token55+}56+57+func lex(src string) ([]token, error) {58+ lx := lexer{src: src}59+ for lx.i < len(src) {60+ if err := lx.next(); err != nil {61+ return nil, err62+ }63+ }64+ return append(lx.toks, token{tokEOF, "", len(src)}), nil65+}66+67+// emit adds a token that started at start and ends at the lexer's position.68+func (lx *lexer) emit(kind tokKind, text string, start int) {69+ lx.toks = append(lx.toks, token{kind, text, start})70+}71+72+func (lx *lexer) next() error {73+ c := lx.src[lx.i]74+ switch {75+ case c == ' ':76+ lx.i++77+ case isDigit(c) || c == '.' && lx.i+1 < len(lx.src) && isDigit(lx.src[lx.i+1]):78+ lx.number()79+ case c == '"':80+ return lx.str()81+ case c == '\'':82+ return lx.quotedSheet()83+ case c == '@' || isIdentStart(c):84+ return lx.ident()85+ case c == '#':86+ return lx.hash()87+ default:88+ return lx.operator()89+ }90+ return nil91+}92+93+// number reads 12, 1.5, .5 or 1.5E-3. A ".." ends it: A1..B3's range94+// operator follows digits.95+func (lx *lexer) number() {96+ src, start := lx.src, lx.i97+ for lx.i < len(src) && (isDigit(src[lx.i]) || src[lx.i] == '.' && !strings.HasPrefix(src[lx.i:], "..")) {98+ lx.i++99+ }100+ lx.exponent()101+ lx.emit(tokNum, src[start:lx.i], start)102+}103+104+// exponent reads E3, E+3 or E-3 after a number's digits, if there is one.105+func (lx *lexer) exponent() {106+ src, j := lx.src, lx.i107+ if j >= len(src) || src[j] != 'e' && src[j] != 'E' {108+ return109+ }110+ j++111+ if j < len(src) && (src[j] == '+' || src[j] == '-') {112+ j++113+ }114+ if j >= len(src) || !isDigit(src[j]) {115+ return116+ }117+ for j < len(src) && isDigit(src[j]) {118+ j++119+ }120+ lx.i = j121+}122+123+// str reads a string literal. Strings use "" to escape a quote, as in124+// Sheets.125+func (lx *lexer) str() error {126+ src, start := lx.src, lx.i127+ var escaped strings.Builder // the text so far, once it has an escape128+ from := start + 1 // the start of the text not yet copied129+ for i := from; i < len(src); i++ {130+ if src[i] != '"' {131+ continue132+ }133+ if i+1 < len(src) && src[i+1] == '"' {134+ escaped.WriteString(src[from : i+1])135+ i++136+ from = i + 1137+ continue138+ }139+ text := src[from:i]140+ if escaped.Len() > 0 {141+ escaped.WriteString(text)142+ text = escaped.String()143+ }144+ lx.i = i + 1145+ lx.emit(tokStr, text, start)146+ return nil147+ }148+ return &ParseError{start, "Missing closing quote"}149+}150+151+// quotedSheet reads a quoted sheet name and its "!", with ” for a152+// quote: 'Q3 ”26'!A1.153+func (lx *lexer) quotedSheet() error {154+ start := lx.i155+ name, end, ok := quotedName(lx.src, start)156+ if !ok || end >= len(lx.src) || lx.src[end] != '!' {157+ return &ParseError{start, "Expected ! after a quoted sheet name"}158+ }159+ lx.i = end + 1160+ lx.emit(tokSheet, name, start)161+ return nil162+}163+164+// ident reads a reference, a name, a function name, a sheet name before165+// "!", or a 1-2-3 style @function, which may omit its parentheses (@PI).166+func (lx *lexer) ident() error {167+ src, start := lx.src, lx.i168+ at := src[start] == '@'169+ if at {170+ lx.i++171+ }172+ for lx.i < len(src) && isIdentPart(src[lx.i]) && !strings.HasPrefix(src[lx.i:], "..") {173+ lx.i++174+ }175+ if !at && lx.i < len(src) && src[lx.i] == '!' {176+ lx.emit(tokSheet, src[start:lx.i], start)177+ lx.i++178+ return nil179+ }180+ name := src[start:lx.i]181+ if at {182+ name = name[1:]183+ }184+ if name == "" {185+ return &ParseError{start, "Missing function name after @"}186+ }187+ kind := tokIdent188+ if at || lx.parenFollows() {189+ kind = tokFunc190+ }191+ lx.emit(kind, strings.ToUpper(name), start)192+ return nil193+}194+195+// parenFollows reports whether "(" comes next, after any spaces.196+func (lx *lexer) parenFollows() bool {197+ j := lx.i198+ for j < len(lx.src) && lx.src[j] == ' ' {199+ j++200+ }201+ return j < len(lx.src) && lx.src[j] == '('202+}203+204+// hash reads #REF! or one of 1-2-3's #AND#, #OR# and #NOT#.205+func (lx *lexer) hash() error {206+ start, rest := lx.i, lx.src[lx.i:]207+ if len(rest) >= len(refErrorText) && strings.EqualFold(rest[:len(refErrorText)], refErrorText) {208+ lx.i += len(refErrorText)209+ lx.emit(tokRefErr, refErrorText, start)210+ return nil211+ }212+ end := strings.IndexByte(rest[1:], '#')213+ if end < 0 {214+ return &ParseError{start, "Unexpected #"}215+ }216+ op := strings.ToUpper(rest[:end+2])217+ for _, known := range hashOps {218+ if op == known {219+ lx.i += end + 2220+ lx.emit(tokOp, known, start)221+ return nil222+ }223+ }224+ return &ParseError{start, "Unknown operator " + op}225+}226+227+func (lx *lexer) operator() error {228+ start, rest := lx.i, lx.src[lx.i:]229+ for _, op := range twoCharOps {230+ if strings.HasPrefix(rest, op) {231+ lx.i += 2232+ lx.emit(tokOp, op, start)233+ return nil234+ }235+ }236+ if strings.IndexByte(oneCharOps, rest[0]) >= 0 {237+ lx.i++238+ lx.emit(tokOp, rest[:1], start)239+ return nil240+ }241+ return &ParseError{start, fmt.Sprintf("Unexpected %q", rest[0])}242+}@@ -0,0 +1,269 @@
1+package formula2+3+import (4+ "fmt"5+ "strconv"6+ "strings"7+)8+9+// Binding powers, lowest to highest, following Sheets' precedence.10+var infixPower = map[string]int{11+ "#OR#": 10, "#AND#": 10,12+ "=": 30, "<>": 30, "<": 30, ">": 30, "<=": 30, ">=": 30,13+ "&": 35,14+ "+": 40, "-": 40,15+ "*": 50, "/": 50,16+ "^": 70,17+}18+19+// Negation binds tighter than ^, so -2^2 is 4 as in Sheets and Excel.20+const (21+ notPower = 2022+ unaryPower = 7523+ percentPower = 8024+)25+26+type parser struct {27+ src string // the formula without its "=", for names' original spelling28+ toks []token29+ pos int30+ funcs Funcs31+}32+33+// Parse parses a formula, finding the functions it calls with funcs. src34+// may start with "=", as typed in a cell; error positions are relative to35+// src.36+func Parse(src string, funcs Funcs) (Node, error) {37+ body := strings.TrimPrefix(src, "=")38+ offset := len(src) - len(body)39+ toks, err := lex(body)40+ if err != nil {41+ return nil, shift(err, offset)42+ }43+ p := &parser{src: body, toks: toks, funcs: funcs}44+ n, err := p.expr(0)45+ if err == nil && p.peek().kind != tokEOF {46+ err = &ParseError{p.peek().pos, "Unexpected " + p.peek().text}47+ }48+ if err != nil {49+ return nil, shift(err, offset)50+ }51+ return n, nil52+}53+54+func shift(err error, offset int) error {55+ if pe, ok := err.(*ParseError); ok {56+ pe.Pos += offset57+ }58+ return err59+}60+61+func (p *parser) peek() token { return p.toks[p.pos] }62+63+func (p *parser) next() token {64+ t := p.toks[p.pos]65+ if t.kind != tokEOF {66+ p.pos++67+ }68+ return t69+}70+71+func (p *parser) isOp(op string) bool {72+ t := p.peek()73+ return t.kind == tokOp && t.text == op74+}75+76+func (p *parser) expr(minPower int) (Node, error) {77+ left, err := p.prefix()78+ if err != nil {79+ return nil, err80+ }81+ for {82+ if p.isOp("%") && percentPower > minPower {83+ p.next()84+ left = Unary{Op: "%", X: left}85+ continue86+ }87+ t := p.peek()88+ power, ok := infixPower[t.text]89+ if t.kind != tokOp || !ok || power <= minPower {90+ return left, nil91+ }92+ p.next()93+ right, err := p.expr(power)94+ if err != nil {95+ return nil, err96+ }97+ left = Binary{Op: t.text, L: left, R: right}98+ }99+}100+101+func (p *parser) prefix() (Node, error) {102+ t := p.next()103+ switch t.kind {104+ case tokNum:105+ v, err := strconv.ParseFloat(t.text, 64)106+ if err != nil {107+ return nil, &ParseError{t.pos, "Invalid number " + t.text}108+ }109+ return Num{v}, nil110+ case tokStr:111+ return Str{t.text}, nil112+ case tokIdent:113+ return p.ident(t, "")114+ case tokSheet:115+ ref := p.next()116+ if ref.kind != tokIdent {117+ return nil, &ParseError{ref.pos, "Expected a cell after " + QuoteSheet(t.text) + "!"}118+ }119+ return p.ident(ref, t.text)120+ case tokFunc:121+ return p.call(t)122+ case tokRefErr:123+ return RefErr{}, nil124+ case tokOp:125+ return p.prefixOp(t)126+ case tokEOF:127+ return nil, &ParseError{t.pos, "Formula is incomplete"}128+ }129+ return nil, &ParseError{t.pos, "Unexpected " + t.text}130+}131+132+// prefixOp parses what starts with an operator: a parenthesized133+// expression, or a prefix + - or #NOT#.134+func (p *parser) prefixOp(t token) (Node, error) {135+ switch t.text {136+ case "(":137+ n, err := p.expr(0)138+ if err != nil {139+ return nil, err140+ }141+ if !p.isOp(")") {142+ return nil, &ParseError{p.peek().pos, "Missing )"}143+ }144+ p.next()145+ return n, nil146+ case "+", "-", "#NOT#":147+ power := unaryPower148+ if t.text == "#NOT#" {149+ power = notPower150+ }151+ x, err := p.expr(power)152+ if err != nil {153+ return nil, err154+ }155+ return Unary{Op: t.text, X: x}, nil156+ }157+ return nil, &ParseError{t.pos, "Unexpected " + t.text}158+}159+160+// ident parses a reference, a range, a boolean or a name. sheet is the161+// sheet the reference was qualified with, if any.162+func (p *parser) ident(t token, sheet string) (Node, error) {163+ a, abs, isRef := ParseRef(t.text)164+ if !isRef {165+ if sheet != "" {166+ return nil, &ParseError{t.pos, "Expected a cell after " + QuoteSheet(sheet) + "!"}167+ }168+ switch t.text {169+ case "TRUE":170+ return Bool{true}, nil171+ case "FALSE":172+ return Bool{false}, nil173+ }174+ // Names match case-insensitively but print as written.175+ return Name{p.src[t.pos : t.pos+len(t.text)]}, nil176+ }177+ if !p.isOp(":") && !p.isOp("..") {178+ return Ref{a, abs, sheet}, nil179+ }180+ sep := p.next()181+ end := p.next()182+ // The second corner may repeat the sheet: Sheet2!A1:Sheet2!B3.183+ if end.kind == tokSheet {184+ if SheetKey(end.text) != SheetKey(sheet) {185+ return nil, &ParseError{end.pos, "A range can't span sheets"}186+ }187+ end = p.next()188+ }189+ b, bAbs, ok := ParseRef(end.text)190+ if end.kind != tokIdent || !ok {191+ return nil, &ParseError{end.pos, "Expected a cell after " + sep.text}192+ }193+ r := NewRange(a, b, abs, bAbs)194+ r.Sheet = sheet195+ return r, nil196+}197+198+// NewRange builds a normalized range from two corners as written. Each199+// absolute marker stays with its column or row, so $B1:A$2 becomes A1:$B$2.200+func NewRange(a, b Addr, aAbs, bAbs Abs) Range {201+ swap := func(bit Abs) {202+ aAbs, bAbs = aAbs&^bit|bAbs&bit, bAbs&^bit|aAbs&bit203+ }204+ if a.Col > b.Col {205+ a.Col, b.Col = b.Col, a.Col206+ swap(AbsCol)207+ }208+ if a.Row > b.Row {209+ a.Row, b.Row = b.Row, a.Row210+ swap(AbsRow)211+ }212+ return Range{Rect{a, b}, [2]Abs{aAbs, bAbs}, ""}213+}214+215+// call parses a function call, with or without parentheses (@PI), and216+// checks its number of arguments.217+func (p *parser) call(t token) (Node, error) {218+ fn, ok := p.funcs(t.text)219+ if !ok {220+ return nil, &ParseError{t.pos, "Unknown function " + t.text}221+ }222+ sig := fn.Signature()223+ n := Call{Fn: fn}224+ if p.isOp("(") {225+ p.next()226+ args, err := p.args(sig.Name)227+ if err != nil {228+ return nil, err229+ }230+ n.Args = args231+ }232+ if !sig.accepts(len(n.Args)) {233+ return nil, &ParseError{t.pos, fmt.Sprintf("Wrong number of arguments to %s(%s)", sig.Name, sig.Args)}234+ }235+ return n, nil236+}237+238+// args parses a call's arguments after its "(", through the ")". An239+// argument may be left out: F(a, , c) or F(a, ).240+func (p *parser) args(name string) ([]Node, error) {241+ if p.isOp(")") {242+ p.next()243+ return nil, nil244+ }245+ var args []Node246+ for {247+ if p.isOp(",") || p.isOp(";") || p.isOp(")") {248+ args = append(args, Empty{})249+ } else {250+ arg, err := p.expr(0)251+ if err != nil {252+ return nil, err253+ }254+ args = append(args, arg)255+ }256+ sep := p.next()257+ switch {258+ case sep.kind == tokOp && sep.text == ")":259+ return args, nil260+ case sep.kind != tokOp || (sep.text != "," && sep.text != ";"):261+ return nil, &ParseError{sep.pos, "Expected , or ) in " + name}262+ }263+ }264+}265+266+// accepts reports whether a call may pass n arguments.267+func (s Signature) accepts(n int) bool {268+ return n >= s.Min && (s.Max < 0 || n <= s.Max) && (s.Step <= 0 || (n-s.Min)%s.Step == 0)269+}@@ -1,4 +1,4 @@
1-package sheet1+package formula22 33 import (44 "math"@@ -6,16 +6,24 @@ import (
66 "strings"77 )88 9-// formulaText prints a parsed formula back to text, with a leading "=".9+// Text prints a parsed formula back to text, with a leading "=".1010 // Rewritten formulas (after a paste or an inserted row) are stored this1111 // way, in Sheets' spelling: ranges as A1:B3, functions without @.12-func formulaText(n Node) string {12+func Text(n Node) string {1313 var b strings.Builder1414 b.WriteByte('=')1515 printNode(&b, n)1616 return b.String()1717 }1818 19+// Expr prints an expression without the leading "=", e.g. to quote part20+// of a formula.21+func Expr(n Node) string {22+ var b strings.Builder23+ printNode(&b, n)24+ return b.String()25+}26+1927 // atomPower is the binding power of nodes that never need parentheses.2028 const atomPower = 1002129 @@ -23,10 +31,10 @@ const atomPower = 100
2331 // the printer adds parentheses exactly where the parser needs them.2432 func power(n Node) int {2533 switch n := n.(type) {26- case binaryNode:27- return infixPower[n.op]28- case unaryNode:29- switch n.op {34+ case Binary:35+ return infixPower[n.Op]36+ case Unary:37+ switch n.Op {3038 case "%":3139 return percentPower3240 case "#NOT#":@@ -39,40 +47,34 @@ func power(n Node) int {
3947 4048 func printNode(b *strings.Builder, n Node) {4149 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- writeSheet(b, n.sheet)50- b.WriteString(refString(n.a, n.abs))51- case rangeNode:52- writeSheet(b, n.sheet)53- b.WriteString(refString(n.r.From, n.abs[0]) + ":" + refString(n.r.To, n.abs[1]))54- case refErrNode:55- b.WriteString("#REF!")56- case nameNode:57- b.WriteString(n.name)58- case unaryNode:59- if n.op == "%" {60- printChild(b, n.x, power(n.x) < percentPower)61- b.WriteByte('%')62- return63- }64- b.WriteString(n.op)65- printChild(b, n.x, power(n.x) < power(n))66- case binaryNode:67- p := infixPower[n.op]50+ case Num:51+ b.WriteString(numText(n.V))52+ case Str:53+ b.WriteString(`"` + strings.ReplaceAll(n.V, `"`, `""`) + `"`)54+ case Bool:55+ b.WriteString(strings.ToUpper(strconv.FormatBool(n.V)))56+ case Ref:57+ writeSheet(b, n.Sheet)58+ b.WriteString(RefString(n.Addr, n.Abs))59+ case Range:60+ writeSheet(b, n.Sheet)61+ b.WriteString(RefString(n.Rect.From, n.Abs[0]) + ":" + RefString(n.Rect.To, n.Abs[1]))62+ case RefErr:63+ b.WriteString(refErrorText)64+ case Name:65+ b.WriteString(n.Name)66+ case Unary:67+ printUnary(b, n)68+ case Binary:69+ p := infixPower[n.Op]6870 // Operators associate to the left, so a right operand of equal6971 // power needs parentheses: 1-(2-3).70- printChild(b, n.l, power(n.l) < p)71- b.WriteString(n.op)72- printChild(b, n.r, power(n.r) <= p)73- case callNode:74- b.WriteString(n.fn.Name + "(")75- for i, a := range n.args {72+ printChild(b, n.L, power(n.L) < p)73+ b.WriteString(n.Op)74+ printChild(b, n.R, power(n.R) <= p)75+ case Call:76+ b.WriteString(n.Fn.Signature().Name + "(")77+ for i, a := range n.Args {7678 if i > 0 {7779 b.WriteByte(',')7880 }@@ -82,10 +84,20 @@ func printNode(b *strings.Builder, n Node) {
8284 }8385 }8486 87+func printUnary(b *strings.Builder, n Unary) {88+ if n.Op == "%" {89+ printChild(b, n.X, power(n.X) < percentPower)90+ b.WriteByte('%')91+ return92+ }93+ b.WriteString(n.Op)94+ printChild(b, n.X, power(n.X) < power(n))95+}96+8597 // writeSheet writes a reference's sheet and its "!", quoted as needed.8698 func writeSheet(b *strings.Builder, sheet string) {8799 if sheet != "" {88- b.WriteString(quoteSheet(sheet) + "!")100+ b.WriteString(QuoteSheet(sheet) + "!")89101 }90102 }91103 @@ -1,11 +1,12 @@
1-package sheet1+package formula22 33 import (44 "strconv"55 "strings"66 )77 8-// Worksheet bounds, matching Lotus 1-2-3 Release 2 (A..IV, 1..8192).8+// Worksheet bounds, matching Lotus 1-2-3 Release 2 (A..IV, 1..8192). A9+// reference past them isn't a reference: IW1 reads as a name.910 const (1011 MaxCols = 2561112 MaxRows = 8192@@ -53,24 +54,24 @@ func ParseCol(s string) (int, bool) {
5354 // ParseAddr parses an A1-style reference. Absolute markers ($A$1) are5455 // accepted and ignored.5556 func ParseAddr(s string) (Addr, bool) {56- a, _, ok := parseRef(s)57+ a, _, ok := ParseRef(s)5758 return a, ok5859 }5960 60-// absFlags records which parts of a reference are absolute ($A$1). Copying61-// a formula shifts only the relative parts.62-type absFlags uint861+// Abs records which parts of a reference are absolute ($A$1). Copying a62+// formula shifts only the relative parts.63+type Abs uint86364 6465 const (65- absCol absFlags = 1 << iota66- absRow66+ AbsCol Abs = 1 << iota67+ AbsRow6768 )6869 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 absFlags70+// ParseRef parses a reference written as A1, $A1, A$1 or $A$1.71+func ParseRef(s string) (Addr, Abs, bool) {72+ var abs Abs7273 if rest, ok := strings.CutPrefix(s, "$"); ok {73- s, abs = rest, absCol74+ s, abs = rest, AbsCol7475 }7576 i := 07677 for i < len(s) && isLetter(s[i]) {@@ -82,7 +83,7 @@ func parseRef(s string) (Addr, absFlags, bool) {
8283 }8384 digits := s[i:]8485 if rest, ok := strings.CutPrefix(digits, "$"); ok {85- digits, abs = rest, abs|absRow86+ digits, abs = rest, abs|AbsRow8687 }8788 if digits == "" || strings.ContainsFunc(digits, func(r rune) bool { return r < '0' || r > '9' }) {8889 return Addr{}, 0, false@@ -92,14 +93,14 @@ func parseRef(s string) (Addr, absFlags, bool) {
9293 return a, abs, err == nil && a.Valid()9394 }9495 95-// refString writes a with its absolute markers, e.g. $A1.96-func refString(a Addr, abs absFlags) string {96+// RefString writes a with its absolute markers, e.g. $A1.97+func RefString(a Addr, abs Abs) string {9798 var b strings.Builder98- if abs&absCol != 0 {99+ if abs&AbsCol != 0 {99100 b.WriteByte('$')100101 }101102 b.WriteString(ColName(a.Col))102- if abs&absRow != 0 {103+ if abs&AbsRow != 0 {103104 b.WriteByte('$')104105 }105106 b.WriteString(strconv.Itoa(a.Row + 1))@@ -150,6 +151,26 @@ func ParseRange(s string) (Rect, bool) {
150151 return NewRect(a, b), ok1 && ok2151152 }152153 154+// LooksLikeRef reports whether an upper-case name reads as a cell in A1 or155+// R1C1 style, even beyond this sheet's edges, so names stay unambiguous in156+// other spreadsheets too.157+func LooksLikeRef(k string) bool {158+ letters := strings.TrimLeft(k, "ABCDEFGHIJKLMNOPQRSTUVWXYZ")159+ if letters != k && letters != "" && strings.Trim(letters, "0123456789") == "" {160+ return true161+ }162+ rest, ok := strings.CutPrefix(k, "R")163+ if !ok {164+ rest, ok = k, strings.HasPrefix(k, "C")165+ }166+ if !ok {167+ return false168+ }169+ rest = strings.TrimLeft(rest, "0123456789")170+ rest, _ = strings.CutPrefix(rest, "C")171+ return strings.Trim(rest, "0123456789") == ""172+}173+153174 func isLetter(c byte) bool {154175 return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')155176 }@@ -0,0 +1,207 @@
1+package formula2+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+// Rewriter maps the references in a formula. Any function may return9+// RefErr; a nil function leaves those nodes alone.10+type Rewriter struct {11+ Ref func(Ref) Node12+ Range func(Range) Node13+ Name func(Name) Node14+}15+16+// Rewrite returns n with its references mapped, and whether anything17+// changed. Unchanged subtrees are shared.18+func Rewrite(n Node, rw Rewriter) (Node, bool) {19+ switch n := n.(type) {20+ case Ref, Range, Name:21+ out := rw.leaf(n)22+ return out, out != n23+ case Unary:24+ x, ok := Rewrite(n.X, rw)25+ if ok {26+ n.X = x27+ }28+ return n, ok29+ case Binary:30+ l, okL := Rewrite(n.L, rw)31+ r, okR := Rewrite(n.R, rw)32+ if okL || okR {33+ n.L, n.R = l, r34+ }35+ return n, okL || okR36+ case Call:37+ return rewriteCall(n, rw)38+ }39+ return n, false40+}41+42+// leaf maps a reference, range or name.43+func (rw Rewriter) leaf(n Node) Node {44+ switch n := n.(type) {45+ case Ref:46+ if rw.Ref != nil {47+ return rw.Ref(n)48+ }49+ case Range:50+ if rw.Range != nil {51+ return rw.Range(n)52+ }53+ case Name:54+ if rw.Name != nil {55+ return rw.Name(n)56+ }57+ }58+ return n59+}60+61+// rewriteCall rewrites a call's arguments, copying them only if one62+// changed.63+func rewriteCall(n Call, rw Rewriter) (Node, bool) {64+ var args []Node65+ for i, a := range n.Args {66+ if b, ok := Rewrite(a, rw); ok {67+ if args == nil {68+ args = append([]Node(nil), n.Args...)69+ }70+ args[i] = b71+ }72+ }73+ if args == nil {74+ return n, false75+ }76+ n.Args = args77+ return n, true78+}79+80+// Shift is the rewrite for copying a formula by (dc, dr): relative parts81+// move, $absolute parts stay, and references pushed off the sheet become82+// #REF!.83+func Shift(dc, dr int) Rewriter {84+ move := func(a Addr, abs Abs) (Addr, bool) {85+ if abs&AbsCol == 0 {86+ a.Col += dc87+ }88+ if abs&AbsRow == 0 {89+ a.Row += dr90+ }91+ return a, a.Valid()92+ }93+ return Rewriter{94+ Ref: func(n Ref) Node {95+ a, ok := move(n.Addr, n.Abs)96+ if !ok {97+ return RefErr{}98+ }99+ return Ref{a, n.Abs, n.Sheet}100+ },101+ Range: func(n Range) Node {102+ from, ok1 := move(n.Rect.From, n.Abs[0])103+ to, ok2 := move(n.Rect.To, n.Abs[1])104+ if !ok1 || !ok2 {105+ return RefErr{}106+ }107+ r := NewRange(from, to, n.Abs[0], n.Abs[1])108+ r.Sheet = n.Sheet109+ return r110+ },111+ }112+}113+114+// Relocate is the rewrite for cells that move on one sheet: cell maps115+// where each cell went (false if it's gone), and rng maps whole ranges.116+// on reports whether a reference written with a sheet name ("" for none)117+// points at the sheet whose cells moved; other references stay.118+func Relocate(on func(sheet string) bool, cell func(Addr) (Addr, bool), rng func(Rect) (Rect, bool)) Rewriter {119+ return Rewriter{120+ Ref: func(n Ref) Node {121+ if !on(n.Sheet) {122+ return n123+ }124+ a, ok := cell(n.Addr)125+ if !ok {126+ return RefErr{}127+ }128+ return Ref{a, n.Abs, n.Sheet}129+ },130+ Range: func(n Range) Node {131+ if !on(n.Sheet) {132+ return n133+ }134+ r, ok := rng(n.Rect)135+ if !ok {136+ return RefErr{}137+ }138+ return Range{r, n.Abs, n.Sheet}139+ },140+ }141+}142+143+// Span inserts (N > 0) or deletes (N < 0) lines starting at index At,144+// along an axis with Size lines.145+type Span struct{ At, N, Size int }146+147+// Point maps a line index; false if the line was deleted or pushed off148+// the end.149+func (sp Span) Point(v int) (int, bool) {150+ switch {151+ case v < sp.At:152+ return v, true153+ case sp.N < 0 && v < sp.At-sp.N:154+ return 0, false155+ }156+ v += sp.N157+ return v, v < sp.Size158+}159+160+// Interval maps a range's lines lo..hi as Sheets does: inserting inside161+// the range grows it, deleting part of it shrinks it, and deleting all of162+// it leaves #REF!.163+func (sp Span) Interval(lo, hi int) (int, int, bool) {164+ if sp.N > 0 {165+ lo, okLo := sp.Point(lo)166+ if !okLo {167+ return 0, 0, false168+ }169+ hi, _ := sp.Point(hi)170+ return lo, min(hi, sp.Size-1), true171+ }172+ end := sp.At - sp.N // first line after the deletion173+ if lo >= sp.At && hi < end {174+ return 0, 0, false175+ }176+ if lo >= sp.At {177+ lo = max(lo, end) + sp.N178+ }179+ if hi >= sp.At {180+ hi = max(hi+sp.N, sp.At-1)181+ }182+ return lo, hi, true183+}184+185+// AxisMaps are the cell and range mappings for inserting or deleting186+// rows (rows true) or columns, for Relocate.187+func AxisMaps(rows bool, sp Span) (func(Addr) (Addr, bool), func(Rect) (Rect, bool)) {188+ cell := func(a Addr) (Addr, bool) {189+ var ok bool190+ if rows {191+ a.Row, ok = sp.Point(a.Row)192+ } else {193+ a.Col, ok = sp.Point(a.Col)194+ }195+ return a, ok196+ }197+ rng := func(r Rect) (Rect, bool) {198+ var ok bool199+ if rows {200+ r.From.Row, r.To.Row, ok = sp.Interval(r.From.Row, r.To.Row)201+ } else {202+ r.From.Col, r.To.Col, ok = sp.Interval(r.From.Col, r.To.Col)203+ }204+ return r, ok205+ }206+ return cell, rng207+}@@ -0,0 +1,75 @@
1+package formula2+3+import "strings"4+5+// Sheet names in references: Sheet2!A1, 'Q3 plan'!B2:C9.6+7+// SheetKey is how sheet names compare: ignoring case, as in Sheets.8+func SheetKey(name string) string { return strings.ToUpper(name) }9+10+// QuoteSheet writes a sheet name as a formula needs it: bare when it reads11+// as a plain identifier (Sheet2), otherwise in single quotes with quotes12+// doubled, e.g. 'Q3 plan'.13+func QuoteSheet(name string) string {14+ bare := name != "" && (isLetter(name[0]) || name[0] == '_') && !looksLikeCell(strings.ToUpper(name))15+ for i := 0; bare && i < len(name); i++ {16+ c := name[i]17+ bare = isLetter(c) || isDigit(c) || c == '_' || c == '.'18+ }19+ if bare {20+ return name21+ }22+ return "'" + strings.ReplaceAll(name, "'", "''") + "'"23+}24+25+// looksLikeCell reports whether an upper-case name reads as a cell in26+// Excel, A1 to XFD1048576 or R1C1, so a sheet of that name is quoted27+// everywhere its formulas may go. Sheet1 doesn't: SHEET is no column.28+func looksLikeCell(k string) bool {29+ digits := strings.TrimLeft(k, "ABCDEFGHIJKLMNOPQRSTUVWXYZ")30+ if n := len(k) - len(digits); n >= 1 && n <= 3 && digits != "" && strings.Trim(digits, "0123456789") == "" {31+ return true32+ }33+ return (strings.HasPrefix(k, "R") || strings.HasPrefix(k, "C")) && LooksLikeRef(k)34+}35+36+// quotedName reads a quoted sheet name starting at the quote at src[i] and37+// returns it unquoted with the index just past the closing quote.38+func quotedName(src string, i int) (string, int, bool) {39+ var b strings.Builder40+ for j := i + 1; j < len(src); j++ {41+ if src[j] != '\'' {42+ b.WriteByte(src[j])43+ continue44+ }45+ if j+1 < len(src) && src[j+1] == '\'' {46+ b.WriteByte('\'')47+ j++48+ continue49+ }50+ return b.String(), j + 1, b.Len() > 051+ }52+ return "", len(src), false53+}54+55+// SplitSheet splits a reference such as "Sheet2!A1:B3" or "'Q3 plan'!B2"56+// into the sheet name, unquoted, and the rest. Without a sheet, sheet is57+// "" and rest is s.58+func SplitSheet(s string) (sheet, rest string) {59+ s = strings.TrimSpace(s)60+ if strings.HasPrefix(s, "'") {61+ if name, end, ok := quotedName(s, 0); ok && end < len(s) && s[end] == '!' {62+ return name, s[end+1:]63+ }64+ return "", s65+ }66+ if i := strings.LastIndexByte(s, '!'); i > 0 {67+ return s[:i], s[i+1:]68+ }69+ return "", s70+}71+72+// Qualified writes r on sheet as a formula would: Sheet2!A1:B3.73+func Qualified(sheet string, r Rect) string {74+ return QuoteSheet(sheet) + "!" + r.String()75+}@@ -3,6 +3,8 @@ package sheet
33 import (44 "strings"55 "unicode/utf8"6+7+ "github.com/FelineStateMachine/012/internal/formula"68 )79 810 // Argument helpers shared by the function library. Errors come back as a@@ -22,7 +24,7 @@ func given(args []Node, i int) bool {
2224 if i >= len(args) {2325 return false2426 }25- _, empty := args[i].(emptyArg)27+ _, empty := args[i].(formula.Empty)2628 return !empty2729 }2830 @@ -82,10 +84,10 @@ func (m matrix) vector() bool { return m.rows == 1 || m.cols == 1 }
8284 8385 func matrixArg(n Node, get lookup) matrix {8486 switch n := n.(type) {85- case rangeNode:86- return rectMatrix(n.sheet, n.r, get)87- case refNode:88- return rectMatrix(n.sheet, Rect{From: n.a, To: n.a}, get)87+ case formula.Range:88+ return rectMatrix(n.Sheet, n.Rect, get)89+ case formula.Ref:90+ return rectMatrix(n.Sheet, Rect{From: n.Addr, To: n.Addr}, get)8991 }9092 v := eval(n, get)9193 return matrix{rows: 1, cols: 1, cell: func(int, int) Value { return v }}@@ -1,5 +1,7 @@
11 package sheet22 3+import "github.com/FelineStateMachine/012/internal/formula"4+35 // Formatting changes go through these methods rather than by editing46 // cells directly, so history (undo) can hook a single place.57 @@ -102,15 +104,15 @@ func (s *Sheet) eachCell(r Rect, create bool, fn func(Addr, *Cell)) {
102104 // without its inputs changing (TODAY, NOW, RAND).103105 func isVolatile(n Node) bool {104106 switch n := n.(type) {105- case unaryNode:106- return isVolatile(n.x)107- case binaryNode:108- return isVolatile(n.l) || isVolatile(n.r)109- case callNode:110- if n.fn.Volatile {107+ case formula.Unary:108+ return isVolatile(n.X)109+ case formula.Binary:110+ return isVolatile(n.L) || isVolatile(n.R)111+ case formula.Call:112+ if funcOf(n).Volatile {111113 return true112114 }113- for _, a := range n.args {115+ for _, a := range n.Args {114116 if isVolatile(a) {115117 return true116118 }@@ -125,19 +127,19 @@ func isVolatile(n Node) bool {
125127 // currency and a date plus days shows a date.126128 func inferFormat(n Node, at func(string, Addr) Format) Format {127129 switch n := n.(type) {128- case refNode:129- return at(n.sheet, n.a)130- case rangeNode:131- return at(n.sheet, n.r.From)132- case unaryNode:133- if n.op == "-" || n.op == "+" {134- return inferFormat(n.x, at)130+ case formula.Ref:131+ return at(n.Sheet, n.Addr)132+ case formula.Range:133+ return at(n.Sheet, n.Rect.From)134+ case formula.Unary:135+ if n.Op == "-" || n.Op == "+" {136+ return inferFormat(n.X, at)135137 }136- case binaryNode:137- l, r := inferFormat(n.l, at), inferFormat(n.r, at)138- switch n.op {138+ case formula.Binary:139+ l, r := inferFormat(n.L, at), inferFormat(n.R, at)140+ switch n.Op {139141 case "+", "-":140- if n.op == "-" && l.Kind.isTime() && r.Kind.isTime() {142+ if n.Op == "-" && l.Kind.isTime() && r.Kind.isTime() {141143 return Format{} // days between two dates142144 }143145 return firstFormat(l, r)@@ -147,9 +149,9 @@ func inferFormat(n Node, at func(string, Addr) Format) Format {
147149 }148150 return firstFormat(l, r)149151 }150- case callNode:151- if n.fn.format != nil {152- return n.fn.format(n.args, func(n Node) Format { return inferFormat(n, at) })152+ case formula.Call:153+ if funcOf(n).format != nil {154+ return funcOf(n).format(n.Args, func(n Node) Format { return inferFormat(n, at) })153155 }154156 }155157 return Format{}@@ -158,7 +158,7 @@ func (s *Sheet) displayText(a Addr) string {
158158 func (s *Sheet) clipToUsed(r Rect) Rect {159159 used, ok := s.UsedRange()160160 if !ok {161- return Rect{r.From, r.From}161+ return Rect{From: r.From, To: r.From}162162 }163163 r.To.Col = max(min(r.To.Col, used.To.Col), r.From.Col)164164 r.To.Row = max(min(r.To.Row, used.To.Row), r.From.Row)@@ -189,14 +189,14 @@ func (s *Sheet) Region(a Addr) Rect {
189189 }190190 around(a)191191 if len(queue) == 0 {192- return Rect{a, a}192+ return Rect{From: a, To: a}193193 }194- r := Rect{queue[0], queue[0]}194+ r := Rect{From: queue[0], To: queue[0]}195195 for len(queue) > 0 {196196 for len(queue) > 0 {197197 p := queue[0]198198 queue = queue[1:]199- r = union(r, Rect{p, p})199+ r = union(r, Rect{From: p, To: p})200200 around(p)201201 }202202 // Cells touching the bounding box join too.@@ -3,6 +3,7 @@ package sheet
33 import (44 "sync"55 6+ "github.com/FelineStateMachine/012/internal/formula"67 "github.com/FelineStateMachine/012/internal/numfmt"78 "github.com/cockroachdb/apd/v3"89 )@@ -199,24 +200,31 @@ var decFuncs = sync.OnceValue(func() map[string]*FuncDef {
199200 return out200201 })201202 203+// Operators computed in decimal are marked by these types, which eval204+// knows; parsed formulas never contain them.205+type (206+ decUnary formula.Unary207+ decBinary formula.Binary208+)209+202210 // decimalize returns a copy of n that evaluates in decimal: operators are203211 // marked, and functions with a decimal twin call it.204212 func decimalize(n Node) Node {205213 switch n := n.(type) {206- case unaryNode:207- n.x, n.dec = decimalize(n.x), true208- return n209- case binaryNode:210- n.l, n.r, n.dec = decimalize(n.l), decimalize(n.r), true211- return n212- case callNode:213- args := make([]Node, len(n.args))214- for i, a := range n.args {214+ case formula.Unary:215+ n.X = decimalize(n.X)216+ return decUnary(n)217+ case formula.Binary:218+ n.L, n.R = decimalize(n.L), decimalize(n.R)219+ return decBinary(n)220+ case formula.Call:221+ args := make([]Node, len(n.Args))222+ for i, a := range n.Args {215223 args[i] = decimalize(a)216224 }217- n.args = args218- if twin, ok := decFuncs()[n.fn.Name]; ok {219- n.fn = twin225+ n.Args = args226+ if twin, ok := decFuncs()[funcOf(n).Name]; ok {227+ n.Fn = twin220228 }221229 return n222230 }@@ -4,6 +4,7 @@ import (
44 "math"55 "strings"66 7+ "github.com/FelineStateMachine/012/internal/formula"78 "github.com/FelineStateMachine/012/internal/numfmt"89 )910 @@ -94,80 +95,92 @@ type lookup func(sheet string, a Addr) Value
9495 9596 func eval(n Node, get lookup) Value {9697 switch n := n.(type) {97- case numLit:98- return num(n.v)99- case strLit:100- return Value{Kind: Text, Str: n.v}101- case boolLit:102- return boolean(n.v)103- case refNode:104- return get(n.sheet, n.a)105- case nameNode:98+ case formula.Num:99+ return num(n.V)100+ case formula.Str:101+ return Value{Kind: Text, Str: n.V}102+ case formula.Bool:103+ return boolean(n.V)104+ case formula.Ref:105+ return get(n.Sheet, n.Addr)106+ case formula.Name:106107 return ErrName107- case refErrNode:108+ case formula.RefErr:108109 return ErrRef109- case emptyArg:110+ case formula.Empty:110111 return Value{}111- case rangeNode:112+ case formula.Range:112113 // A range outside a function: Sheets uses the top-left cell here113114 // for single-cell ranges and #VALUE! otherwise.114- if n.r.From == n.r.To {115- return get(n.sheet, n.r.From)115+ if n.Rect.From == n.Rect.To {116+ return get(n.Sheet, n.Rect.From)116117 }117118 return ErrValue118- case unaryNode:119- x := eval(n.x, get)120- if x.Kind == Error {121- return x119+ case formula.Unary:120+ return evalUnary(n, get, false)121+ case decUnary:122+ return evalUnary(formula.Unary(n), get, true)123+ case formula.Binary:124+ return evalBinary(n, get, false)125+ case decBinary:126+ return evalBinary(formula.Binary(n), get, true)127+ case formula.Call:128+ return funcOf(n).call(n.Args, get)129+ }130+ return ErrValue131+}132+133+// evalUnary computes a prefix operator or the postfix %; with dec, % is134+// decimal (decimal.go).135+func evalUnary(n formula.Unary, get lookup, dec bool) Value {136+ x := eval(n.X, get)137+ if x.Kind == Error {138+ return x139+ }140+ switch n.Op {141+ case "-":142+ f, err := toNum(x)143+ if err != nil {144+ return *err122145 }123- switch n.op {124- case "-":125- f, err := toNum(x)126- if err != nil {127- return *err128- }129- return num(-f)130- case "#NOT#":131- f, err := toNum(x)132- if err != nil {133- return *err134- }135- return boolean(f == 0)136- case "%":137- f, err := toNum(x)138- if err != nil {139- return *err140- }141- if n.dec {142- if v, ok := decArith("/", f, 100); ok {143- return v144- }146+ return num(-f)147+ case "#NOT#":148+ f, err := toNum(x)149+ if err != nil {150+ return *err151+ }152+ return boolean(f == 0)153+ case "%":154+ f, err := toNum(x)155+ if err != nil {156+ return *err157+ }158+ if dec {159+ if v, ok := decArith("/", f, 100); ok {160+ return v145161 }146- return num(f / 100)147162 }148- return x // unary + is identity149- case binaryNode:150- return evalBinary(n, get)151- case callNode:152- return n.fn.call(n.args, get)163+ return num(f / 100)153164 }154- return ErrValue165+ return x // unary + is identity155166 }156167 157-func evalBinary(n binaryNode, get lookup) Value {158- l, r := eval(n.l, get), eval(n.r, get)168+// evalBinary computes a binary operator; with dec, arithmetic is decimal169+// (decimal.go).170+func evalBinary(n formula.Binary, get lookup, dec bool) Value {171+ l, r := eval(n.L, get), eval(n.R, get)159172 if l.Kind == Error {160173 return l161174 }162175 if r.Kind == Error {163176 return r164177 }165- switch n.op {178+ switch n.Op {166179 case "&":167180 return Value{Kind: Text, Str: text(l) + text(r)}168181 case "=", "<>", "<", ">", "<=", ">=":169182 c := compare(l, r)170- switch n.op {183+ switch n.Op {171184 case "=":172185 return boolean(c == 0)173186 case "<>":@@ -189,12 +202,12 @@ func evalBinary(n binaryNode, get lookup) Value {
189202 if err != nil {190203 return *err191204 }192- if n.dec {193- if v, ok := decArith(n.op, a, b); ok {205+ if dec {206+ if v, ok := decArith(n.Op, a, b); ok {194207 return v195208 }196209 }197- switch n.op {210+ switch n.Op {198211 case "+":199212 return num(a + b)200213 case "-":@@ -3,6 +3,8 @@ package sheet
33 import (44 "slices"55 "strings"6+7+ "github.com/FelineStateMachine/012/internal/formula"68 )79 810 // ExplainError says why the cell at a shows an error, for the context@@ -19,7 +21,7 @@ func (l loc) label(from *Sheet) string {
1921 if l.s == from {2022 return l.a.String()2123 }22- return quoteSheet(l.s.name) + "!" + l.a.String()24+ return formula.QuoteSheet(l.s.name) + "!" + l.a.String()2325 }2426 2527 // maxExplainExpr caps how much of a formula an explanation quotes.@@ -57,11 +59,11 @@ func (s *Sheet) explain(a Addr, path []loc) string {
5759 }5860 return "From " + name + ": " + strings.ToLower(why[:1]) + why[1:]5961 }60- if r, ok := n.(refNode); ok && s.wb.resolve(s, r.sheet) == nil {61- return "Unresolved sheet name " + quoteSheet(r.sheet)62+ if r, ok := n.(formula.Ref); ok && s.wb.resolve(s, r.Sheet) == nil {63+ return "Unresolved sheet name " + formula.QuoteSheet(r.Sheet)6264 }63- if r, ok := n.(rangeNode); ok && s.wb.resolve(s, r.sheet) == nil {64- return "Unresolved sheet name " + quoteSheet(r.sheet)65+ if r, ok := n.(formula.Range); ok && s.wb.resolve(s, r.Sheet) == nil {66+ return "Unresolved sheet name " + formula.QuoteSheet(r.Sheet)6567 }6668 return describeError(v, n)6769 }@@ -79,35 +81,35 @@ func (s *Sheet) errorOrigin(n Node, want Value) (Node, *loc) {
7981 return nil // an unresolved sheet name is the error itself8082 }8183 switch n := n.(type) {82- case refNode:83- if same(get(n.sheet, n.a)) {84- return n, at(n.sheet, n.a)84+ case formula.Ref:85+ if same(get(n.Sheet, n.Addr)) {86+ return n, at(n.Sheet, n.Addr)8587 }86- case rangeNode:87- if n.r.From == n.r.To && same(get(n.sheet, n.r.From)) {88- return n, at(n.sheet, n.r.From)88+ case formula.Range:89+ if n.Rect.From == n.Rect.To && same(get(n.Sheet, n.Rect.From)) {90+ return n, at(n.Sheet, n.Rect.From)8991 }90- case unaryNode:91- if same(eval(n.x, get)) {92- return s.errorOrigin(n.x, want)92+ case formula.Unary:93+ if same(eval(n.X, get)) {94+ return s.errorOrigin(n.X, want)9395 }94- case binaryNode:95- for _, x := range []Node{n.l, n.r} {96+ case formula.Binary:97+ for _, x := range []Node{n.L, n.R} {9698 if same(eval(x, get)) {9799 return s.errorOrigin(x, want)98100 }99101 }100- case callNode:101- if n.fn.remote != nil {102+ case formula.Call:103+ if funcOf(n).remote != nil {102104 return n, nil // JEV explains its own answers103105 }104- for _, arg := range n.args {105- if r, ok := arg.(rangeNode); ok && r.r.From != r.r.To {106- t := s.wb.resolve(s, r.sheet)106+ for _, arg := range n.Args {107+ if r, ok := arg.(formula.Range); ok && r.Rect.From != r.Rect.To {108+ t := s.wb.resolve(s, r.Sheet)107109 if t == nil {108110 return arg, nil109111 }110- cells := t.cellsIn(r.r)112+ cells := t.cellsIn(r.Rect)111113 sortAddrs(cells)112114 for _, a := range cells {113115 if same(t.Value(a)) {@@ -129,9 +131,7 @@ var lookupFuncs = []string{"VLOOKUP", "HLOOKUP", "XLOOKUP", "LOOKUP", "MATCH", "
129131 130132 // describeError explains an error code produced by n.131133 func describeError(v Value, n Node) string {132- var b strings.Builder133- printNode(&b, n)134- expr := b.String()134+ expr := formula.Expr(n)135135 if r := []rune(expr); len(r) > maxExplainExpr {136136 expr = string(r[:maxExplainExpr-1]) + "…"137137 }@@ -139,24 +139,24 @@ func describeError(v Value, n Node) string {
139139 case ErrDiv0.Str:140140 return "Division by zero in " + expr141141 case ErrValue.Str:142- if _, ok := n.(rangeNode); ok {142+ if _, ok := n.(formula.Range); ok {143143 return "A range where one value is expected: " + expr144144 }145145 return "Wrong type of value in " + expr146146 case ErrName.Str:147- if nm, ok := n.(nameNode); ok {148- return "Unknown name " + nm.name147+ if nm, ok := n.(formula.Name); ok {148+ return "Unknown name " + nm.Name149149 }150150 return "Unknown name in " + expr151151 case ErrNA.Str:152- if call, ok := n.(callNode); ok && slices.Contains(lookupFuncs, call.fn.Name) {152+ if call, ok := n.(formula.Call); ok && slices.Contains(lookupFuncs, funcOf(call).Name) {153153 return "No match found by " + expr154154 }155155 return "Value not available in " + expr156156 case ErrNum.Str:157157 return "Number out of range in " + expr158158 case ErrRef.Str:159- if _, ok := n.(refErrNode); ok {159+ if _, ok := n.(formula.RefErr); ok {160160 return "Reference to deleted cells"161161 }162162 return "Invalid reference in " + expr@@ -59,7 +59,7 @@ func TestFilterConditions(t *testing.T) {
5959 for _, tt := range tests {6060 t.Run(tt.name, func(t *testing.T) {6161 s := fruit(t)62- s.CreateFilter(Rect{at("A1"), at("B6")})62+ s.CreateFilter(Rect{From: at("A1"), To: at("B6")})6363 s.FilterColumn(tt.col, tt.cr)6464 if got := shownRows(s); !reflect.DeepEqual(got, tt.want) {6565 t.Errorf("shown rows %v, want %v", got, tt.want)@@ -74,7 +74,7 @@ func TestFilterConditions(t *testing.T) {
7474 7575 func TestFilterFollowsValuesAndUndo(t *testing.T) {7676 s := fruit(t)77- s.CreateFilter(Rect{at("A1"), at("B6")})77+ s.CreateFilter(Rect{From: at("A1"), To: at("B6")})7878 s.FilterColumn(1, Criteria{Cond: Condition{CondGreater, "5"}})7979 if !s.RowHidden(2) || s.HiddenRows() != 3 {8080 t.Fatalf("row 3 shown, %d hidden", s.HiddenRows())@@ -109,7 +109,7 @@ func TestFilterFollowsValuesAndUndo(t *testing.T) {
109109 110110 func TestFilterValues(t *testing.T) {111111 s := fruit(t)112- s.CreateFilter(Rect{at("A1"), at("B6")})112+ s.CreateFilter(Rect{From: at("A1"), To: at("B6")})113113 s.FilterColumn(1, Criteria{Hidden: []string{"3", "gone"}})114114 want := []FilterValue{{"3", 2, false}, {"10", 1, true}, {"$25", 1, true}, {"gone", 0, false}, {"", 1, true}}115115 if got := s.FilterValues(1); !reflect.DeepEqual(got, want) {@@ -125,7 +125,7 @@ func TestFilterValues(t *testing.T) {
125125 126126 func TestFilterFollowsInsertAndDelete(t *testing.T) {127127 s := fruit(t)128- s.CreateFilter(Rect{at("A1"), at("B6")})128+ s.CreateFilter(Rect{From: at("A1"), To: at("B6")})129129 s.FilterColumn(1, Criteria{Hidden: []string{"3"}})130130 s.InsertRows(2, 2) // inside the range: it grows131131 if f := s.Filter(); f.Range.String() != "A1:B8" {@@ -148,7 +148,7 @@ func TestFilterFollowsInsertAndDelete(t *testing.T) {
148148 149149 func TestEdgeSkipsHiddenRows(t *testing.T) {150150 s := fruit(t)151- s.CreateFilter(Rect{at("A1"), at("B6")})151+ s.CreateFilter(Rect{From: at("A1"), To: at("B6")})152152 s.FilterColumn(1, Criteria{Cond: Condition{CondEqual, "3"}}) // rows 1, 3 and 6 show153153 tests := []struct {154154 from string@@ -200,7 +200,7 @@ func TestFreeze(t *testing.T) {
200200 func TestViewFileRoundTrip(t *testing.T) {201201 s := fruit(t)202202 s.SetFrozen(1, 2)203- s.CreateFilter(Rect{at("A1"), at("B6")})203+ s.CreateFilter(Rect{From: at("A1"), To: at("B6")})204204 s.FilterColumn(1, Criteria{Hidden: []string{"3", ""}, Cond: Condition{CondLess, "$20"}})205205 var buf bytes.Buffer206206 if err := s.Write(&buf); err != nil {@@ -0,0 +1,77 @@
1+package sheet2+3+import "github.com/FelineStateMachine/012/internal/formula"4+5+// The formula language lives in internal/formula. The engine's API keeps6+// its names for addresses, ranges and parsing, so callers see one package.7+8+type (9+ // Addr identifies a cell by zero-based column and row.10+ Addr = formula.Addr11+ // Rect is an inclusive rectangular range of cells.12+ Rect = formula.Rect13+ // Node is a parsed formula expression.14+ Node = formula.Node15+ // ParseError describes a formula that could not be parsed.16+ ParseError = formula.ParseError17+)18+19+// Worksheet bounds, matching Lotus 1-2-3 Release 2 (A..IV, 1..8192).20+const (21+ MaxCols = formula.MaxCols22+ MaxRows = formula.MaxRows23+)24+25+// ColName converts a zero-based column index to letters: 0 -> A, 26 -> AA.26+func ColName(c int) string { return formula.ColName(c) }27+28+// ParseCol converts column letters (case-insensitive) to a zero-based index.29+func ParseCol(s string) (int, bool) { return formula.ParseCol(s) }30+31+// ParseAddr parses an A1-style reference, ignoring absolute markers.32+func ParseAddr(s string) (Addr, bool) { return formula.ParseAddr(s) }33+34+// NewRect returns the normalized rectangle spanning a and b.35+func NewRect(a, b Addr) Rect { return formula.NewRect(a, b) }36+37+// ParseRange parses "A1", "A1:B3" or 1-2-3 style "A1..B3".38+func ParseRange(s string) (Rect, bool) { return formula.ParseRange(s) }39+40+// QuoteSheet writes a sheet name as a formula needs it, e.g. 'Q3 plan'.41+func QuoteSheet(name string) string { return formula.QuoteSheet(name) }42+43+// SplitSheet splits a reference such as "'Q3 plan'!B2" into the sheet44+// name, unquoted, and the rest.45+func SplitSheet(s string) (sheet, rest string) { return formula.SplitSheet(s) }46+47+// Qualified writes r on sheet as a formula would: Sheet2!A1:B3.48+func Qualified(sheet string, r Rect) string { return formula.Qualified(sheet, r) }49+50+// Parse parses a formula with this engine's functions. src may start51+// with "="; error positions are relative to src.52+func Parse(src string) (Node, error) { return formula.Parse(src, parserFuncs) }53+54+// parserFuncs is the function table as the parser sees it.55+func parserFuncs(name string) (formula.Func, bool) {56+ if f, ok := LookupFunc(name); ok {57+ return f, true58+ }59+ return nil, false60+}61+62+// Signature is how the parser checks calls to f.63+func (f *FuncDef) Signature() formula.Signature {64+ return formula.Signature{Name: f.Name, Args: f.Args, Min: f.Min, Max: f.Max, Step: f.step}65+}66+67+// funcOf is the function a parsed call calls: always one of ours, since68+// the parser only finds functions through parserFuncs.69+func funcOf(c formula.Call) *FuncDef { return c.Fn.(*FuncDef) }70+71+func isLetter(c byte) bool {72+ return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')73+}74+75+func isDigit(c byte) bool {76+ return c >= '0' && c <= '9'77+}@@ -4,6 +4,7 @@ import (
44 "math"55 "slices"66 7+ "github.com/FelineStateMachine/012/internal/formula"78 "github.com/FelineStateMachine/012/internal/numfmt"89 )910 @@ -164,19 +165,19 @@ type agg struct {
164165 // #VALUE!.165166 func each(args []Node, get lookup, fn func(v Value, direct bool) *Value) *Value {166167 for _, arg := range args {167- if ref, ok := arg.(refNode); ok {168- if e := fn(get(ref.sheet, ref.a), false); e != nil {168+ if ref, ok := arg.(formula.Ref); ok {169+ if e := fn(get(ref.Sheet, ref.Addr), false); e != nil {169170 return e170171 }171172 continue172173 }173- if _, ok := arg.(emptyArg); ok {174+ if _, ok := arg.(formula.Empty); ok {174175 continue175176 }176- if rn, ok := arg.(rangeNode); ok {177- for r := rn.r.From.Row; r <= rn.r.To.Row; r++ {178- for c := rn.r.From.Col; c <= rn.r.To.Col; c++ {179- if e := fn(get(rn.sheet, Addr{Col: c, Row: r}), false); e != nil {177+ if rn, ok := arg.(formula.Range); ok {178+ for r := rn.Rect.From.Row; r <= rn.Rect.To.Row; r++ {179+ for c := rn.Rect.From.Col; c <= rn.Rect.To.Col; c++ {180+ if e := fn(get(rn.Sheet, Addr{Col: c, Row: r}), false); e != nil {180181 return e181182 }182183 }@@ -2,6 +2,8 @@ package sheet
22 33 import (44 "strings"5+6+ "github.com/FelineStateMachine/012/internal/formula"57 )68 79 // JEV functions ask TypeSafe's hosted model (jev) typed questions about a@@ -76,12 +78,12 @@ func (e inputError) Error() string { return e.v.Str }
7678 // jevState turns the value argument into what the model sees: text,7779 // numbers and booleans as themselves, a range as rows of them.7880 func jevState(n Node, get lookup) (any, error) {79- if rn, ok := n.(rangeNode); ok {81+ if rn, ok := n.(formula.Range); ok {8082 var rows [][]any81- for r := rn.r.From.Row; r <= rn.r.To.Row; r++ {83+ for r := rn.Rect.From.Row; r <= rn.Rect.To.Row; r++ {8284 var row []any83- for c := rn.r.From.Col; c <= rn.r.To.Col; c++ {84- v := get(rn.sheet, Addr{Col: c, Row: r})85+ for c := rn.Rect.From.Col; c <= rn.Rect.To.Col; c++ {86+ v := get(rn.Sheet, Addr{Col: c, Row: r})8587 if v.Kind == Error {8688 return nil, inputError{v}8789 }@@ -134,10 +136,10 @@ func jevQuestion(n Node, get lookup) (string, error) {
134136 // skipped.135137 func jevList(n Node, get lookup) ([]string, error) {136138 var out []string137- if rn, ok := n.(rangeNode); ok {138- for r := rn.r.From.Row; r <= rn.r.To.Row; r++ {139- for c := rn.r.From.Col; c <= rn.r.To.Col; c++ {140- v := get(rn.sheet, Addr{Col: c, Row: r})139+ if rn, ok := n.(formula.Range); ok {140+ for r := rn.Rect.From.Row; r <= rn.Rect.To.Row; r++ {141+ for c := rn.Rect.From.Col; c <= rn.Rect.To.Col; c++ {142+ v := get(rn.Sheet, Addr{Col: c, Row: r})141143 if v.Kind == Error {142144 return nil, inputError{v}143145 }@@ -1,6 +1,10 @@
11 package sheet22 3-import "strings"3+import (4+ "strings"5+6+ "github.com/FelineStateMachine/012/internal/formula"7+)48 59 func init() {610 define(&FuncDef{Name: "HYPERLINK", Args: "url, [link_label]", Desc: "A link that opens url, shown as its label", Min: 1, Max: 2,@@ -30,8 +34,8 @@ func (s *Sheet) Link(a Addr) string {
3034 if c == nil || c.Value.Kind != Text {3135 return ""3236 }33- if call, ok := c.expr.(callNode); ok && call.fn.Name == "HYPERLINK" && c.IsFormula() {34- v := eval(call.args[0], s.wb.values(s))37+ if call, ok := c.expr.(formula.Call); ok && funcOf(call).Name == "HYPERLINK" && c.IsFormula() {38+ v := eval(call.Args[0], s.wb.values(s))3539 if v.Kind == Error {3640 return ""3741 }@@ -3,6 +3,8 @@ package sheet
33 import (44 "maps"55 "slices"6+7+ "github.com/FelineStateMachine/012/internal/formula"68 )79 810 // MaxUndo is how many steps of undo history a workbook keeps.@@ -222,7 +224,7 @@ func (w *Workbook) setSheets(l *sheetList) {
222224 for _, s := range w.sheets {223225 s.name = l.names[s]224226 if s.live {225- w.byKey[sheetKey(s.name)] = s227+ w.byKey[formula.SheetKey(s.name)] = s226228 } else {227229 w.attach(s)228230 }@@ -404,12 +406,12 @@ func (w *Workbook) swap(from, to *[]*step) (Change, bool) {
404406 405407 // colRect is the range covering whole columns from..to.406408 func colRect(from, to int) Rect {407- return Rect{Addr{Col: from}, Addr{Col: to, Row: MaxRows - 1}}409+ return Rect{From: Addr{Col: from}, To: Addr{Col: to, Row: MaxRows - 1}}408410 }409411 410412 // rowRect is the range covering whole rows from..to.411413 func rowRect(from, to int) Rect {412- return Rect{Addr{Row: from}, Addr{Col: MaxCols - 1, Row: to}}414+ return Rect{From: Addr{Row: from}, To: Addr{Col: MaxCols - 1, Row: to}}413415 }414416 415417 func union(a, b Rect) Rect {@@ -6,6 +6,8 @@ import (
66 "maps"77 "slices"88 "strings"9+10+ "github.com/FelineStateMachine/012/internal/formula"911 )1012 1113 // Named ranges, as in Sheets' Data > Named ranges: a name such as Sales@@ -66,32 +68,12 @@ func ValidName(name string) error {
6668 switch k := nameKey(name); {6769 case k == "TRUE" || k == "FALSE":6870 return errors.New("TRUE and FALSE can't be names")69- case looksLikeRef(k):71+ case formula.LooksLikeRef(k):7072 return errors.New("A name can't look like a cell reference, e.g. A1 or R1C1")7173 }7274 return nil7375 }7476 75-// looksLikeRef reports whether an upper-case name reads as a cell in A1 or76-// R1C1 style, even beyond this sheet's edges, so names stay unambiguous in77-// other spreadsheets too.78-func looksLikeRef(k string) bool {79- letters := strings.TrimLeft(k, "ABCDEFGHIJKLMNOPQRSTUVWXYZ")80- if letters != k && letters != "" && strings.Trim(letters, "0123456789") == "" {81- return true82- }83- rest, ok := strings.CutPrefix(k, "R")84- if !ok {85- rest, ok = k, strings.HasPrefix(k, "C")86- }87- if !ok {88- return false89- }90- rest = strings.TrimLeft(rest, "0123456789")91- rest, _ = strings.CutPrefix(rest, "C")92- return strings.Trim(rest, "0123456789") == ""93-}94-9577 // Names returns the named ranges, sorted by name.9678 func (w *Workbook) Names() []Name {9779 // The keys are already upper case: sorting names by nameKey upper-cased@@ -178,9 +160,9 @@ func (s *Sheet) EditName(old, name string, r Rect) error {
178160 // renameInFormulas rewrites the formulas that use the name with key from179161 // to spell it to instead.180162 func (w *Workbook) renameInFormulas(from, to string) {181- rw := refRewrite{name: func(n nameNode) Node {182- if nameKey(n.name) == from {183- return nameNode{to}163+ rw := formula.Rewriter{Name: func(n formula.Name) Node {164+ if nameKey(n.Name) == from {165+ return formula.Name{Name: to}184166 }185167 return n186168 }}@@ -242,23 +224,23 @@ func (s *Sheet) bound(c *Cell) Node {
242224 if len(c.names) == 0 {243225 return c.expr244226 }245- const fixed = absCol | absRow246- n, _ := rewrite(c.expr, refRewrite{name: func(nn nameNode) Node {247- nm, ok := s.wb.names[nameKey(nn.name)]227+ const fixed = formula.AbsCol | formula.AbsRow228+ n, _ := formula.Rewrite(c.expr, formula.Rewriter{Name: func(nn formula.Name) Node {229+ nm, ok := s.wb.names[nameKey(nn.Name)]248230 switch {249231 case !ok:250232 return nn251233 case nm.Gone():252- return refErrNode{}234+ return formula.RefErr{}253235 }254236 sheet := ""255237 if nm.Sheet != s {256238 sheet = nm.Sheet.name257239 }258240 if nm.Range.From == nm.Range.To {259- return refNode{nm.Range.From, fixed, sheet}241+ return formula.Ref{Addr: nm.Range.From, Abs: fixed, Sheet: sheet}260242 }261- return rangeNode{nm.Range, [2]absFlags{fixed, fixed}, sheet}243+ return formula.Range{Rect: nm.Range, Abs: [2]formula.Abs{fixed, fixed}, Sheet: sheet}262244 }})263245 return n264246 }@@ -4,6 +4,8 @@ import (
44 "errors"55 "fmt"66 "strconv"7+8+ "github.com/FelineStateMachine/012/internal/formula"79 )810 911 // Errors returned by operations that would lose data or don't fit.@@ -22,7 +24,7 @@ const maxFill = 1 << 20
2224 // sheets, with cell and rng, as one undo step. References to other sheets2325 // stay as they are.2426 func (s *Sheet) remap(label string, focus Rect, cell func(Addr) (Addr, bool), rng func(Rect) (Rect, bool)) {25- rw := relocate(s.onThis(s), cell, rng)27+ rw := formula.Relocate(s.onThis(s), cell, rng)2628 next := make(map[Addr]*Cell, len(s.cells))2729 for a, c := range s.cells {2830 if to, ok := cell(a); ok {@@ -35,7 +37,7 @@ func (s *Sheet) remap(label string, focus Rect, cell func(Addr) (Addr, bool), rn
3537 continue3638 }3739 c := l.s.cells[l.a]38- if nc := c.rewritten(relocate(s.onThis(l.s), cell, rng)); nc != c {40+ if nc := c.rewritten(formula.Relocate(s.onThis(l.s), cell, rng)); nc != c {3941 others[l] = nc4042 }4143 }@@ -70,12 +72,12 @@ func (s *Sheet) InsertCols(at, n int) error {
7072 // DeleteRows deletes n rows starting at row at. References to deleted7173 // cells become #REF!; ranges that lose some of their rows shrink.7274 func (s *Sheet) DeleteRows(at, n int) {73- s.restructure(true, span{at, -n, MaxRows})75+ s.restructure(true, formula.Span{At: at, N: -n, Size: MaxRows})7476 }7577 7678 // DeleteCols deletes n columns starting at column at.7779 func (s *Sheet) DeleteCols(at, n int) {78- s.restructure(false, span{at, -n, MaxCols})80+ s.restructure(false, formula.Span{At: at, N: -n, Size: MaxCols})7981 }8082 8183 func (s *Sheet) insert(rows bool, at, n int) error {@@ -92,26 +94,26 @@ func (s *Sheet) insert(rows bool, at, n int) error {
9294 return ErrPushedOff9395 }9496 }95- s.restructure(rows, span{at, n, size})97+ s.restructure(rows, formula.Span{At: at, N: n, Size: size})9698 return nil9799 }98100 99-func (s *Sheet) restructure(rows bool, sp span) {100- cell, rng := axisRewrite(rows, sp)101- last := sp.at + abs(sp.n) - 1102- focus := colRect(sp.at, min(last, MaxCols-1))101+func (s *Sheet) restructure(rows bool, sp formula.Span) {102+ cell, rng := formula.AxisMaps(rows, sp)103+ last := sp.At + abs(sp.N) - 1104+ focus := colRect(sp.At, min(last, MaxCols-1))103105 if rows {104- focus = rowRect(sp.at, min(last, MaxRows-1))106+ focus = rowRect(sp.At, min(last, MaxRows-1))105107 }106108 verb, noun := "insert", "column"107- if sp.n < 0 {109+ if sp.N < 0 {108110 verb = "delete"109111 }110112 if rows {111113 noun = "row"112114 }113- label := fmt.Sprintf("%s %d %s", verb, abs(sp.n), noun)114- if abs(sp.n) > 1 {115+ label := fmt.Sprintf("%s %d %s", verb, abs(sp.N), noun)116+ if abs(sp.N) > 1 {115117 label += "s"116118 }117119 s.change(label, focus, func() {@@ -126,10 +128,10 @@ func (s *Sheet) restructure(rows bool, sp span) {
126128 }127129 128130 // shiftWidths moves column widths along with inserted or deleted columns.129-func (s *Sheet) shiftWidths(sp span) {131+func (s *Sheet) shiftWidths(sp formula.Span) {130132 next := map[int]int{}131133 for c, w := range s.widths {132- if to, ok := sp.point(c); ok {134+ if to, ok := sp.Point(c); ok {133135 next[to] = w134136 }135137 }@@ -151,7 +153,7 @@ func (s *Sheet) shiftWidths(sp span) {
151153 // #REF!. It returns the destination range.152154 func (s *Sheet) Move(src Rect, to Addr) (Rect, error) {153155 dc, dr := to.Col-src.From.Col, to.Row-src.From.Row154- dst := Rect{to, Addr{Col: src.To.Col + dc, Row: src.To.Row + dr}}156+ dst := Rect{From: to, To: Addr{Col: src.To.Col + dc, Row: src.To.Row + dr}}155157 if !dst.To.Valid() {156158 return Rect{}, ErrPasteEdge157159 }@@ -167,7 +169,7 @@ func (s *Sheet) Move(src Rect, to Addr) (Rect, error) {
167169 }168170 rng := func(r Rect) (Rect, bool) {169171 if src.Contains(r.From) && src.Contains(r.To) {170- return Rect{shift(r.From), shift(r.To)}, true172+ return Rect{From: shift(r.From), To: shift(r.To)}, true171173 }172174 return r, true173175 }@@ -190,12 +192,12 @@ func (s *Sheet) MoveTo(dst *Sheet, src Rect, to Addr) (Rect, error) {
190192 return s.Move(src, to)191193 }192194 dc, dr := to.Col-src.From.Col, to.Row-src.From.Row193- d := Rect{to, Addr{Col: src.To.Col + dc, Row: src.To.Row + dr}}195+ d := Rect{From: to, To: Addr{Col: src.To.Col + dc, Row: src.To.Row + dr}}194196 if !d.To.Valid() {195197 return Rect{}, ErrPasteEdge196198 }197199 shift := func(a Addr) Addr { return Addr{Col: a.Col + dc, Row: a.Row + dr} }198- rw := func(orig, home *Sheet) refRewrite { return s.moveRewrite(dst, src, d, orig, home) }200+ rw := func(orig, home *Sheet) formula.Rewriter { return s.moveRewrite(dst, src, d, orig, home) }199201 200202 moved := map[Addr]*Cell{}201203 for _, a := range s.cellsIn(src) {@@ -221,7 +223,7 @@ func (s *Sheet) MoveTo(dst *Sheet, src Rect, to Addr) (Rect, error) {
221223 keep(l)222224 }223225 }224- label := "move " + src.String() + " to " + quoteSheet(dst.name) + "!" + d.String()226+ label := "move " + src.String() + " to " + formula.QuoteSheet(dst.name) + "!" + d.String()225227 dst.change(label, d, func() {226228 for _, a := range s.cellsIn(src) {227229 s.place(a, nil)@@ -237,7 +239,7 @@ func (s *Sheet) MoveTo(dst *Sheet, src Rect, to Addr) (Rect, error) {
237239 }238240 for k, n := range s.wb.names {239241 if n.Sheet == s && !n.Lost && src.Contains(n.Range.From) && src.Contains(n.Range.To) {240- n.Sheet, n.Range = dst, Rect{shift(n.Range.From), shift(n.Range.To)}242+ n.Sheet, n.Range = dst, Rect{From: shift(n.Range.From), To: shift(n.Range.To)}241243 s.wb.putName(k, &n)242244 }243245 }@@ -248,7 +250,7 @@ func (s *Sheet) MoveTo(dst *Sheet, src Rect, to Addr) (Rect, error) {
248250 // moveRewrite maps the references of a formula that was on orig and is249251 // on home after cells in src on s move to d on dst. A reference names its250252 // sheet unless it points at home.251-func (s *Sheet) moveRewrite(dst *Sheet, src, d Rect, orig, home *Sheet) refRewrite {253+func (s *Sheet) moveRewrite(dst *Sheet, src, d Rect, orig, home *Sheet) formula.Rewriter {252254 dc, dr := d.From.Col-src.From.Col, d.From.Row-src.From.Row253255 shift := func(a Addr) Addr { return Addr{Col: a.Col + dc, Row: a.Row + dr} }254256 // written is how a reference names now, having named was as written.@@ -261,28 +263,28 @@ func (s *Sheet) moveRewrite(dst *Sheet, src, d Rect, orig, home *Sheet) refRewri
261263 }262264 return now.name263265 }264- return refRewrite{265- ref: func(n refNode) Node {266- t := s.wb.resolve(orig, n.sheet)266+ return formula.Rewriter{267+ Ref: func(n formula.Ref) Node {268+ t := s.wb.resolve(orig, n.Sheet)267269 switch {268270 case t == nil:269271 return n270- case t == s && src.Contains(n.a):271- return refNode{shift(n.a), n.abs, written(n.sheet, t, dst)}272- case t == dst && d.Contains(n.a):273- return refErrNode{}272+ case t == s && src.Contains(n.Addr):273+ return formula.Ref{Addr: shift(n.Addr), Abs: n.Abs, Sheet: written(n.Sheet, t, dst)}274+ case t == dst && d.Contains(n.Addr):275+ return formula.RefErr{}274276 }275- return refNode{n.a, n.abs, written(n.sheet, t, t)}277+ return formula.Ref{Addr: n.Addr, Abs: n.Abs, Sheet: written(n.Sheet, t, t)}276278 },277- rng: func(n rangeNode) Node {278- t := s.wb.resolve(orig, n.sheet)279+ Range: func(n formula.Range) Node {280+ t := s.wb.resolve(orig, n.Sheet)279281 switch {280282 case t == nil:281283 return n282- case t == s && src.Contains(n.r.From) && src.Contains(n.r.To):283- return rangeNode{Rect{shift(n.r.From), shift(n.r.To)}, n.abs, written(n.sheet, t, dst)}284+ case t == s && src.Contains(n.Rect.From) && src.Contains(n.Rect.To):285+ return formula.Range{Rect: Rect{From: shift(n.Rect.From), To: shift(n.Rect.To)}, Abs: n.Abs, Sheet: written(n.Sheet, t, dst)}284286 }285- return rangeNode{n.r, n.abs, written(n.sheet, t, t)}287+ return formula.Range{Rect: n.Rect, Abs: n.Abs, Sheet: written(n.Sheet, t, t)}286288 },287289 }288290 }@@ -367,7 +369,7 @@ func (s *Sheet) pasteCell(a Addr, c *Cell, dc, dr int, values bool) {
367369 case values:368370 s.put(a, valueInput(c.Value))369371 default:370- s.place(a, c.rewritten(shiftRefs(dc, dr)).clone())372+ s.place(a, c.rewritten(formula.Shift(dc, dr)).clone())371373 }372374 }373375 @@ -394,7 +396,7 @@ func (s *Sheet) FillDown(r Rect) (Rect, error) {
394396 if src, ok := s.seriesStart(r, true); ok {395397 return s.FillSeries(src, r)396398 }397- src := Rect{r.From, Addr{Col: r.To.Col, Row: r.From.Row}}399+ src := Rect{From: r.From, To: Addr{Col: r.To.Col, Row: r.From.Row}}398400 if r.From.Row == r.To.Row {399401 if r.From.Row == 0 {400402 return r, nil@@ -415,7 +417,7 @@ func (s *Sheet) FillRight(r Rect) (Rect, error) {
415417 if src, ok := s.seriesStart(r, false); ok {416418 return s.FillSeries(src, r)417419 }418- src := Rect{r.From, Addr{Col: r.From.Col, Row: r.To.Row}}420+ src := Rect{From: r.From, To: Addr{Col: r.From.Col, Row: r.To.Row}}419421 if r.From.Col == r.To.Col {420422 if r.From.Col == 0 {421423 return r, nil@@ -1,466 +0,0 @@
1-package sheet2-3-import (4- "fmt"5- "strconv"6- "strings"7-)8-9-// Node is a parsed formula expression.10-type Node any11-12-type (13- numLit struct{ v float64 }14- strLit struct{ v string }15- boolLit struct{ v bool }16- // refNode is a cell reference. sheet is the sheet name as written17- // before the "!" (Sheet2!A1), or "" for the formula's own sheet.18- refNode struct {19- a Addr20- abs absFlags21- sheet string22- }23- // rangeNode is kept normalized (r.From is the top-left corner); abs24- // holds the absolute markers of r.From and r.To.25- rangeNode struct {26- r Rect27- abs [2]absFlags28- sheet string29- }30- refErrNode struct{} // a reference to deleted cells: #REF!31- nameNode struct{ name string } // a named range as spelled in the formula; #NAME? if undefined32- emptyArg struct{} // an omitted argument, as in XLOOKUP(a, b, c, , 1)33- unaryNode struct {34- op string35- x Node36- dec bool // computed in decimal (decimal.go)37- }38- binaryNode struct {39- op string40- l, r Node41- dec bool // computed in decimal (decimal.go)42- }43- callNode struct {44- fn *FuncDef45- args []Node46- }47-)48-49-type tokKind int50-51-const (52- tokEOF tokKind = iota53- tokNum54- tokStr55- tokIdent // cell reference, TRUE/FALSE or a name56- tokFunc // identifier directly followed by "("57- tokOp // operators and punctuation58- tokRefErr // #REF!, left behind when a referenced cell is deleted59- tokSheet // a sheet name and its "!", as in Sheet2!A1 or 'My Sheet'!A1; text is the name60-)61-62-type token struct {63- kind tokKind64- text string65- pos int66-}67-68-// ParseError describes a formula that could not be parsed. Pos is the byte69-// offset in the entry where the problem was found, used to place the edit70-// cursor.71-type ParseError struct {72- Pos int73- Msg string74-}75-76-func (e *ParseError) Error() string { return e.Msg }77-78-func isIdentStart(c byte) bool { return isLetter(c) || c == '_' || c == '$' }79-80-func isIdentPart(c byte) bool { return isIdentStart(c) || isDigit(c) || c == '.' }81-82-func lex(src string) ([]token, error) {83- var toks []token84- i := 085- for i < len(src) {86- c := src[i]87- start := i88- switch {89- case c == ' ':90- i++91- continue92- case isDigit(c) || (c == '.' && i+1 < len(src) && isDigit(src[i+1])):93- for i < len(src) && (isDigit(src[i]) || src[i] == '.' && !strings.HasPrefix(src[i:], "..")) {94- i++95- }96- if i < len(src) && (src[i] == 'e' || src[i] == 'E') {97- j := i + 198- if j < len(src) && (src[j] == '+' || src[j] == '-') {99- j++100- }101- if j < len(src) && isDigit(src[j]) {102- i = j103- for i < len(src) && isDigit(src[i]) {104- i++105- }106- }107- }108- toks = append(toks, token{tokNum, src[start:i], start})109- case c == '"':110- // Strings use "" to escape a quote, as in Sheets.111- var sb strings.Builder112- i++113- for {114- if i >= len(src) {115- return nil, &ParseError{start, "Missing closing quote"}116- }117- if src[i] == '"' {118- if i+1 < len(src) && src[i+1] == '"' {119- sb.WriteByte('"')120- i += 2121- continue122- }123- i++124- break125- }126- sb.WriteByte(src[i])127- i++128- }129- toks = append(toks, token{tokStr, sb.String(), start})130- case c == '\'':131- // A quoted sheet name, with '' for a quote: 'Q3 ''26'!A1.132- name, end, ok := quotedName(src, i)133- if !ok || end >= len(src) || src[end] != '!' {134- return nil, &ParseError{start, "Expected ! after a quoted sheet name"}135- }136- i = end + 1137- toks = append(toks, token{tokSheet, name, start})138- case c == '@' || isIdentStart(c):139- if c == '@' { // 1-2-3 style @SUM140- i++141- }142- for i < len(src) && isIdentPart(src[i]) && !strings.HasPrefix(src[i:], "..") {143- i++144- }145- if c != '@' && i < len(src) && src[i] == '!' {146- toks = append(toks, token{tokSheet, src[start:i], start})147- i++148- continue149- }150- name := strings.ToUpper(strings.TrimPrefix(src[start:i], "@"))151- if name == "" {152- return nil, &ParseError{start, "Missing function name after @"}153- }154- j := i155- for j < len(src) && src[j] == ' ' {156- j++157- }158- kind := tokIdent159- if j < len(src) && src[j] == '(' {160- kind = tokFunc161- } else if c == '@' {162- kind = tokFunc // @PI without parentheses163- }164- toks = append(toks, token{kind, name, start})165- case c == '#' && strings.HasPrefix(strings.ToUpper(src[i:]), "#REF!"):166- i += len("#REF!")167- toks = append(toks, token{tokRefErr, "#REF!", start})168- case c == '#':169- end := strings.IndexByte(src[i+1:], '#')170- if end < 0 {171- return nil, &ParseError{start, "Unexpected #"}172- }173- op := strings.ToUpper(src[i : i+end+2])174- if op != "#AND#" && op != "#OR#" && op != "#NOT#" {175- return nil, &ParseError{start, "Unknown operator " + op}176- }177- i += end + 2178- toks = append(toks, token{tokOp, op, start})179- case strings.HasPrefix(src[i:], ".."), strings.HasPrefix(src[i:], "<="),180- strings.HasPrefix(src[i:], ">="), strings.HasPrefix(src[i:], "<>"):181- i += 2182- toks = append(toks, token{tokOp, src[start:i], start})183- case strings.IndexByte("+-*/^=<>&(),;:%", c) >= 0:184- i++185- toks = append(toks, token{tokOp, string(c), start})186- default:187- return nil, &ParseError{start, fmt.Sprintf("Unexpected %q", c)}188- }189- }190- return append(toks, token{tokEOF, "", len(src)}), nil191-}192-193-// Binding powers, lowest to highest, following Sheets' precedence.194-var infixPower = map[string]int{195- "#OR#": 10, "#AND#": 10,196- "=": 30, "<>": 30, "<": 30, ">": 30, "<=": 30, ">=": 30,197- "&": 35,198- "+": 40, "-": 40,199- "*": 50, "/": 50,200- "^": 70,201-}202-203-// Negation binds tighter than ^, so -2^2 is 4 as in Sheets and Excel.204-const (205- notPower = 20206- unaryPower = 75207- percentPower = 80208-)209-210-type parser struct {211- src string // the formula without its "=", for names' original spelling212- toks []token213- pos int214-}215-216-// Parse parses a formula. src may start with "=", as typed in a cell;217-// error positions are relative to src.218-func Parse(src string) (Node, error) {219- body := strings.TrimPrefix(src, "=")220- offset := len(src) - len(body)221- toks, err := lex(body)222- if err != nil {223- return nil, shift(err, offset)224- }225- p := &parser{src: body, toks: toks}226- n, err := p.expr(0)227- if err == nil && p.peek().kind != tokEOF {228- err = &ParseError{p.peek().pos, "Unexpected " + p.peek().text}229- }230- if err != nil {231- return nil, shift(err, offset)232- }233- return n, nil234-}235-236-func shift(err error, offset int) error {237- if pe, ok := err.(*ParseError); ok {238- pe.Pos += offset239- }240- return err241-}242-243-func (p *parser) peek() token { return p.toks[p.pos] }244-245-func (p *parser) next() token {246- t := p.toks[p.pos]247- if t.kind != tokEOF {248- p.pos++249- }250- return t251-}252-253-func (p *parser) isOp(op string) bool {254- t := p.peek()255- return t.kind == tokOp && t.text == op256-}257-258-func (p *parser) expr(minPower int) (Node, error) {259- left, err := p.prefix()260- if err != nil {261- return nil, err262- }263- for {264- if p.isOp("%") && percentPower > minPower {265- p.next()266- left = unaryNode{op: "%", x: left}267- continue268- }269- t := p.peek()270- power, ok := infixPower[t.text]271- if t.kind != tokOp || !ok || power <= minPower {272- return left, nil273- }274- p.next()275- right, err := p.expr(power)276- if err != nil {277- return nil, err278- }279- left = binaryNode{op: t.text, l: left, r: right}280- }281-}282-283-func (p *parser) prefix() (Node, error) {284- t := p.next()285- switch t.kind {286- case tokNum:287- v, err := strconv.ParseFloat(t.text, 64)288- if err != nil {289- return nil, &ParseError{t.pos, "Invalid number " + t.text}290- }291- return numLit{v}, nil292- case tokStr:293- return strLit{t.text}, nil294- case tokIdent:295- return p.ident(t, "")296- case tokSheet:297- ref := p.next()298- if ref.kind != tokIdent {299- return nil, &ParseError{ref.pos, "Expected a cell after " + quoteSheet(t.text) + "!"}300- }301- return p.ident(ref, t.text)302- case tokFunc:303- return p.call(t)304- case tokRefErr:305- return refErrNode{}, nil306- case tokOp:307- switch t.text {308- case "(":309- n, err := p.expr(0)310- if err != nil {311- return nil, err312- }313- if !p.isOp(")") {314- return nil, &ParseError{p.peek().pos, "Missing )"}315- }316- p.next()317- return n, nil318- case "+", "-":319- x, err := p.expr(unaryPower)320- if err != nil {321- return nil, err322- }323- return unaryNode{op: t.text, x: x}, nil324- case "#NOT#":325- x, err := p.expr(notPower)326- if err != nil {327- return nil, err328- }329- return unaryNode{op: t.text, x: x}, nil330- }331- case tokEOF:332- return nil, &ParseError{t.pos, "Formula is incomplete"}333- }334- return nil, &ParseError{t.pos, "Unexpected " + t.text}335-}336-337-// ident parses a reference, a range, a boolean or a name. sheet is the338-// sheet the reference was qualified with, if any.339-func (p *parser) ident(t token, sheet string) (Node, error) {340- a, abs, isRef := parseRef(t.text)341- if !isRef {342- if sheet != "" {343- return nil, &ParseError{t.pos, "Expected a cell after " + quoteSheet(sheet) + "!"}344- }345- switch t.text {346- case "TRUE":347- return boolLit{true}, nil348- case "FALSE":349- return boolLit{false}, nil350- }351- // Names match case-insensitively but print as written.352- return nameNode{p.src[t.pos : t.pos+len(t.text)]}, nil353- }354- if p.isOp(":") || p.isOp("..") {355- sep := p.next()356- end := p.next()357- // The second corner may repeat the sheet: Sheet2!A1:Sheet2!B3.358- if end.kind == tokSheet {359- if sheetKey(end.text) != sheetKey(sheet) {360- return nil, &ParseError{end.pos, "A range can't span sheets"}361- }362- end = p.next()363- }364- b, bAbs, ok := parseRef(end.text)365- if end.kind != tokIdent || !ok {366- return nil, &ParseError{end.pos, "Expected a cell after " + sep.text}367- }368- r := newRange(a, b, abs, bAbs)369- r.sheet = sheet370- return r, nil371- }372- return refNode{a, abs, sheet}, nil373-}374-375-// newRange builds a normalized range from two corners as written. Each376-// absolute marker stays with its column or row, so $B1:A$2 becomes A1:$B$2.377-func newRange(a, b Addr, aAbs, bAbs absFlags) rangeNode {378- swap := func(bit absFlags) {379- aAbs, bAbs = aAbs&^bit|bAbs&bit, bAbs&^bit|aAbs&bit380- }381- if a.Col > b.Col {382- a.Col, b.Col = b.Col, a.Col383- swap(absCol)384- }385- if a.Row > b.Row {386- a.Row, b.Row = b.Row, a.Row387- swap(absRow)388- }389- return rangeNode{Rect{a, b}, [2]absFlags{aAbs, bAbs}, ""}390-}391-392-func (p *parser) call(t token) (Node, error) {393- fn, ok := LookupFunc(t.text)394- if !ok {395- return nil, &ParseError{t.pos, "Unknown function " + t.text}396- }397- n := callNode{fn: fn}398- if p.isOp("(") {399- p.next()400- if p.isOp(")") {401- p.next()402- } else {403- for {404- // An argument may be left out: F(a, , c) or F(a, ).405- if p.isOp(",") || p.isOp(";") || p.isOp(")") {406- n.args = append(n.args, emptyArg{})407- } else {408- arg, err := p.expr(0)409- if err != nil {410- return nil, err411- }412- n.args = append(n.args, arg)413- }414- sep := p.next()415- if sep.kind == tokOp && sep.text == ")" {416- break417- }418- if sep.kind != tokOp || (sep.text != "," && sep.text != ";") {419- return nil, &ParseError{sep.pos, "Expected , or ) in " + fn.Name}420- }421- }422- }423- }424- if len(n.args) < fn.Min || (fn.Max >= 0 && len(n.args) > fn.Max) ||425- (fn.step > 0 && (len(n.args)-fn.Min)%fn.step != 0) {426- return nil, &ParseError{t.pos, fmt.Sprintf("Wrong number of arguments to %s(%s)", fn.Name, fn.Args)}427- }428- return n, nil429-}430-431-// walkNames calls fn for every name in n.432-func walkNames(n Node, fn func(nameNode)) {433- switch n := n.(type) {434- case nameNode:435- fn(n)436- case unaryNode:437- walkNames(n.x, fn)438- case binaryNode:439- walkNames(n.l, fn)440- walkNames(n.r, fn)441- case callNode:442- for _, a := range n.args {443- walkNames(a, fn)444- }445- }446-}447-448-// walkRefs calls fn for every single-cell reference and range in n, with449-// the sheet it was qualified with ("" for the formula's own sheet).450-func walkRefs(n Node, ref func(string, Addr), rng func(string, Rect)) {451- switch n := n.(type) {452- case refNode:453- ref(n.sheet, n.a)454- case rangeNode:455- rng(n.sheet, n.r)456- case unaryNode:457- walkRefs(n.x, ref, rng)458- case binaryNode:459- walkRefs(n.l, ref, rng)460- walkRefs(n.r, ref, rng)461- case callNode:462- for _, a := range n.args {463- walkRefs(a, ref, rng)464- }465- }466-}@@ -1,219 +1,28 @@
11 package sheet22 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!.3+import "github.com/FelineStateMachine/012/internal/formula"74 8-// refRewrite maps the references in a formula. Any function may return9-// refErrNode; a nil function leaves those nodes alone.10-type refRewrite struct {11- ref func(refNode) Node12- rng func(rangeNode) Node13- name func(nameNode) Node14-}15-16-// rewrite returns n with its references mapped, and whether anything17-// changed. Unchanged subtrees are shared.18-func rewrite(n Node, rw refRewrite) (Node, bool) {19- switch n := n.(type) {20- case refNode:21- if rw.ref == nil {22- return n, false23- }24- out := rw.ref(n)25- return out, out != Node(n)26- case rangeNode:27- if rw.rng == nil {28- return n, false29- }30- out := rw.rng(n)31- return out, out != Node(n)32- case nameNode:33- if rw.name == nil {34- return n, false35- }36- out := rw.name(n)37- return out, out != Node(n)38- case unaryNode:39- x, ok := rewrite(n.x, rw)40- if ok {41- n.x = x42- }43- return n, ok44- case binaryNode:45- l, okL := rewrite(n.l, rw)46- r, okR := rewrite(n.r, rw)47- if okL || okR {48- n.l, n.r = l, r49- }50- return n, okL || okR51- case callNode:52- var args []Node53- for i, a := range n.args {54- if b, ok := rewrite(a, rw); ok {55- if args == nil {56- args = append([]Node(nil), n.args...)57- }58- args[i] = b59- }60- }61- if args == nil {62- return n, false63- }64- n.args = args65- return n, true66- }67- return n, false68-}69-70-// shiftRefs is the rewrite for copying a formula by (dc, dr): relative71-// parts move, $absolute parts stay, and references pushed off the sheet72-// become #REF!.73-func shiftRefs(dc, dr int) refRewrite {74- move := func(a Addr, abs absFlags) (Addr, bool) {75- if abs&absCol == 0 {76- a.Col += dc77- }78- if abs&absRow == 0 {79- a.Row += dr80- }81- return a, a.Valid()82- }83- return refRewrite{84- ref: func(n refNode) Node {85- a, ok := move(n.a, n.abs)86- if !ok {87- return refErrNode{}88- }89- return refNode{a, n.abs, n.sheet}90- },91- rng: func(n rangeNode) Node {92- from, ok1 := move(n.r.From, n.abs[0])93- to, ok2 := move(n.r.To, n.abs[1])94- if !ok1 || !ok2 {95- return refErrNode{}96- }97- r := newRange(from, to, n.abs[0], n.abs[1])98- r.sheet = n.sheet99- return r100- },101- }102-}103-104-// relocate is the rewrite for cells that move on one sheet: cell maps105-// where each cell went (false if it's gone), and rng maps whole ranges.106-// on reports whether a reference written with a sheet name ("" for none)107-// points at the sheet whose cells moved; other references stay.108-func relocate(on func(sheet string) bool, cell func(Addr) (Addr, bool), rng func(Rect) (Rect, bool)) refRewrite {109- return refRewrite{110- ref: func(n refNode) Node {111- if !on(n.sheet) {112- return n113- }114- a, ok := cell(n.a)115- if !ok {116- return refErrNode{}117- }118- return refNode{a, n.abs, n.sheet}119- },120- rng: func(n rangeNode) Node {121- if !on(n.sheet) {122- return n123- }124- r, ok := rng(n.r)125- if !ok {126- return refErrNode{}127- }128- return rangeNode{r, n.abs, n.sheet}129- },130- }131-}5+// References in a stored formula are rewritten (see formula.Rewrite) by6+// building a new cell: stored cells are never changed in place.1327 1338 // withFormula returns a copy of c holding expression n, printed as its1349 // new input. Other fields (formats, say) come along unchanged.13510 func (c *Cell) withFormula(n Node) *Cell {13611 cp := c.clone()137- cp.Input = formulaText(n)12+ cp.Input = formula.Text(n)13813 cp.setExpr(n)13914 return cp14015 }14116 14217 // rewritten returns c with its references rewritten, or c itself if none14318 // changed.144-func (c *Cell) rewritten(rw refRewrite) *Cell {19+func (c *Cell) rewritten(rw formula.Rewriter) *Cell {14520 if c.expr == nil || (len(c.refs) == 0 && len(c.ranges) == 0 && len(c.names) == 0 && len(c.xrefs) == 0) {14621 return c14722 }148- n, changed := rewrite(c.expr, rw)23+ n, changed := formula.Rewrite(c.expr, rw)14924 if !changed {15025 return c15126 }15227 return c.withFormula(n)15328 }154-155-// span inserts (n > 0) or deletes (n < 0) lines starting at index at,156-// along an axis with size lines.157-type span struct{ at, n, size int }158-159-// point maps a line index; false if the line was deleted or pushed off160-// the end.161-func (sp span) point(v int) (int, bool) {162- switch {163- case v < sp.at:164- return v, true165- case sp.n < 0 && v < sp.at-sp.n:166- return 0, false167- }168- v += sp.n169- return v, v < sp.size170-}171-172-// interval maps a range's lines lo..hi as Sheets does: inserting inside173-// the range grows it, deleting part of it shrinks it, and deleting all of174-// it leaves #REF!.175-func (sp span) interval(lo, hi int) (int, int, bool) {176- if sp.n > 0 {177- lo, okLo := sp.point(lo)178- if !okLo {179- return 0, 0, false180- }181- hi, _ := sp.point(hi)182- return lo, min(hi, sp.size-1), true183- }184- end := sp.at - sp.n // first line after the deletion185- if lo >= sp.at && hi < end {186- return 0, 0, false187- }188- if lo >= sp.at {189- lo = max(lo, end) + sp.n190- }191- if hi >= sp.at {192- hi = max(hi+sp.n, sp.at-1)193- }194- return lo, hi, true195-}196-197-// axisRewrite is the cell and range mapping for inserting or deleting rows198-// (rows true) or columns.199-func axisRewrite(rows bool, sp span) (func(Addr) (Addr, bool), func(Rect) (Rect, bool)) {200- cell := func(a Addr) (Addr, bool) {201- var ok bool202- if rows {203- a.Row, ok = sp.point(a.Row)204- } else {205- a.Col, ok = sp.point(a.Col)206- }207- return a, ok208- }209- rng := func(r Rect) (Rect, bool) {210- var ok bool211- if rows {212- r.From.Row, r.To.Row, ok = sp.interval(r.From.Row, r.To.Row)213- } else {214- r.From.Col, r.To.Col, ok = sp.interval(r.From.Col, r.To.Col)215- }216- return r, ok217- }218- return cell, rng219-}@@ -2,127 +2,10 @@ package sheet
22 33 import (44 "maps"5- "reflect"65 "strings"76 "testing"87 )98 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{op: "+", l: refErrNode{}, r: 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"}, // negation binds tighter than ^46- {"=-(2^2)", "=-(2^2)"},47- {"=-(A1+1)", "=-(A1+1)"},48- {"=50%", "=50%"},49- {"=(A1+1)%", "=(A1+1)%"},50- {"=#NOT#(A1=1) #AND# B1", "=#NOT#A1=1#AND#B1"},51- {"=1+(#NOT#A1)", "=1+(#NOT#A1)"},52- {`="say ""hi"""&A1`, `="say ""hi"""&A1`},53- {"=1.5E3+1e-9+2E21", "=1500+1E-09+2E+21"},54- {"=@PI", "=PI()"},55- {"=IF(A1>=2,TRUE,profit)", "=IF(A1>=2,TRUE,profit)"},56- {"=SUM(#REF!)", "=SUM(#REF!)"},57- {"+A1", "=+A1"},58- }59- for _, tt := range tests {60- n, err := Parse(tt.in)61- if err != nil {62- t.Fatalf("Parse(%q): %v", tt.in, err)63- }64- if got := formulaText(n); got != tt.want {65- t.Errorf("print(%q) = %q, want %q", tt.in, got, tt.want)66- }67- }68-}69-70-func TestShiftRefs(t *testing.T) {71- tests := []struct {72- in string73- dc, dr int74- want string75- }{76- {"=A1+B2", 1, 2, "=B3+C4"},77- {"=$A$1+A1", 1, 1, "=$A$1+B2"},78- {"=$A1+A$1", 2, 3, "=$A4+C$1"},79- {"=SUM(A1:B2)", 0, 5, "=SUM(A6:B7)"},80- {"=SUM($A$1:B2)", 1, 1, "=SUM($A$1:C3)"},81- {"=A2", 0, -1, "=A1"},82- {"=A2", 0, -2, "=#REF!"},83- {"=SUM(A1:B2)+1", -1, 0, "=SUM(#REF!)+1"},84- {"=$A2", -1, 0, "=$A2"},85- {"=IV1", 1, 0, "=#REF!"},86- // A mixed range can flip when one corner is anchored.87- {"=SUM($C1:D1)", -2, 0, "=SUM(B1:$C1)"},88- }89- for _, tt := range tests {90- n, _ := Parse(tt.in)91- out, _ := rewrite(n, shiftRefs(tt.dc, tt.dr))92- if got := formulaText(out); got != tt.want {93- t.Errorf("shift(%q, %d, %d) = %q, want %q", tt.in, tt.dc, tt.dr, got, tt.want)94- }95- }96-}97-98-func TestSpanInterval(t *testing.T) {99- tests := []struct {100- sp span101- lo, hi int102- wLo, wHi int103- wOK bool104- name string105- checkPoint bool106- }{107- {span{2, 2, 100}, 0, 1, 0, 1, true, "insert below", false},108- {span{2, 2, 100}, 1, 3, 1, 5, true, "insert inside grows", false},109- {span{2, 2, 100}, 2, 3, 4, 5, true, "insert at start shifts", false},110- {span{2, 2, 100}, 50, 99, 52, 99, true, "insert clamps at the edge", false},111- {span{2, -2, 100}, 1, 4, 1, 2, true, "delete inside shrinks", false},112- {span{2, -2, 100}, 2, 4, 2, 2, true, "delete top part", false},113- {span{2, -2, 100}, 1, 2, 1, 1, true, "delete bottom part", false},114- {span{2, -2, 100}, 3, 6, 2, 4, true, "delete overlapping start", false},115- {span{2, -2, 100}, 5, 6, 3, 4, true, "delete above shifts", false},116- {span{2, -2, 100}, 2, 3, 0, 0, false, "delete all of it", false},117- }118- for _, tt := range tests {119- lo, hi, ok := tt.sp.interval(tt.lo, tt.hi)120- if ok != tt.wOK || (ok && (lo != tt.wLo || hi != tt.wHi)) {121- t.Errorf("%s: interval(%d, %d) = %d, %d, %v", tt.name, tt.lo, tt.hi, lo, hi, ok)122- }123- }124-}125-1269 func TestInsertDeleteRows(t *testing.T) {12710 base := map[string]string{12811 "A1": "1", "A2": "2", "A3": "3", "A4": "4",@@ -338,28 +221,3 @@ func TestFill(t *testing.T) {
338221 t.Errorf("fill entry is one undo step, D1 = %q", got)339222 }340223 }341-342-// FuzzPrint checks that printing a parsed formula gives text that parses343-// back to the same expression.344-func FuzzPrint(f *testing.F) {345- for _, s := range []string{346- "=A1*2", "=$A$1+A$2-$B3", "=SUM($A1:B$3)", "=-2^-2%", "=1-(2-3)", "=#NOT#A1=1#OR#B1",347- "=2^(3^2)", `=IF(A1>2,"x""y",#REF!)`, "=1.5E-3^2", "=@SUM(A1..B3)", "=(1+2)%*-A1",348- } {349- f.Add(s)350- }351- f.Fuzz(func(t *testing.T, in string) {352- n, err := Parse(in)353- if err != nil {354- return355- }356- text := formulaText(n)357- back, err := Parse(text)358- if err != nil {359- t.Fatalf("%q printed as %q, which doesn't parse: %v", in, text, err)360- }361- if !reflect.DeepEqual(n, back) {362- t.Fatalf("%q printed as %q parses differently:\n%#v\n%#v", in, text, n, back)363- }364- })365-}@@ -1,5 +1,7 @@
11 package sheet22 3+import "github.com/FelineStateMachine/012/internal/formula"4+35 // Some functions (JEV.*) are answered by a hosted model. The engine never46 // talks to the network: it describes each question as a RemoteCall and57 // asks Remote for the answer. Until the answer arrives the cell shows@@ -65,18 +67,18 @@ func (s *Sheet) RemoteCalls(a Addr) []RemoteCall {
6567 var walk func(Node)6668 walk = func(n Node) {6769 switch n := n.(type) {68- case unaryNode:69- walk(n.x)70- case binaryNode:71- walk(n.l)72- walk(n.r)73- case callNode:74- if n.fn.remote != nil {75- if call, err := n.fn.remote(n.args, get); err == nil {70+ case formula.Unary:71+ walk(n.X)72+ case formula.Binary:73+ walk(n.L)74+ walk(n.R)75+ case formula.Call:76+ if funcOf(n).remote != nil {77+ if call, err := funcOf(n).remote(n.Args, get); err == nil {7678 calls = append(calls, call)7779 }7880 }79- for _, arg := range n.args {81+ for _, arg := range n.Args {8082 walk(arg)8183 }8284 }@@ -6,6 +6,7 @@ import (
66 "strconv"77 "strings"88 9+ "github.com/FelineStateMachine/012/internal/formula"910 "github.com/FelineStateMachine/012/internal/numfmt"1011 )1112 @@ -120,7 +121,7 @@ func detectSeries(cells []*Cell) func(i int) string {
120121 func numberSeries(cells []*Cell) func(int) string {121122 vals := make([]float64, len(cells))122123 for i, c := range cells {123- if _, ok := c.expr.(numLit); !ok || c.Value.Kind != Number {124+ if _, ok := c.expr.(formula.Num); !ok || c.Value.Kind != Number {124125 return nil125126 }126127 vals[i] = c.Value.Num@@ -63,7 +63,7 @@ func TestFillSeries(t *testing.T) {
6363 func TestFillSeriesDirections(t *testing.T) {6464 s := sheetOf(t, map[string]string{"C3": "5", "C4": "6", "D3": "Mon", "D4": "Tue"})6565 // Up continues backwards.66- s.FillSeries(Rect{at("C3"), at("D4")}, Rect{at("C1"), at("D4")})66+ s.FillSeries(Rect{From: at("C3"), To: at("D4")}, Rect{From: at("C1"), To: at("D4")})6767 if got := column(s, "C", 1, 2); !reflect.DeepEqual(got, []string{"3", "4"}) {6868 t.Errorf("up: C = %q", got)6969 }@@ -72,8 +72,8 @@ func TestFillSeriesDirections(t *testing.T) {
7272 }7373 // Right and left, per row.7474 s = sheetOf(t, map[string]string{"B1": "1", "C1": "3", "B2": "x"})75- s.FillSeries(Rect{at("B1"), at("C2")}, Rect{at("A1"), at("C2")})76- s.FillSeries(Rect{at("A1"), at("C2")}, Rect{at("A1"), at("E2")})75+ s.FillSeries(Rect{From: at("B1"), To: at("C2")}, Rect{From: at("A1"), To: at("C2")})76+ s.FillSeries(Rect{From: at("A1"), To: at("C2")}, Rect{From: at("A1"), To: at("E2")})7777 want := map[string]string{"A1": "-1", "B1": "1", "C1": "3", "D1": "5", "E1": "7", "A2": "", "B2": "x", "D2": "", "E2": "x"}7878 for a, in := range want {7979 if got := input(s, a); got != in {@@ -113,7 +113,7 @@ func TestFillDownContinuesSeries(t *testing.T) {
113113 })114114 }115115 s := sheetOf(t, map[string]string{"A1": "Jan", "B1": "Feb"})116- s.FillRight(Rect{at("A1"), at("D1")})116+ s.FillRight(Rect{From: at("A1"), To: at("D1")})117117 if got := input(s, "D1"); got != "Apr" {118118 t.Errorf("fill right: D1 = %q", got)119119 }@@ -7,6 +7,8 @@ import (
77 "sort"88 "strings"99 "unicode"10+11+ "github.com/FelineStateMachine/012/internal/formula"1012 )1113 1214 // DefaultWidth is the initial column width: nine characters plus padding.@@ -182,13 +184,13 @@ func classify(input string) (Node, Format, error) {
182184 return nil, Format{}, nil183185 }184186 if v, f, ok := ParseValue(input); ok {185- return numLit{v}, f, nil187+ return formula.Num{V: v}, f, nil186188 }187189 switch strings.ToUpper(input) {188190 case "TRUE":189- return boolLit{true}, Format{}, nil191+ return formula.Bool{V: true}, Format{}, nil190192 case "FALSE":191- return boolLit{false}, Format{}, nil193+ return formula.Bool{V: false}, Format{}, nil192194 }193195 if IsFormulaEntry(input) {194196 n, err := Parse(input)@@ -203,7 +205,7 @@ func classify(input string) (Node, Format, error) {
203205 // unchanged.204206 func (s *Sheet) Set(a Addr, input string) error {205207 var err error206- s.change("edit "+a.String(), Rect{a, a}, func() { err = s.put(a, input) })208+ s.change("edit "+a.String(), Rect{From: a, To: a}, func() { err = s.put(a, input) })207209 return err208210 }209211 @@ -267,22 +269,22 @@ func formattingOnly(f Format, st Style) *Cell {
267269 func (c *Cell) setExpr(n Node) {268270 c.expr, c.refs, c.ranges, c.xrefs, c.names, c.volatile = n, nil, nil, nil, nil, false269271 if n != nil {270- walkRefs(n,272+ formula.WalkRefs(n,271273 func(sheet string, r Addr) {272274 if sheet != "" {273- c.xrefs = append(c.xrefs, xref{sheetKey(sheet), Rect{r, r}})275+ c.xrefs = append(c.xrefs, xref{formula.SheetKey(sheet), Rect{From: r, To: r}})274276 return275277 }276278 c.refs = append(c.refs, r)277279 },278280 func(sheet string, r Rect) {279281 if sheet != "" {280- c.xrefs = append(c.xrefs, xref{sheetKey(sheet), r})282+ c.xrefs = append(c.xrefs, xref{formula.SheetKey(sheet), r})281283 return282284 }283285 c.ranges = append(c.ranges, r)284286 })285- walkNames(n, func(nn nameNode) { c.names = append(c.names, nameKey(nn.name)) })287+ formula.WalkNames(n, func(nn formula.Name) { c.names = append(c.names, nameKey(nn.Name)) })286288 c.volatile = isVolatile(n)287289 }288290 }@@ -461,7 +463,7 @@ func (w *Workbook) affected(changed []loc) int {
461463 }462464 }463465 }464- if w.crossKeys[sheetKey(s.name)] == 0 {466+ if w.crossKeys[formula.SheetKey(s.name)] == 0 {465467 continue // no formula names this sheet466468 }467469 for u := range w.crossUsers {@@ -505,9 +507,9 @@ func (w *Workbook) evaluate() {
505507 case c.expr == nil:506508 c.Value = Value{Kind: Text, Str: strings.TrimPrefix(c.Input, "'")}507509 default:508- expr := w.arith(s.bound(c))509- c.Value = eval(expr, s.calcGet)510- if _, lit := expr.(numLit); !lit {510+ expr := s.bound(c)511+ c.Value = eval(w.arith(expr), s.calcGet)512+ if _, lit := expr.(formula.Num); !lit {511513 c.auto = inferFormat(expr, s.calcFmt)512514 }513515 }@@ -557,7 +559,7 @@ func (w *Workbook) lookupOn(s *Sheet, get func(*Sheet, Addr) Value) lookup {
557559 return get(s, a)558560 }559561 if sheet != lastName || last == nil {560- lastName, last = sheet, w.byKey[sheetKey(sheet)]562+ lastName, last = sheet, w.byKey[formula.SheetKey(sheet)]561563 }562564 if last == nil {563565 return ErrRef@@ -16,46 +16,6 @@ func at(s string) Addr {
1616 return a1717 }1818 19-func TestAddr(t *testing.T) {20- tests := []struct {21- in string22- want Addr23- ok bool24- }{25- {"A1", Addr{0, 0}, true},26- {"z10", Addr{25, 9}, true},27- {"AA1", Addr{26, 0}, true},28- {"IV8192", Addr{255, 8191}, true},29- {"$B$2", Addr{1, 1}, true},30- {"IW1", Addr{}, false},31- {"A0", Addr{}, false},32- {"A8193", Addr{}, false},33- {"ABC1", Addr{}, false},34- {"A+1", Addr{}, false},35- {"1A", Addr{}, false},36- }37- for _, tt := range tests {38- got, ok := ParseAddr(tt.in)39- if ok != tt.ok || (ok && got != tt.want) {40- t.Errorf("ParseAddr(%q) = %v, %v; want %v, %v", tt.in, got, ok, tt.want, tt.ok)41- }42- }43- for c := range MaxCols {44- if got, ok := ParseCol(ColName(c)); !ok || got != c {45- t.Fatalf("ParseCol(ColName(%d)) = %d, %v", c, got, ok)46- }47- }48-}49-50-func TestParseRange(t *testing.T) {51- for _, in := range []string{"B3..A1", "a1:b3", "$A$1:$B$3"} {52- r, ok := ParseRange(in)53- if !ok || r.String() != "A1:B3" {54- t.Errorf("ParseRange(%q) = %v, %v", in, r, ok)55- }56- }57-}58-5919 func txt(s string) Value { return Value{Kind: Text, Str: s} }6020 6121 func TestFormulas(t *testing.T) {@@ -3,6 +3,8 @@ package sheet
33 import (44 "slices"55 "strings"6+7+ "github.com/FelineStateMachine/012/internal/formula"68 )79 810 // SortKey is one column to sort by.@@ -98,7 +100,7 @@ func (s *Sheet) SortRange(r Rect, keys []SortKey) {
98100 to := Addr{Col: col, Row: dst}99101 switch c := old[Addr{Col: col, Row: src}]; {100102 case c != nil:101- s.place(to, c.rewritten(shiftRefs(0, dst-src)))103+ s.place(to, c.rewritten(formula.Shift(0, dst-src)))102104 case s.cells[to] != nil:103105 s.place(to, nil)104106 }@@ -49,7 +49,7 @@ func TestSortRange(t *testing.T) {
4949 for _, tt := range tests {5050 t.Run(tt.name, func(t *testing.T) {5151 s := sheetOf(t, data)52- s.SortRange(Rect{at("A2"), at("B6")}, tt.keys)52+ s.SortRange(Rect{From: at("A2"), To: at("B6")}, tt.keys)5353 if got := column(s, "A", 2, 6); !reflect.DeepEqual(got, tt.a) {5454 t.Errorf("A = %q, want %q", got, tt.a)5555 }@@ -78,7 +78,7 @@ func TestSortMovesFormulasWithTheirRows(t *testing.T) {
7878 "A3": "2", "B3": "=A3*2", "C3": "=$A$1",7979 "E1": "=A1", // outside the sort: left pointing at A18080 })81- s.SortRange(Rect{at("A1"), at("C3")}, []SortKey{{Col: 0}})81+ s.SortRange(Rect{From: at("A1"), To: at("C3")}, []SortKey{{Col: 0}})8282 want := map[string]string{"A1": "1", "B1": "=A1*2", "B2": "=A2*2", "B3": "=A3*2", "C1": "=$A$1", "E1": "=A1"}8383 for a, in := range want {8484 if got := s.Cell(at(a)).Input; got != in {@@ -96,7 +96,7 @@ func TestSortMovesFormulasWithTheirRows(t *testing.T) {
9696 func TestSortClampsToData(t *testing.T) {9797 s := sheetOf(t, map[string]string{"A1": "b", "A2": "a"})9898 // A whole-column selection sorts only as far as the data goes.99- s.SortRange(Rect{at("A1"), Addr{Col: 0, Row: MaxRows - 1}}, []SortKey{{Col: 0}})99+ s.SortRange(Rect{From: at("A1"), To: Addr{Col: 0, Row: MaxRows - 1}}, []SortKey{{Col: 0}})100100 if got := column(s, "A", 1, 3); !reflect.DeepEqual(got, []string{"a", "b", ""}) {101101 t.Errorf("A = %q", got)102102 }@@ -3,6 +3,8 @@ package sheet
33 import (44 "cmp"55 "slices"6+7+ "github.com/FelineStateMachine/012/internal/formula"68 )79 810 // Tracing, as Excel's Trace Precedents and Trace Dependents: which cells@@ -32,7 +34,7 @@ func (s *Sheet) Precedents(a Addr) []Target {
3234 out = append(out, t)3335 }3436 }35- walkRefs(s.bound(c), func(sheet string, a Addr) { add(sheet, Rect{a, a}) }, add)37+ formula.WalkRefs(s.bound(c), func(sheet string, a Addr) { add(sheet, Rect{From: a, To: a}) }, add)3638 return out3739 }3840 @@ -66,7 +68,7 @@ func (s *Sheet) Dependents(a Addr) []Target {
6668 }6769 out := make([]Target, 0, len(seen))6870 for l := range seen {69- out = append(out, Target{l.s, Rect{l.a, l.a}})71+ out = append(out, Target{l.s, Rect{From: l.a, To: l.a}})7072 }7173 rank := func(t *Sheet) int {7274 if t == s {@@ -5,6 +5,8 @@ import (
55 "encoding/json"66 "fmt"77 "reflect"8+9+ "github.com/FelineStateMachine/012/internal/formula"810 )911 1012 // View state that belongs to the worksheet rather than to a cell: frozen@@ -80,14 +82,14 @@ func (v viewState) equal(w viewState) bool {
8082 // deleted rows or columns: inserting inside the frozen area freezes the8183 // new lines too, and the filter's range grows, shrinks or moves like a8284 // range reference (and goes away when all of it is deleted).83-func (s *Sheet) shiftView(rows bool, sp span) {85+func (s *Sheet) shiftView(rows bool, sp formula.Span) {8486 v := s.view8587 frozen := &v.frozenCols8688 if rows {8789 frozen = &v.frozenRows8890 }8991 if *frozen > 0 {90- if lo, hi, ok := sp.interval(0, *frozen-1); ok && lo == 0 {92+ if lo, hi, ok := sp.Interval(0, *frozen-1); ok && lo == 0 {9193 *frozen = hi + 19294 } else if !ok {9395 *frozen = 0@@ -95,13 +97,13 @@ func (s *Sheet) shiftView(rows bool, sp span) {
9597 *frozen = min(*frozen, MaxFrozen)9698 }9799 if f := v.filter; f != nil {98- _, rng := axisRewrite(rows, sp)100+ _, rng := formula.AxisMaps(rows, sp)99101 if r, ok := rng(f.Range); ok {100102 nf := &Filter{Range: r, Cols: map[int]Criteria{}}101103 for c, cr := range f.Cols {102104 if !rows {103105 var keep bool104- if c, keep = sp.point(c); !keep || c < r.From.Col || c > r.To.Col {106+ if c, keep = sp.Point(c); !keep || c < r.From.Col || c > r.To.Col {105107 continue106108 }107109 }@@ -5,6 +5,8 @@ import (
55 "fmt"66 "slices"77 "strings"8+9+ "github.com/FelineStateMachine/012/internal/formula"810 )911 1012 // A Workbook is an ordered list of sheets, as a Google Sheets@@ -97,7 +99,7 @@ func (w *Workbook) Sheet(i int) *Sheet { return w.sheets[i] }
9799 func (w *Workbook) Index(s *Sheet) int { return slices.Index(w.sheets, s) }98100 99101 // Lookup finds a sheet by name, ignoring case.100-func (w *Workbook) Lookup(name string) *Sheet { return w.byKey[sheetKey(name)] }102+func (w *Workbook) Lookup(name string) *Sheet { return w.byKey[formula.SheetKey(name)] }101103 102104 // Active returns the index of the sheet last shown, as saved in the file.103105 func (w *Workbook) Active() int { return clampInt(w.active, 0, len(w.sheets)-1) }@@ -141,79 +143,6 @@ func ValidSheetName(name string) error {
141143 return nil142144 }143145 144-// sheetKey is how sheet names compare: ignoring case, as in Sheets.145-func sheetKey(name string) string { return strings.ToUpper(name) }146-147-// quoteSheet writes a sheet name as a formula needs it: bare when it reads148-// as a plain identifier (Sheet2), otherwise in single quotes with quotes149-// doubled, e.g. 'Q3 plan'.150-func quoteSheet(name string) string {151- bare := name != "" && (isLetter(name[0]) || name[0] == '_') && !looksLikeCell(strings.ToUpper(name))152- for i := 0; bare && i < len(name); i++ {153- c := name[i]154- bare = isLetter(c) || isDigit(c) || c == '_' || c == '.'155- }156- if bare {157- return name158- }159- return "'" + strings.ReplaceAll(name, "'", "''") + "'"160-}161-162-// looksLikeCell reports whether an upper-case name reads as a cell in163-// Excel, A1 to XFD1048576 or R1C1, so a sheet of that name is quoted164-// everywhere its formulas may go. Sheet1 doesn't: SHEET is no column.165-func looksLikeCell(k string) bool {166- digits := strings.TrimLeft(k, "ABCDEFGHIJKLMNOPQRSTUVWXYZ")167- if n := len(k) - len(digits); n >= 1 && n <= 3 && digits != "" && strings.Trim(digits, "0123456789") == "" {168- return true169- }170- return (strings.HasPrefix(k, "R") || strings.HasPrefix(k, "C")) && looksLikeRef(k)171-}172-173-// QuoteSheet is quoteSheet for the UI, e.g. to show 'Q3 plan'!B2.174-func QuoteSheet(name string) string { return quoteSheet(name) }175-176-// quotedName reads a quoted sheet name starting at the quote at src[i] and177-// returns it unquoted with the index just past the closing quote.178-func quotedName(src string, i int) (string, int, bool) {179- var b strings.Builder180- for j := i + 1; j < len(src); j++ {181- if src[j] != '\'' {182- b.WriteByte(src[j])183- continue184- }185- if j+1 < len(src) && src[j+1] == '\'' {186- b.WriteByte('\'')187- j++188- continue189- }190- return b.String(), j + 1, b.Len() > 0191- }192- return "", len(src), false193-}194-195-// SplitSheet splits a reference such as "Sheet2!A1:B3" or "'Q3 plan'!B2"196-// into the sheet name, unquoted, and the rest. Without a sheet, sheet is197-// "" and rest is s.198-func SplitSheet(s string) (sheet, rest string) {199- s = strings.TrimSpace(s)200- if strings.HasPrefix(s, "'") {201- if name, end, ok := quotedName(s, 0); ok && end < len(s) && s[end] == '!' {202- return name, s[end+1:]203- }204- return "", s205- }206- if i := strings.LastIndexByte(s, '!'); i > 0 {207- return s[:i], s[i+1:]208- }209- return "", s210-}211-212-// Qualified writes r on sheet as a formula would: Sheet2!A1:B3.213-func Qualified(sheet string, r Rect) string {214- return quoteSheet(sheet) + "!" + r.String()215-}216-217146 // nextSheetName is the first free name among Sheet1, Sheet2, ... after218147 // the sheet count, as Sheets numbers new sheets.219148 func (w *Workbook) nextSheetName() string {@@ -326,13 +255,13 @@ func (w *Workbook) RenameSheet(s *Sheet, name string) error {
326255 if name == s.name {327256 return nil328257 }329- old := sheetKey(s.name)258+ old := formula.SheetKey(s.name)330259 w.change(s, "rename "+s.name+" to "+name, Rect{}, func() {331260 w.recordSheets()332261 delete(w.byKey, old)333262 s.name = name334- w.byKey[sheetKey(name)] = s335- if old != sheetKey(name) {263+ w.byKey[formula.SheetKey(name)] = s264+ if old != formula.SheetKey(name) {336265 w.renameRefs(old, name)337266 }338267 })@@ -341,16 +270,16 @@ func (w *Workbook) RenameSheet(s *Sheet, name string) error {
341270 342271 // renameRefs rewrites references written with the sheet key old to name.343272 func (w *Workbook) renameRefs(old, name string) {344- rw := refRewrite{345- ref: func(n refNode) Node {346- if n.sheet != "" && sheetKey(n.sheet) == old {347- n.sheet = name273+ rw := formula.Rewriter{274+ Ref: func(n formula.Ref) Node {275+ if n.Sheet != "" && formula.SheetKey(n.Sheet) == old {276+ n.Sheet = name348277 }349278 return n350279 },351- rng: func(n rangeNode) Node {352- if n.sheet != "" && sheetKey(n.sheet) == old {353- n.sheet = name280+ Range: func(n formula.Range) Node {281+ if n.Sheet != "" && formula.SheetKey(n.Sheet) == old {282+ n.Sheet = name354283 }355284 return n356285 },@@ -398,7 +327,7 @@ func (w *Workbook) attach(s *Sheet) {
398327 return399328 }400329 s.live = true401- w.byKey[sheetKey(s.name)] = s330+ w.byKey[formula.SheetKey(s.name)] = s402331 for a, c := range s.cells {403332 w.index(loc{s, a}, c)404333 }@@ -412,8 +341,8 @@ func (w *Workbook) detach(s *Sheet) {
412341 return413342 }414343 s.live = false415- if w.byKey[sheetKey(s.name)] == s {416- delete(w.byKey, sheetKey(s.name))344+ if w.byKey[formula.SheetKey(s.name)] == s {345+ delete(w.byKey, formula.SheetKey(s.name))417346 }418347 for a, c := range s.cells {419348 w.unindex(loc{s, a}, c)@@ -469,7 +398,7 @@ func (w *Workbook) resolve(s *Sheet, sheet string) *Sheet {
469398 if sheet == "" {470399 return s471400 }472- return w.byKey[sheetKey(sheet)]401+ return w.byKey[formula.SheetKey(sheet)]473402 }474403 475404 // onThis reports, for a formula on from, whether a reference written with@@ -58,47 +58,6 @@ func expect(t *testing.T, w *Workbook, want map[string]string) {
5858 }5959 }6060 61-func TestParseSheetRefs(t *testing.T) {62- tests := []struct{ in, out, err string }{63- {in: "=Sheet2!A1", out: "=Sheet2!A1"},64- {in: "=sheet2!$a$1+1", out: "=sheet2!$A$1+1"},65- {in: "='My Sheet'!B2:C9", out: "='My Sheet'!B2:C9"},66- {in: "=SUM('Bob''s'!A1:A3)", out: "=SUM('Bob''s'!A1:A3)"},67- {in: "=Data!B3:Data!A1", out: "=Data!A1:B3"},68- {in: "='2026'!A1", out: "='2026'!A1"},69- {in: "='A1'!A1", out: "='A1'!A1"},70- {in: "=Q3.plan!A1..B2", out: "=Q3.plan!A1:B2"},71- {in: "=Sheet2!Sales", err: "Expected a cell after Sheet2!"},72- {in: "=Sheet2!A1:Other!B2", err: "A range can't span sheets"},73- {in: "='Open!A1", err: "Expected ! after a quoted sheet name"},74- {in: "='x'A1", err: "Expected ! after a quoted sheet name"},75- }76- for _, tt := range tests {77- n, err := Parse(tt.in)78- switch {79- case tt.err != "":80- if err == nil || err.Error() != tt.err {81- t.Errorf("%s: error %v, want %q", tt.in, err, tt.err)82- }83- case err != nil:84- t.Errorf("%s: %v", tt.in, err)85- case formulaText(n) != tt.out:86- t.Errorf("%s prints as %s, want %s", tt.in, formulaText(n), tt.out)87- }88- }89-}90-91-func TestQuoteSheet(t *testing.T) {92- for name, want := range map[string]string{93- "Sheet1": "Sheet1", "Q3_plan": "Q3_plan", "My Sheet": "'My Sheet'", "Bob's": "'Bob''s'",94- "2026": "'2026'", "AB12": "'AB12'", "R1C1": "'R1C1'", "Café": "'Café'",95- } {96- if got := quoteSheet(name); got != want {97- t.Errorf("quoteSheet(%q) = %s, want %s", name, got, want)98- }99- }100-}101-10261 func TestSheetNames(t *testing.T) {10362 w := NewBook()10463 tests := []struct{ name, err string }{@@ -481,21 +440,6 @@ func BenchmarkSameSheetEdit(b *testing.B) {
481440 }482441 }483442 484-func TestSplitSheet(t *testing.T) {485- tests := []struct{ in, sheet, rest string }{486- {"A1", "", "A1"},487- {"Sheet2!A1:B3", "Sheet2", "A1:B3"},488- {"'Q3 plan'!B2", "Q3 plan", "B2"},489- {"'Bob''s'!C1", "Bob's", "C1"},490- {" Data!Z9 ", "Data", "Z9"},491- }492- for _, tt := range tests {493- if sheet, rest := SplitSheet(tt.in); sheet != tt.sheet || rest != tt.rest {494- t.Errorf("SplitSheet(%q) = %q, %q", tt.in, sheet, rest)495- }496- }497-}498-499443 func TestTraceAcrossSheets(t *testing.T) {500444 w := bookOf(t,501445 page{"Sheet1", map[string]string{"A1": "1", "B1": "=A1+Data!A1+SUM('Q3 plan'!A1:A2)+Gone!A1", "B2": "=Plan"}},