Entries follow Sheets: = starts a formula (a leading + or - that parses as one also counts), numbers like $1,200 and 12% are detected, ' forces text, and TRUE/FALSE are booleans. Formulas use plain function names and A1:B3 ranges; 1-2-3's @SUM(A1..B3) still parses. Errors use Sheets codes (#DIV/0!, #VALUE!, #NAME?, #N/A, #NUM!, #REF! for cycles), text in arithmetic is #VALUE!, and COUNT/COUNTA/AVERAGE/INT/MOD match Sheets. A function table carries each function's signature, description and arity, so wrong argument counts are reported at entry with the expected signature, and autocomplete and help can use the same data.
- commit
- 831c0ee3c9fc05990afb631490c177fbe9f41383
- parent
- 5acda22f33b03e0b53bd2f01173ddc7380c2b4c6
- author
- Dami <dami.etoile@gmail.com>
| M | .gitignore | 1 | + |
| M | internal/ | 9 | +- |
| M | internal/ | 327 | ++++++++++++------------------ |
| A | internal/ | 263 | ++++++++++++++++++++++++ |
| A | internal/ | 128 | ++++++++++++ |
| M | internal/ | 202 | +++++++++++------- |
| M | internal/ | 138 | +++++-------- |
| M | internal/ | 195 | ++++++++++++------ |
| A | internal/ | 2 | + |
@@ -89,12 +89,15 @@ func (r Rect) Contains(a Addr) bool {
8989 a.Row >= r.From.Row && a.Row <= r.To.Row9090 }9191 92-// String returns the range in 1-2-3 notation, e.g. A1..B3.92+// String returns the range as A1:B3, or A1 for a single cell.9393 func (r Rect) String() string {94- return r.From.String() + ".." + r.To.String()94+ if r.From == r.To {95+ return r.From.String()96+ }97+ return r.From.String() + ":" + r.To.String()9598 }9699 97-// ParseRange parses "A1", "A1..B3" or "A1:B3".100+// ParseRange parses "A1", "A1:B3" or 1-2-3 style "A1..B3".98101 func ParseRange(s string) (Rect, bool) {99102 s = strings.TrimSpace(s)100103 sep := strings.Index(s, "..")@@ -12,52 +12,71 @@ type Kind int
1212 const (1313 Empty Kind = iota1414 Number15- Label15+ Text16+ Bool1617 Error1718 )1819 1920 // Value is the computed contents of a cell.2021 type Value struct {2122 Kind Kind22- Num float6423- Str string // label text or error name (ERR, NA)23+ Num float64 // numbers, and booleans as 1 or 024+ Str string // text, or the error code such as #DIV/0!2425 }2526 27+// String renders a value the way a cell shows it in General format.28+func (v Value) String() string {29+ switch v.Kind {30+ case Number:31+ return strconv.FormatFloat(v.Num, 'f', -1, 64)32+ case Bool:33+ if v.Num != 0 {34+ return "TRUE"35+ }36+ return "FALSE"37+ }38+ return v.Str39+}40+41+// Error values, using Google Sheets codes.2642 var (27- errValue = Value{Kind: Error, Str: "ERR"}28- naValue = Value{Kind: Error, Str: "NA"}43+ ErrDiv0 = Value{Kind: Error, Str: "#DIV/0!"}44+ ErrValue = Value{Kind: Error, Str: "#VALUE!"}45+ ErrName = Value{Kind: Error, Str: "#NAME?"}46+ ErrNA = Value{Kind: Error, Str: "#N/A"}47+ ErrNum = Value{Kind: Error, Str: "#NUM!"}48+ ErrRef = Value{Kind: Error, Str: "#REF!"} // also circular references2949 )3050 3151 func num(v float64) Value {3252 if math.IsNaN(v) || math.IsInf(v, 0) {33- return errValue53+ return ErrNum3454 }3555 return Value{Kind: Number, Num: v}3656 }3757 38-func boolNum(b bool) Value {58+func boolean(b bool) Value {3959 if b {40- return num(1)60+ return Value{Kind: Bool, Num: 1}4161 }42- return num(0)62+ return Value{Kind: Bool}4363 }4464 45-// asNum converts v for arithmetic. Like 1-2-3, labels and blanks count as 0.46-func asNum(v Value) float64 {47- if v.Kind == Number {48- return v.Num49- }50- return 051-}52-53-func asStr(v Value) string {65+// toNum coerces v for arithmetic as Sheets does: blanks are 0, booleans66+// 1 or 0, numeric text is its number, other text is #VALUE!.67+func toNum(v Value) (float64, *Value) {5468 switch v.Kind {55- case Label:56- return v.Str57- case Number:58- return strconv.FormatFloat(v.Num, 'f', -1, 64)69+ case Empty:70+ return 0, nil71+ case Number, Bool:72+ return v.Num, nil73+ case Text:74+ if n, ok := ParseNumber(v.Str); ok {75+ return n, nil76+ }77+ return 0, &ErrValue5978 }60- return ""79+ return 0, &v6180 }6281 6382 // lookup resolves the current value of a referenced cell.@@ -68,12 +87,20 @@ func eval(n Node, get lookup) Value {
6887 case numLit:6988 return num(n.v)7089 case strLit:71- return Value{Kind: Label, Str: n.v}90+ return Value{Kind: Text, Str: n.v}91+ case boolLit:92+ return boolean(n.v)7293 case refNode:7394 return get(n.a)95+ case nameNode:96+ return ErrName7497 case rangeNode:75- // A bare range outside a function is an error in 1-2-3.76- return errValue98+ // A range outside a function: Sheets uses the top-left cell here99+ // for single-cell ranges and #VALUE! otherwise.100+ if n.r.From == n.r.To {101+ return get(n.r.From)102+ }103+ return ErrValue77104 case unaryNode:78105 x := eval(n.x, get)79106 if x.Kind == Error {@@ -81,18 +108,25 @@ func eval(n Node, get lookup) Value {
81108 }82109 switch n.op {83110 case "-":84- return num(-asNum(x))111+ f, err := toNum(x)112+ if err != nil {113+ return *err114+ }115+ return num(-f)85116 case "#NOT#":86- return boolNum(asNum(x) == 0)117+ f, err := toNum(x)118+ if err != nil {119+ return *err120+ }121+ return boolean(f == 0)87122 }88- // Unary + is identity, so string formulas like +B1&"x" work.89- return x123+ return x // unary + is identity90124 case binaryNode:91125 return evalBinary(n, get)92126 case callNode:93- return functions[n.name](n.args, get)127+ return n.fn.call(n.args, get)94128 }95- return errValue129+ return ErrValue96130 }97131 98132 func evalBinary(n binaryNode, get lookup) Value {@@ -103,7 +137,33 @@ func evalBinary(n binaryNode, get lookup) Value {
103137 if r.Kind == Error {104138 return r105139 }106- a, b := asNum(l), asNum(r)140+ switch n.op {141+ case "&":142+ return Value{Kind: Text, Str: text(l) + text(r)}143+ case "=", "<>", "<", ">", "<=", ">=":144+ c := compare(l, r)145+ switch n.op {146+ case "=":147+ return boolean(c == 0)148+ case "<>":149+ return boolean(c != 0)150+ case "<":151+ return boolean(c < 0)152+ case ">":153+ return boolean(c > 0)154+ case "<=":155+ return boolean(c <= 0)156+ }157+ return boolean(c >= 0)158+ }159+ a, err := toNum(l)160+ if err != nil {161+ return *err162+ }163+ b, err := toNum(r)164+ if err != nil {165+ return *err166+ }107167 switch n.op {108168 case "+":109169 return num(a + b)@@ -113,189 +173,56 @@ func evalBinary(n binaryNode, get lookup) Value {
113173 return num(a * b)114174 case "/":115175 if b == 0 {116- return errValue176+ return ErrDiv0117177 }118178 return num(a / b)119179 case "^":120180 return num(math.Pow(a, b))121- case "&":122- return Value{Kind: Label, Str: asStr(l) + asStr(r)}123181 case "#AND#":124- return boolNum(a != 0 && b != 0)182+ return boolean(a != 0 && b != 0)125183 case "#OR#":126- return boolNum(a != 0 || b != 0)184+ return boolean(a != 0 || b != 0)127185 }128- cmp := compare(l, r)129- switch n.op {130- case "=":131- return boolNum(cmp == 0)132- case "<>":133- return boolNum(cmp != 0)134- case "<":135- return boolNum(cmp < 0)136- case ">":137- return boolNum(cmp > 0)138- case "<=":139- return boolNum(cmp <= 0)140- case ">=":141- return boolNum(cmp >= 0)186+ return ErrValue187+}188+189+// text converts a value for string concatenation.190+func text(v Value) string {191+ if v.Kind == Empty {192+ return ""142193 }143- return errValue194+ return v.String()144195 }145196 197+// compare orders values like Sheets: numbers < text < booleans, text is198+// case-insensitive, and a blank equals 0 or "".146199 func compare(l, r Value) int {147- if l.Kind == Label && r.Kind == Label {200+ rank := func(v Value) int {201+ switch v.Kind {202+ case Text:203+ return 1204+ case Bool:205+ return 2206+ }207+ return 0208+ }209+ if l.Kind == Empty && r.Kind == Text {210+ l = Value{Kind: Text}211+ }212+ if r.Kind == Empty && l.Kind == Text {213+ r = Value{Kind: Text}214+ }215+ if d := rank(l) - rank(r); d != 0 {216+ return d217+ }218+ if l.Kind == Text {148219 return strings.Compare(strings.ToUpper(l.Str), strings.ToUpper(r.Str))149220 }150- a, b := asNum(l), asNum(r)151221 switch {152- case a < b:222+ case l.Num < r.Num:153223 return -1154- case a > b:224+ case l.Num > r.Num:155225 return 1156226 }157227 return 0158228 }159-160-type function func(args []Node, get lookup) Value161-162-var functions map[string]function163-164-func init() {165- functions = map[string]function{166- "SUM": aggregate(func(s stats) Value { return num(s.sum) }),167- "COUNT": aggregate(func(s stats) Value { return num(float64(s.count)) }),168- "AVG": aggregate(func(s stats) Value {169- if s.count == 0 {170- return errValue171- }172- return num(s.sum / float64(s.count))173- }),174- "MIN": aggregate(func(s stats) Value {175- if s.nums == 0 {176- return num(0)177- }178- return num(s.min)179- }),180- "MAX": aggregate(func(s stats) Value {181- if s.nums == 0 {182- return num(0)183- }184- return num(s.max)185- }),186- "ABS": math1(math.Abs),187- "INT": math1(math.Trunc),188- "SQRT": math1(math.Sqrt),189- "ROUND": math2(func(x, places float64) float64 {190- p := math.Pow(10, math.Trunc(places))191- return math.Round(x*p) / p192- }),193- "MOD": math2(func(x, y float64) float64 {194- if y == 0 {195- return math.NaN()196- }197- return math.Mod(x, y)198- }),199- "PI": constant(num(math.Pi)),200- "TRUE": constant(num(1)),201- "FALSE": constant(num(0)),202- "ERR": constant(errValue),203- "NA": constant(naValue),204- "IF": func(args []Node, get lookup) Value {205- if len(args) != 3 {206- return errValue207- }208- c := eval(args[0], get)209- if c.Kind == Error {210- return c211- }212- if asNum(c) != 0 {213- return eval(args[1], get)214- }215- return eval(args[2], get)216- },217- }218-}219-220-type stats struct {221- sum float64222- count int // non-blank cells, as 1-2-3's @COUNT223- nums int224- min, max float64225-}226-227-// aggregate builds a list function such as @SUM. Ranges contribute every228-// non-blank cell; labels count toward @COUNT but add 0, as in 1-2-3.229-func aggregate(done func(stats) Value) function {230- return func(args []Node, get lookup) Value {231- s := stats{min: math.Inf(1), max: math.Inf(-1)}232- add := func(v Value) *Value {233- switch v.Kind {234- case Error:235- return &v236- case Empty:237- return nil238- case Number:239- s.nums++240- s.min, s.max = math.Min(s.min, v.Num), math.Max(s.max, v.Num)241- }242- s.count++243- s.sum += asNum(v)244- return nil245- }246- for _, arg := range args {247- if rn, ok := arg.(rangeNode); ok {248- for r := rn.r.From.Row; r <= rn.r.To.Row; r++ {249- for c := rn.r.From.Col; c <= rn.r.To.Col; c++ {250- if e := add(get(Addr{Col: c, Row: r})); e != nil {251- return *e252- }253- }254- }255- continue256- }257- if e := add(eval(arg, get)); e != nil {258- return *e259- }260- }261- return done(s)262- }263-}264-265-func math1(f func(float64) float64) function {266- return func(args []Node, get lookup) Value {267- if len(args) != 1 {268- return errValue269- }270- x := eval(args[0], get)271- if x.Kind == Error {272- return x273- }274- return num(f(asNum(x)))275- }276-}277-278-func math2(f func(float64, float64) float64) function {279- return func(args []Node, get lookup) Value {280- if len(args) != 2 {281- return errValue282- }283- x, y := eval(args[0], get), eval(args[1], get)284- if x.Kind == Error {285- return x286- }287- if y.Kind == Error {288- return y289- }290- return num(f(asNum(x), asNum(y)))291- }292-}293-294-func constant(v Value) function {295- return func(args []Node, _ lookup) Value {296- if len(args) != 0 {297- return errValue298- }299- return v300- }301-}@@ -0,0 +1,263 @@
1+package sheet2+3+import (4+ "math"5+ "slices"6+)7+8+// FuncDef describes a spreadsheet function. The table drives parsing9+// (arity checks), evaluation, autocomplete and help.10+type FuncDef struct {11+ Name string12+ Args string // signature shown to users, e.g. "value1, [value2, ...]"13+ Desc string14+ Min int15+ Max int // -1 for variadic16+ eval func(args []Node, get lookup) Value17+}18+19+func (f *FuncDef) call(args []Node, get lookup) Value { return f.eval(args, get) }20+21+var funcs = map[string]*FuncDef{}22+23+// aliases maps alternative names (including 1-2-3's) to canonical ones.24+var aliases = map[string]string{"AVG": "AVERAGE"}25+26+func define(defs ...*FuncDef) {27+ for _, d := range defs {28+ funcs[d.Name] = d29+ }30+}31+32+// LookupFunc finds a function by name or alias, case-insensitively33+// (callers pass upper case).34+func LookupFunc(name string) (*FuncDef, bool) {35+ if canon, ok := aliases[name]; ok {36+ name = canon37+ }38+ f, ok := funcs[name]39+ return f, ok40+}41+42+// Funcs returns every function, sorted by name.43+func Funcs() []*FuncDef {44+ out := make([]*FuncDef, 0, len(funcs))45+ for _, f := range funcs {46+ out = append(out, f)47+ }48+ slices.SortFunc(out, func(a, b *FuncDef) int {49+ if a.Name < b.Name {50+ return -151+ }52+ return 153+ })54+ return out55+}56+57+func init() {58+ define(59+ &FuncDef{Name: "SUM", Args: "value1, [value2, ...]", Desc: "Sum of numbers", Min: 1, Max: -1,60+ eval: aggregate(func(s agg) Value { return num(s.sum) })},61+ &FuncDef{Name: "AVERAGE", Args: "value1, [value2, ...]", Desc: "Average of numbers, ignoring text", Min: 1, Max: -1,62+ eval: aggregate(func(s agg) Value {63+ if s.nums == 0 {64+ return ErrDiv065+ }66+ return num(s.sum / float64(s.nums))67+ })},68+ &FuncDef{Name: "COUNT", Args: "value1, [value2, ...]", Desc: "Count of numeric values", Min: 1, Max: -1,69+ eval: aggregate(func(s agg) Value { return num(float64(s.nums)) })},70+ &FuncDef{Name: "COUNTA", Args: "value1, [value2, ...]", Desc: "Count of non-empty values", Min: 1, Max: -1,71+ eval: aggregate(func(s agg) Value { return num(float64(s.count)) })},72+ &FuncDef{Name: "MIN", Args: "value1, [value2, ...]", Desc: "Smallest number", Min: 1, Max: -1,73+ eval: aggregate(func(s agg) Value {74+ if s.nums == 0 {75+ return num(0)76+ }77+ return num(s.min)78+ })},79+ &FuncDef{Name: "MAX", Args: "value1, [value2, ...]", Desc: "Largest number", Min: 1, Max: -1,80+ eval: aggregate(func(s agg) Value {81+ if s.nums == 0 {82+ return num(0)83+ }84+ return num(s.max)85+ })},86+ &FuncDef{Name: "ABS", Args: "value", Desc: "Absolute value", Min: 1, Max: 1, eval: math1(math.Abs)},87+ &FuncDef{Name: "INT", Args: "value", Desc: "Round down to the nearest integer", Min: 1, Max: 1, eval: math1(math.Floor)},88+ &FuncDef{Name: "SQRT", Args: "value", Desc: "Square root", Min: 1, Max: 1, eval: math1(math.Sqrt)},89+ &FuncDef{Name: "ROUND", Args: "value, [places]", Desc: "Round to a number of decimal places", Min: 1, Max: 2,90+ eval: numeric(func(x []float64) Value {91+ places := 0.092+ if len(x) > 1 {93+ places = math.Trunc(x[1])94+ }95+ p := math.Pow(10, places)96+ return num(math.Round(x[0]*p) / p)97+ })},98+ &FuncDef{Name: "MOD", Args: "dividend, divisor", Desc: "Remainder, with the sign of the divisor", Min: 2, Max: 2,99+ eval: numeric(func(x []float64) Value {100+ if x[1] == 0 {101+ return ErrDiv0102+ }103+ return num(x[0] - x[1]*math.Floor(x[0]/x[1]))104+ })},105+ &FuncDef{Name: "PI", Desc: "The number pi", Max: 0, eval: constant(num(math.Pi))},106+ &FuncDef{Name: "TRUE", Desc: "The logical value TRUE", Max: 0, eval: constant(boolean(true))},107+ &FuncDef{Name: "FALSE", Desc: "The logical value FALSE", Max: 0, eval: constant(boolean(false))},108+ &FuncDef{Name: "NA", Desc: "The #N/A error", Max: 0, eval: constant(ErrNA)},109+ &FuncDef{Name: "IF", Args: "condition, value_if_true, [value_if_false]", Desc: "Choose a value by a condition", Min: 2, Max: 3,110+ eval: func(args []Node, get lookup) Value {111+ c := eval(args[0], get)112+ if c.Kind == Error {113+ return c114+ }115+ f, err := toNum(c)116+ if err != nil {117+ return *err118+ }119+ if f != 0 {120+ return eval(args[1], get)121+ }122+ if len(args) < 3 {123+ return boolean(false)124+ }125+ return eval(args[2], get)126+ }},127+ &FuncDef{Name: "IFERROR", Args: "value, [value_if_error]", Desc: "A fallback when a value is an error", Min: 1, Max: 2,128+ eval: func(args []Node, get lookup) Value {129+ v := eval(args[0], get)130+ if v.Kind != Error {131+ return v132+ }133+ if len(args) < 2 {134+ return Value{Kind: Text}135+ }136+ return eval(args[1], get)137+ }},138+ &FuncDef{Name: "AND", Args: "logical1, [logical2, ...]", Desc: "TRUE if all arguments are true", Min: 1, Max: -1,139+ eval: logical(func(t, n int) bool { return t == n })},140+ &FuncDef{Name: "OR", Args: "logical1, [logical2, ...]", Desc: "TRUE if any argument is true", Min: 1, Max: -1,141+ eval: logical(func(t, _ int) bool { return t > 0 })},142+ &FuncDef{Name: "NOT", Args: "logical", Desc: "The opposite of a logical value", Min: 1, Max: 1,143+ eval: numeric(func(x []float64) Value { return boolean(x[0] == 0) })},144+ )145+}146+147+type agg struct {148+ sum float64149+ count int // non-empty values150+ nums int151+ min, max float64152+}153+154+// each calls fn for every value in args: every cell of a range, or the155+// value of any other argument. direct is false for range cells.156+func each(args []Node, get lookup, fn func(v Value, direct bool) *Value) *Value {157+ for _, arg := range args {158+ if rn, ok := arg.(rangeNode); ok {159+ for r := rn.r.From.Row; r <= rn.r.To.Row; r++ {160+ for c := rn.r.From.Col; c <= rn.r.To.Col; c++ {161+ if e := fn(get(Addr{Col: c, Row: r}), false); e != nil {162+ return e163+ }164+ }165+ }166+ continue167+ }168+ if e := fn(eval(arg, get), true); e != nil {169+ return e170+ }171+ }172+ return nil173+}174+175+// aggregate builds SUM-like functions with Sheets semantics: in ranges only176+// numbers count and text is ignored; direct arguments are coerced, so177+// SUM("a") is #VALUE!.178+func aggregate(done func(agg) Value) func([]Node, lookup) Value {179+ return func(args []Node, get lookup) Value {180+ s := agg{min: math.Inf(1), max: math.Inf(-1)}181+ e := each(args, get, func(v Value, direct bool) *Value {182+ switch v.Kind {183+ case Error:184+ return &v185+ case Empty:186+ return nil187+ }188+ s.count++189+ if v.Kind != Number && !direct {190+ return nil191+ }192+ f, err := toNum(v)193+ if err != nil {194+ return err195+ }196+ s.nums++197+ s.sum += f198+ s.min, s.max = math.Min(s.min, f), math.Max(s.max, f)199+ return nil200+ })201+ if e != nil {202+ return *e203+ }204+ return done(s)205+ }206+}207+208+func logical(test func(trues, n int) bool) func([]Node, lookup) Value {209+ return func(args []Node, get lookup) Value {210+ trues, n := 0, 0211+ e := each(args, get, func(v Value, direct bool) *Value {212+ switch {213+ case v.Kind == Error:214+ return &v215+ case v.Kind == Empty, v.Kind == Text && !direct:216+ return nil217+ }218+ f, err := toNum(v)219+ if err != nil {220+ return err221+ }222+ n++223+ if f != 0 {224+ trues++225+ }226+ return nil227+ })228+ switch {229+ case e != nil:230+ return *e231+ case n == 0:232+ return ErrValue233+ }234+ return boolean(test(trues, n))235+ }236+}237+238+// numeric evaluates every argument as a number, propagating errors.239+func numeric(f func([]float64) Value) func([]Node, lookup) Value {240+ return func(args []Node, get lookup) Value {241+ xs := make([]float64, len(args))242+ for i, a := range args {243+ v := eval(a, get)244+ if v.Kind == Error {245+ return v246+ }247+ x, err := toNum(v)248+ if err != nil {249+ return *err250+ }251+ xs[i] = x252+ }253+ return f(xs)254+ }255+}256+257+func math1(f func(float64) float64) func([]Node, lookup) Value {258+ return numeric(func(x []float64) Value { return num(f(x[0])) })259+}260+261+func constant(v Value) func([]Node, lookup) Value {262+ return func([]Node, lookup) Value { return v }263+}@@ -0,0 +1,128 @@
1+package sheet2+3+import (4+ "strconv"5+ "strings"6+)7+8+// ParseNumber recognizes numbers the way Google Sheets does on entry:9+// optional sign, optional leading currency symbol, thousands separators,10+// decimals, exponent, and a trailing percent sign ("12%" is 0.12).11+func ParseNumber(s string) (float64, bool) {12+ s = strings.TrimSpace(s)13+ if s == "" {14+ return 0, false15+ }16+ neg := false17+ if s[0] == '-' || s[0] == '+' {18+ neg = s[0] == '-'19+ s = s[1:]20+ }21+ s = strings.TrimPrefix(s, "$")22+ pct := strings.HasSuffix(s, "%")23+ s = strings.TrimSuffix(s, "%")24+ if s == "" || !isDigit(s[0]) && s[0] != '.' {25+ return 0, false26+ }27+ if strings.Contains(s, ",") {28+ if !validGrouping(s) {29+ return 0, false30+ }31+ s = strings.ReplaceAll(s, ",", "")32+ }33+ v, err := strconv.ParseFloat(s, 64)34+ if err != nil {35+ return 0, false36+ }37+ if pct {38+ v /= 10039+ }40+ if neg {41+ v = -v42+ }43+ return v, true44+}45+46+// validGrouping checks thousands separators: groups of three digits47+// before the decimal point.48+func validGrouping(s string) bool {49+ intPart, _, _ := strings.Cut(s, ".")50+ groups := strings.Split(intPart, ",")51+ if len(groups[0]) == 0 || len(groups[0]) > 3 {52+ return false53+ }54+ for _, g := range groups[1:] {55+ if len(g) != 3 {56+ return false57+ }58+ }59+ return true60+}61+62+// FormatValue renders v in width columns with one column of padding, the63+// way the grid shows it: numbers right-aligned, booleans and errors64+// centered. Text is not handled here because it can overflow into65+// neighboring cells.66+func FormatValue(v Value, width int) string {67+ inner := width - 168+ switch v.Kind {69+ case Number:70+ return padLeft(formatGeneral(v.Num, inner), inner) + " "71+ case Bool, Error:72+ return centerPad(v.String(), width)73+ }74+ return ""75+}76+77+// FormatNumber formats a number in the General format within width78+// columns, for use outside the grid (e.g. the status line).79+func FormatNumber(v float64, width int) string {80+ return formatGeneral(v, width)81+}82+83+// formatGeneral mimics General number format: as many digits as fit,84+// rounding decimals first, then scientific notation, then #s.85+func formatGeneral(v float64, width int) string {86+ if width <= 0 {87+ return ""88+ }89+ s := strconv.FormatFloat(v, 'f', -1, 64)90+ if len(s) <= width {91+ return s92+ }93+ // Try fewer decimals before falling back to scientific notation, but94+ // never round a non-zero number down to zero.95+ if dot := strings.IndexByte(s, '.'); dot >= 0 && dot <= width {96+ for prec := max(width-dot-1, 0); prec >= 0; prec-- {97+ f := strconv.FormatFloat(v, 'f', prec, 64)98+ if len(f) > width {99+ continue100+ }101+ if r, _ := strconv.ParseFloat(f, 64); r != 0 || v == 0 {102+ return f103+ }104+ break105+ }106+ }107+ for prec := width; prec >= 0; prec-- {108+ if e := strconv.FormatFloat(v, 'E', prec, 64); len(e) <= width {109+ return e110+ }111+ }112+ return strings.Repeat("#", width)113+}114+115+func padLeft(s string, width int) string {116+ if len(s) >= width {117+ return s118+ }119+ return strings.Repeat(" ", width-len(s)) + s120+}121+122+func centerPad(s string, width int) string {123+ if len(s) >= width {124+ return s[:width]125+ }126+ pad := width - len(s)127+ return strings.Repeat(" ", pad/2) + s + strings.Repeat(" ", pad-pad/2)128+}@@ -12,8 +12,10 @@ type Node any
1212 type (1313 numLit struct{ v float64 }1414 strLit struct{ v string }15+ boolLit struct{ v bool }1516 refNode struct{ a Addr }1617 rangeNode struct{ r Rect }18+ nameNode struct{ name string } // an identifier that isn't a cell; #NAME? until names exist1719 unaryNode struct {1820 op string1921 x Node@@ -23,7 +25,7 @@ type (
2325 l, r Node2426 }2527 callNode struct {26- name string28+ fn *FuncDef2729 args []Node2830 }2931 )@@ -34,8 +36,8 @@ const (
3436 tokEOF tokKind = iota3537 tokNum3638 tokStr37- tokRef38- tokFunc39+ tokIdent // cell reference, TRUE/FALSE or a name40+ tokFunc // identifier directly followed by "("3941 tokOp4042 )4143 @@ -46,15 +48,18 @@ type token struct {
4648 }4749 4850 // ParseError describes a formula that could not be parsed. Pos is the byte49-// offset where the problem was found, used to place the EDIT cursor.51+// offset in the entry where the problem was found, used to place the edit52+// cursor.5053 type ParseError struct {5154 Pos int5255 Msg string5356 }5457 55-func (e *ParseError) Error() string {56- return fmt.Sprintf("%s at position %d", e.Msg, e.Pos+1)57-}58+func (e *ParseError) Error() string { return e.Msg }59+60+func isIdentStart(c byte) bool { return isLetter(c) || c == '_' || c == '$' }61+62+func isIdentPart(c byte) bool { return isIdentStart(c) || isDigit(c) || c == '.' }5863 5964 func lex(src string) ([]token, error) {6065 var toks []token@@ -67,7 +72,7 @@ func lex(src string) ([]token, error) {
6772 i++6873 continue6974 case isDigit(c) || (c == '.' && i+1 < len(src) && isDigit(src[i+1])):70- for i < len(src) && (isDigit(src[i]) || src[i] == '.') {75+ for i < len(src) && (isDigit(src[i]) || src[i] == '.' && !strings.HasPrefix(src[i:], "..")) {7176 i++7277 }7378 if i < len(src) && (src[i] == 'e' || src[i] == 'E') {@@ -84,40 +89,56 @@ func lex(src string) ([]token, error) {
8489 }8590 toks = append(toks, token{tokNum, src[start:i], start})8691 case c == '"':92+ // Strings use "" to escape a quote, as in Sheets.93+ var sb strings.Builder8794 i++88- for i < len(src) && src[i] != '"' {95+ for {96+ if i >= len(src) {97+ return nil, &ParseError{start, "Missing closing quote"}98+ }99+ if src[i] == '"' {100+ if i+1 < len(src) && src[i+1] == '"' {101+ sb.WriteByte('"')102+ i += 2103+ continue104+ }105+ i++106+ break107+ }108+ sb.WriteByte(src[i])89109 i++90110 }91- if i == len(src) {92- return nil, &ParseError{start, "unterminated string"}111+ toks = append(toks, token{tokStr, sb.String(), start})112+ case c == '@' || isIdentStart(c):113+ if c == '@' { // 1-2-3 style @SUM114+ i++93115 }94- i++95- toks = append(toks, token{tokStr, src[start+1 : i-1], start})96- case c == '@':97- i++98- for i < len(src) && isLetter(src[i]) {116+ for i < len(src) && isIdentPart(src[i]) && !strings.HasPrefix(src[i:], "..") {99117 i++100118 }101- if i == start+1 {102- return nil, &ParseError{start, "missing function name"}119+ name := strings.ToUpper(strings.TrimPrefix(src[start:i], "@"))120+ if name == "" {121+ return nil, &ParseError{start, "Missing function name after @"}103122 }104- toks = append(toks, token{tokFunc, strings.ToUpper(src[start+1 : i]), start})105- case c == '$' || isLetter(c):106- for i < len(src) && (src[i] == '$' || isLetter(src[i]) || isDigit(src[i])) {107- i++123+ j := i124+ for j < len(src) && src[j] == ' ' {125+ j++108126 }109- if _, ok := ParseAddr(src[start:i]); !ok {110- return nil, &ParseError{start, fmt.Sprintf("invalid cell reference %q", src[start:i])}127+ kind := tokIdent128+ if j < len(src) && src[j] == '(' {129+ kind = tokFunc130+ } else if c == '@' {131+ kind = tokFunc // @PI without parentheses111132 }112- toks = append(toks, token{tokRef, strings.ToUpper(src[start:i]), start})133+ toks = append(toks, token{kind, name, start})113134 case c == '#':114135 end := strings.IndexByte(src[i+1:], '#')115136 if end < 0 {116- return nil, &ParseError{start, "unterminated logical operator"}137+ return nil, &ParseError{start, "Unexpected #"}117138 }118139 op := strings.ToUpper(src[i : i+end+2])119140 if op != "#AND#" && op != "#OR#" && op != "#NOT#" {120- return nil, &ParseError{start, "unknown operator " + op}141+ return nil, &ParseError{start, "Unknown operator " + op}121142 }122143 i += end + 2123144 toks = append(toks, token{tokOp, op, start})@@ -125,17 +146,17 @@ func lex(src string) ([]token, error) {
125146 strings.HasPrefix(src[i:], ">="), strings.HasPrefix(src[i:], "<>"):126147 i += 2127148 toks = append(toks, token{tokOp, src[start:i], start})128- case strings.IndexByte("+-*/^=<>&(),;:", c) >= 0:149+ case strings.IndexByte("+-*/^=<>&(),;:%", c) >= 0:129150 i++130151 toks = append(toks, token{tokOp, string(c), start})131152 default:132- return nil, &ParseError{start, fmt.Sprintf("unexpected %q", c)}153+ return nil, &ParseError{start, fmt.Sprintf("Unexpected %q", c)}133154 }134155 }135156 return append(toks, token{tokEOF, "", len(src)}), nil136157 }137158 138-// Binding powers, lowest to highest, following 1-2-3's precedence table.159+// Binding powers, lowest to highest, following Sheets' precedence.139160 var infixPower = map[string]int{140161 "#OR#": 10, "#AND#": 10,141162 "=": 30, "<>": 30, "<": 30, ">": 30, "<=": 30, ">=": 30,@@ -146,8 +167,9 @@ var infixPower = map[string]int{
146167 }147168 148169 const (149- notPower = 20150- unaryPower = 60170+ notPower = 20171+ unaryPower = 60172+ percentPower = 80151173 )152174 153175 type parser struct {@@ -155,8 +177,8 @@ type parser struct {
155177 pos int156178 }157179 158-// Parse parses a value entry. A leading '=' is accepted for users coming159-// from modern spreadsheets; 1-2-3 itself starts formulas with '+'.180+// Parse parses a formula. src may start with "=", as typed in a cell;181+// error positions are relative to src.160182 func Parse(src string) (Node, error) {161183 body := strings.TrimPrefix(src, "=")162184 offset := len(src) - len(body)@@ -167,7 +189,7 @@ func Parse(src string) (Node, error) {
167189 p := &parser{toks: toks}168190 n, err := p.expr(0)169191 if err == nil && p.peek().kind != tokEOF {170- err = &ParseError{p.peek().pos, "unexpected " + p.peek().text}192+ err = &ParseError{p.peek().pos, "Unexpected " + p.peek().text}171193 }172194 if err != nil {173195 return nil, shift(err, offset)@@ -192,11 +214,9 @@ func (p *parser) next() token {
192214 return t193215 }194216 195-func (p *parser) expect(op string) error {196- if t := p.next(); t.kind != tokOp || t.text != op {197- return &ParseError{t.pos, "expected " + op}198- }199- return nil217+func (p *parser) isOp(op string) bool {218+ t := p.peek()219+ return t.kind == tokOp && t.text == op200220 }201221 202222 func (p *parser) expr(minPower int) (Node, error) {@@ -205,6 +225,11 @@ func (p *parser) expr(minPower int) (Node, error) {
205225 return nil, err206226 }207227 for {228+ if p.isOp("%") && percentPower > minPower {229+ p.next()230+ left = binaryNode{op: "/", l: left, r: numLit{100}}231+ continue232+ }208233 t := p.peek()209234 power, ok := infixPower[t.text]210235 if t.kind != tokOp || !ok || power <= minPower {@@ -225,23 +250,13 @@ func (p *parser) prefix() (Node, error) {
225250 case tokNum:226251 v, err := strconv.ParseFloat(t.text, 64)227252 if err != nil {228- return nil, &ParseError{t.pos, "invalid number " + t.text}253+ return nil, &ParseError{t.pos, "Invalid number " + t.text}229254 }230255 return numLit{v}, nil231256 case tokStr:232257 return strLit{t.text}, nil233- case tokRef:234- a, _ := ParseAddr(t.text)235- if n := p.peek(); n.kind == tokOp && (n.text == ".." || n.text == ":") {236- p.next()237- end := p.next()238- if end.kind != tokRef {239- return nil, &ParseError{end.pos, "expected cell reference after " + n.text}240- }241- b, _ := ParseAddr(end.text)242- return rangeNode{NewRect(a, b)}, nil243- }244- return refNode{a}, nil258+ case tokIdent:259+ return p.ident(t)245260 case tokFunc:246261 return p.call(t)247262 case tokOp:@@ -251,7 +266,11 @@ func (p *parser) prefix() (Node, error) {
251266 if err != nil {252267 return nil, err253268 }254- return n, p.expect(")")269+ if !p.isOp(")") {270+ return nil, &ParseError{p.peek().pos, "Missing )"}271+ }272+ p.next()273+ return n, nil255274 case "+", "-":256275 x, err := p.expr(unaryPower)257276 if err != nil {@@ -266,38 +285,65 @@ func (p *parser) prefix() (Node, error) {
266285 return unaryNode{op: t.text, x: x}, nil267286 }268287 case tokEOF:269- return nil, &ParseError{t.pos, "incomplete formula"}288+ return nil, &ParseError{t.pos, "Formula is incomplete"}270289 }271- return nil, &ParseError{t.pos, "unexpected " + t.text}290+ return nil, &ParseError{t.pos, "Unexpected " + t.text}272291 }273292 274-func (p *parser) call(fn token) (Node, error) {275- if _, ok := functions[fn.text]; !ok {276- return nil, &ParseError{fn.pos, "unknown function @" + fn.text}293+func (p *parser) ident(t token) (Node, error) {294+ a, isRef := ParseAddr(t.text)295+ if !isRef {296+ switch t.text {297+ case "TRUE":298+ return boolLit{true}, nil299+ case "FALSE":300+ return boolLit{false}, nil301+ }302+ return nameNode{t.text}, nil277303 }278- n := callNode{name: fn.text}279- if t := p.peek(); t.kind != tokOp || t.text != "(" {280- return n, nil304+ if p.isOp(":") || p.isOp("..") {305+ sep := p.next()306+ end := p.next()307+ b, ok := ParseAddr(end.text)308+ if end.kind != tokIdent || !ok {309+ return nil, &ParseError{end.pos, "Expected a cell after " + sep.text}310+ }311+ return rangeNode{NewRect(a, b)}, nil281312 }282- p.next()283- if t := p.peek(); t.kind == tokOp && t.text == ")" {284- p.next()285- return n, nil313+ return refNode{a}, nil314+}315+316+func (p *parser) call(t token) (Node, error) {317+ fn, ok := LookupFunc(t.text)318+ if !ok {319+ return nil, &ParseError{t.pos, "Unknown function " + t.text}286320 }287- for {288- arg, err := p.expr(0)289- if err != nil {290- return nil, err291- }292- n.args = append(n.args, arg)293- t := p.next()294- if t.kind == tokOp && t.text == ")" {295- return n, nil296- }297- if t.kind != tokOp || (t.text != "," && t.text != ";") {298- return nil, &ParseError{t.pos, "expected , or )"}321+ n := callNode{fn: fn}322+ if p.isOp("(") {323+ p.next()324+ if p.isOp(")") {325+ p.next()326+ } else {327+ for {328+ arg, err := p.expr(0)329+ if err != nil {330+ return nil, err331+ }332+ n.args = append(n.args, arg)333+ sep := p.next()334+ if sep.kind == tokOp && sep.text == ")" {335+ break336+ }337+ if sep.kind != tokOp || (sep.text != "," && sep.text != ";") {338+ return nil, &ParseError{sep.pos, "Expected , or ) in " + fn.Name}339+ }340+ }299341 }300342 }343+ if len(n.args) < fn.Min || (fn.Max >= 0 && len(n.args) > fn.Max) {344+ return nil, &ParseError{t.pos, fmt.Sprintf("Wrong number of arguments to %s(%s)", fn.Name, fn.Args)}345+ }346+ return n, nil301347 }302348 303349 // walkRefs calls fn for every single-cell reference and range in n.@@ -5,35 +5,28 @@ package sheet
55 import (66 "maps"77 "sort"8- "strconv"98 "strings"109 "unicode"1110 )1211 13-// DefaultWidth is the initial column width, as in 1-2-3.14-const DefaultWidth = 912+// DefaultWidth is the initial column width: nine characters plus padding.13+const DefaultWidth = 101514 1615 // Cell holds what the user typed and what it evaluates to.1716 type Cell struct {18- // Input is the entry as typed. Labels always carry an alignment19- // prefix: ' (left), " (right) or ^ (center).17+ // Input is the entry exactly as typed: text, a number such as "$1,200"18+ // or "12%", or a formula starting with "=".2019 Input string2120 Value Value2221 23- expr Node // nil for labels22+ expr Node // nil for text2423 refs []Addr // single-cell references in expr2524 ranges []Rect // range references in expr2625 }2726 28-// IsLabel reports whether the cell holds a label rather than a value.29-func (c *Cell) IsLabel() bool { return c.expr == nil }30-31-// Align returns the label prefix character, or 0 for values.32-func (c *Cell) Align() byte {33- if c.IsLabel() {34- return c.Input[0]35- }36- return 027+// IsFormula reports whether the cell holds a formula.28+func (c *Cell) IsFormula() bool {29+ return c.expr != nil && IsFormulaEntry(c.Input)3730 }3831 3932 // Sheet is a sparse worksheet.@@ -112,19 +105,47 @@ func (s *Sheet) Widths() map[int]int {
112105 return maps.Clone(s.widths)113106 }114107 115-// IsValueEntry reports whether 1-2-3 would treat input as a value (number or116-// formula) rather than a label, based on its first character.117-func IsValueEntry(input string) bool {118- if input == "" {108+// IsFormulaEntry reports whether input is written as a formula: it starts109+// with "=", or with "+" or "-" followed by something that isn't a plain110+// number (as Sheets accepts "+A1").111+func IsFormulaEntry(input string) bool {112+ if strings.HasPrefix(input, "=") {113+ return true114+ }115+ if !strings.HasPrefix(input, "+") && !strings.HasPrefix(input, "-") {119116 return false120117 }121- return strings.IndexByte("0123456789+-.(@#$=", input[0]) >= 0118+ if _, isNum := ParseNumber(input); isNum {119+ return false120+ }121+ _, err := Parse(input)122+ return err == nil123+}124+125+// classify turns an entry into an expression, or nil for text. Entries126+// starting with ' are always text.127+func classify(input string) (Node, error) {128+ if strings.HasPrefix(input, "'") {129+ return nil, nil130+ }131+ if v, ok := ParseNumber(input); ok {132+ return numLit{v}, nil133+ }134+ switch strings.ToUpper(input) {135+ case "TRUE":136+ return boolLit{true}, nil137+ case "FALSE":138+ return boolLit{false}, nil139+ }140+ if IsFormulaEntry(input) {141+ return Parse(input)142+ }143+ return nil, nil122144 }123145 124146 // Set stores an entry at a and recalculates affected cells. An empty input125-// erases the cell. Labels without a prefix get the default ' prefix. A126-// value entry that fails to parse is rejected with a *ParseError and the127-// sheet is left unchanged.147+// erases the cell. A formula that fails to parse is rejected with a148+// *ParseError and the sheet is left unchanged.128149 func (s *Sheet) Set(a Addr, input string) error {129150 if err := s.put(a, input); err != nil {130151 return err@@ -146,18 +167,15 @@ func (s *Sheet) put(a Addr, input string) error {
146167 s.erase(a)147168 return nil148169 }149- c := &Cell{Input: input}150- if IsValueEntry(input) {151- n, err := Parse(input)152- if err != nil {153- return err154- }155- c.expr = n170+ n, err := classify(input)171+ if err != nil {172+ return err173+ }174+ c := &Cell{Input: input, expr: n}175+ if n != nil {156176 walkRefs(n,157177 func(r Addr) { c.refs = append(c.refs, r) },158178 func(r Rect) { c.ranges = append(c.ranges, r) })159- } else if strings.IndexByte(`'"^`, input[0]) < 0 {160- c.Input = "'" + input161179 }162180 s.erase(a)163181 s.cells[a] = c@@ -248,13 +266,13 @@ func (s *Sheet) recalc(changed []Addr) {
248266 return Value{}249267 case state[a] == visiting:250268 s.Circular = true251- return errValue269+ return ErrRef252270 case state[a] != dirty:253271 return c.Value254272 }255273 state[a] = visiting256- if c.IsLabel() {257- c.Value = Value{Kind: Label, Str: c.Input[1:]}274+ if c.expr == nil {275+ c.Value = Value{Kind: Text, Str: strings.TrimPrefix(c.Input, "'")}258276 } else {259277 c.Value = eval(c.expr, compute)260278 }@@ -265,53 +283,3 @@ func (s *Sheet) recalc(changed []Addr) {
265283 compute(a)266284 }267285 }268-269-// FormatValue renders v to fit in width columns using 1-2-3's General270-// format: numbers are right-aligned with a trailing space, and a number271-// that cannot fit is shown as asterisks.272-func FormatValue(v Value, width int) string {273- switch v.Kind {274- case Number:275- return padLeft(formatGeneral(v.Num, width-1), width-1) + " "276- case Error:277- return padLeft(v.Str, width-1) + " "278- }279- return ""280-}281-282-func formatGeneral(v float64, width int) string {283- if width <= 0 {284- return ""285- }286- s := strconv.FormatFloat(v, 'f', -1, 64)287- if len(s) <= width {288- return s289- }290- // Try fewer decimals before falling back to scientific notation, but291- // never round a non-zero number down to zero.292- if dot := strings.IndexByte(s, '.'); dot >= 0 && dot <= width {293- for prec := max(width-dot-1, 0); prec >= 0; prec-- {294- f := strconv.FormatFloat(v, 'f', prec, 64)295- if len(f) > width {296- continue297- }298- if r, _ := strconv.ParseFloat(f, 64); r != 0 || v == 0 {299- return f300- }301- break302- }303- }304- for prec := width; prec >= 0; prec-- {305- if e := strconv.FormatFloat(v, 'E', prec, 64); len(e) <= width {306- return e307- }308- }309- return strings.Repeat("*", width)310-}311-312-func padLeft(s string, width int) string {313- if len(s) >= width {314- return s315- }316- return strings.Repeat(" ", width-len(s)) + s317-}@@ -3,7 +3,9 @@ package sheet
33 import (44 "bytes"55 "errors"6+ "strings"67 "testing"8+ "unicode"79 )810 911 func at(s string) Addr {@@ -46,18 +48,20 @@ func TestAddr(t *testing.T) {
4648 }4749 4850 func TestParseRange(t *testing.T) {49- for _, in := range []string{"B3..A1", "a1:b3", "$A$1..$B$3"} {51+ for _, in := range []string{"B3..A1", "a1:b3", "$A$1:$B$3"} {5052 r, ok := ParseRange(in)51- if !ok || r.String() != "A1..B3" {53+ if !ok || r.String() != "A1:B3" {5254 t.Errorf("ParseRange(%q) = %v, %v", in, r, ok)5355 }5456 }5557 }5658 59+func txt(s string) Value { return Value{Kind: Text, Str: s} }60+5761 func TestFormulas(t *testing.T) {5862 s := New()5963 for addr, in := range map[string]string{60- "A1": "10", "A2": "20", "A3": "30", "B1": "Name", "B2": `"right`,64+ "A1": "10", "A2": "20", "A3": "30", "B1": "Name", "B2": "'42", "B3": "TRUE",6165 } {6266 if err := s.Set(at(addr), in); err != nil {6367 t.Fatal(err)@@ -67,28 +71,47 @@ func TestFormulas(t *testing.T) {
6771 in string6872 want Value6973 }{70- {"+A1+A2*2", num(50)},71- {"(A1+A2)*2", num(60)},72- {"-2^2", num(-4)},74+ {"=A1+A2*2", num(50)},75+ {"=(A1+A2)*2", num(60)},76+ {"=-2^2", num(-4)},7377 {"=A1/4", num(2.5)},74- {"@SUM(A1..A3)", num(60)},75- {"@sum(A1:A3,5)", num(65)},76- {"@AVG(A1..A3)", num(20)},77- {"@COUNT(A1..B3)", num(5)},78- {"@MIN(A1..A3)", num(10)},79- {"@MAX(A1..A3)", num(30)},80- {"@ROUND(@PI,2)", num(3.14)},81- {"@MOD(7;3)", num(1)},82- {"@IF(A1>5,1,2)", num(1)},83- {"+A1>5#AND#A2<10", num(0)},84- {"#NOT#A1=10", num(0)},85- {"+B1+1", num(1)}, // labels count as zero86- {`+B1&" X"`, Value{Kind: Label, Str: "Name X"}},87- {"+A1/0", errValue},88- {"@SQRT(-1)", errValue},89- {"@NA", naValue},90- {"+@NA+1", naValue},91- {"1.5E3", num(1500)},78+ {"+A1*2", num(20)}, // Sheets accepts a leading +79+ {"=SUM(A1:A3)", num(60)},80+ {"=sum(A1:A3, 5)", num(65)},81+ {"=@SUM(A1..A3)", num(60)}, // 1-2-3 function syntax still parses82+ {"=AVERAGE(A1:B3)", num(20)},83+ {"=AVG(A1:A3)", num(20)},84+ {"=COUNT(A1:B3)", num(3)}, // numbers only85+ {"=COUNTA(A1:B3)", num(6)}, // anything non-empty86+ {"=MIN(A1:A3)", num(10)},87+ {"=MAX(A1:A3)", num(30)},88+ {"=ROUND(PI(), 2)", num(3.14)},89+ {"=ROUND(2.5)", num(3)},90+ {"=MOD(-7, 3)", num(2)},91+ {"=INT(-1.5)", num(-2)},92+ {`=IF(A1>5, "big", "small")`, txt("big")},93+ {"=IF(A1>50, 1)", boolean(false)},94+ {"=A1>5", boolean(true)},95+ {"=AND(A1>5, A2<10)", boolean(false)},96+ {"=OR(B3, FALSE)", boolean(true)},97+ {"=NOT(A1=10)", boolean(false)},98+ {"=B2*2", num(84)}, // numeric text coerces99+ {"=B1+1", ErrValue}, // other text doesn't100+ {"=SUM(B1)", ErrValue}, // nor as a direct argument101+ {"=B3+1", num(2)}, // TRUE is 1102+ {`=B1&" X"`, txt("Name X")},103+ {`="say ""hi"""`, txt(`say "hi"`)},104+ {"=A1&B3", txt("10TRUE")},105+ {"=A1/0", ErrDiv0},106+ {"=SQRT(-1)", ErrNum},107+ {"=NA()", ErrNA},108+ {"=NA()+1", ErrNA},109+ {"=IFERROR(A1/0, 0)", num(0)},110+ {"=Z99", Value{}},111+ {"=10%", num(0.1)},112+ {"=A1*50%", num(5)},113+ {"=profit", ErrName},114+ {"=1.5E3", num(1500)},92115 }93116 for _, tt := range tests {94117 if err := s.Set(at("C1"), tt.in); err != nil {@@ -101,33 +124,66 @@ func TestFormulas(t *testing.T) {
101124 }102125 }103126 104-func TestParseErrors(t *testing.T) {105- for _, in := range []string{"+A1+", "@SUM(A1", "@NOPE(1)", "+ZZ1", "(1", "+A1..", "1 2", `+"abc`, "#AND#"} {106- var pe *ParseError107- if err := New().Set(at("A1"), in); !errors.As(err, &pe) {108- t.Errorf("Set(%q) error = %v, want ParseError", in, err)127+func TestEntryKinds(t *testing.T) {128+ tests := []struct {129+ in string130+ want Value131+ formula bool132+ }{133+ {"42", num(42), false},134+ {"-3.5", num(-3.5), false},135+ {"$1,200.50", num(1200.5), false},136+ {"12%", num(0.12), false},137+ {"1e3", num(1000), false},138+ {"true", boolean(true), false},139+ {"Rent", txt("Rent"), false},140+ {"'42", txt("42"), false},141+ {"1,23", txt("1,23"), false}, // bad grouping is text142+ {"-hello", Value{Kind: Error, Str: "#NAME?"}, true},143+ {"A1+1", txt("A1+1"), false}, // no = means text144+ {"=1+1", num(2), true},145+ {"-A1", num(0), true},146+ }147+ for _, tt := range tests {148+ s := New()149+ if err := s.Set(at("A2"), tt.in); err != nil {150+ t.Errorf("Set(%q): %v", tt.in, err)151+ continue152+ }153+ c := s.Cell(at("A2"))154+ if c.Value != tt.want || c.IsFormula() != tt.formula || c.Input != tt.in {155+ t.Errorf("%q = %+v formula=%v input=%q", tt.in, c.Value, c.IsFormula(), c.Input)109156 }110157 }111158 }112159 113-func TestLabels(t *testing.T) {114- s := New()115- s.Set(at("A1"), "Sales")116- s.Set(at("A2"), "^Mid")117- if c := s.Cell(at("A1")); c.Input != "'Sales" || c.Align() != '\'' || c.Value.Str != "Sales" {118- t.Errorf("A1 = %+v", c)160+func TestParseErrors(t *testing.T) {161+ tests := []struct{ in, msg string }{162+ {"=A1+", "Formula is incomplete"},163+ {"=SUM(A1", "Expected , or ) in SUM"},164+ {"=NOPE(1)", "Unknown function NOPE"},165+ {"=(1", "Missing )"},166+ {"=A1:", "Expected a cell after :"},167+ {"=1 2", "Unexpected 2"},168+ {`="abc`, "Missing closing quote"},169+ {"=ROUND()", "Wrong number of arguments to ROUND(value, [places])"},170+ {"=PI(1)", "Wrong number of arguments to PI()"},119171 }120- if c := s.Cell(at("A2")); c.Align() != '^' || c.Value.Str != "Mid" {121- t.Errorf("A2 = %+v", c)172+ for _, tt := range tests {173+ var pe *ParseError174+ err := New().Set(at("A1"), tt.in)175+ if !errors.As(err, &pe) || pe.Msg != tt.msg {176+ t.Errorf("Set(%q) error = %v, want %q", tt.in, err, tt.msg)177+ }122178 }123179 }124180 125181 func TestRecalcPropagates(t *testing.T) {126182 s := New()127183 s.Set(at("A1"), "1")128- s.Set(at("A2"), "+A1*2")129- s.Set(at("A3"), "+A2+A1")130- s.Set(at("B1"), "@SUM(A1..A10)")184+ s.Set(at("A2"), "=A1*2")185+ s.Set(at("A3"), "=A2+A1")186+ s.Set(at("B1"), "=SUM(A1:A10)")131187 132188 s.Set(at("A1"), "5")133189 if got := s.Value(at("A3")).Num; got != 15 {@@ -157,16 +213,16 @@ func TestRecalcPropagates(t *testing.T) {
157213 158214 func TestCircular(t *testing.T) {159215 s := New()160- s.Set(at("A1"), "+B1+1")161- s.Set(at("B1"), "+A1+1")162- if !s.Circular || s.Value(at("A1")) != errValue || s.Value(at("B1")) != errValue {216+ s.Set(at("A1"), "=B1+1")217+ s.Set(at("B1"), "=A1+1")218+ if !s.Circular || s.Value(at("A1")) != ErrRef || s.Value(at("B1")) != ErrRef {163219 t.Errorf("circular: %v %+v %+v", s.Circular, s.Value(at("A1")), s.Value(at("B1")))164220 }165221 s.Set(at("B1"), "3")166222 if s.Circular || s.Value(at("A1")).Num != 4 {167223 t.Errorf("after break: %v %+v", s.Circular, s.Value(at("A1")))168224 }169- s.Set(at("C1"), "@SUM(C1..C2)")225+ s.Set(at("C1"), "=SUM(C1:C2)")170226 if !s.Circular {171227 t.Error("self-referencing range not flagged")172228 }@@ -174,33 +230,52 @@ func TestCircular(t *testing.T) {
174230 175231 func TestFormatValue(t *testing.T) {176232 tests := []struct {177- v float64233+ v Value178234 width int179235 want string180236 }{181- {42, 9, " 42 "},182- {-3.5, 9, " -3.5 "},183- {1.0 / 3, 9, "0.333333 "},184- {9.9999, 4, " 10 "},185- {123456789012, 9, "1.23E+11 "},186- {0.000000001, 9, "1.00E-09 "},187- {0, 3, " 0 "},188- {123456, 3, "** "},237+ {num(42), 10, " 42 "},238+ {num(-3.5), 10, " -3.5 "},239+ {num(1.0 / 3), 10, "0.3333333 "},240+ {num(9.9999), 4, " 10 "},241+ {num(123456789012), 10, "1.235E+11 "},242+ {num(0.000000001), 10, "1.000E-09 "},243+ {num(0), 3, " 0 "},244+ {num(123456), 3, "## "},245+ {boolean(true), 10, " TRUE "},246+ {ErrDiv0, 10, " #DIV/0! "},189247 }190248 for _, tt := range tests {191- if got := FormatValue(num(tt.v), tt.width); got != tt.want {249+ if got := FormatValue(tt.v, tt.width); got != tt.want {192250 t.Errorf("FormatValue(%v, %d) = %q, want %q", tt.v, tt.width, got, tt.want)193251 }194252 }195253 }196254 255+func TestParseNumber(t *testing.T) {256+ for in, want := range map[string]float64{257+ "0": 0, "12": 12, "-12.5": -12.5, "+3": 3, ".5": 0.5, "1,234": 1234,258+ "1,234,567.89": 1234567.89, "$99": 99, "-$5": -5, "50%": 0.5, "2.5e2": 250,259+ } {260+ if got, ok := ParseNumber(in); !ok || got != want {261+ t.Errorf("ParseNumber(%q) = %v, %v", in, got, ok)262+ }263+ }264+ for _, in := range []string{"", "abc", "1,2", "12,34", "$", "%", "1.2.3", "--1", "1 2"} {265+ if _, ok := ParseNumber(in); ok {266+ t.Errorf("ParseNumber(%q) accepted", in)267+ }268+ }269+}270+197271 func FuzzParse(f *testing.F) {198- for _, s := range []string{"+A1*2", "@SUM(A1..B3)", "@IF(A1>2#AND#B1,1,@NA)", `"a"&"b"`, "=1.5E-3^2"} {272+ for _, s := range []string{"=A1*2", "=SUM(A1:B3)", "=IF(AND(A1>2,B1),1,NA())", `="a"&"b"`, "=1.5E-3^2", "$1,200", "12%", "@SUM(A1..B3)"} {199273 f.Add(s)200274 }201275 f.Fuzz(func(t *testing.T, in string) {202276 s := New()203- if err := s.Set(Addr{}, in); err == nil && in != "" && s.Cell(Addr{}) == nil {277+ printable := strings.ContainsFunc(in, func(r rune) bool { return !unicode.IsControl(r) })278+ if err := s.Set(Addr{}, in); err == nil && printable && s.Cell(Addr{}) == nil {204279 t.Fatalf("Set(%q) succeeded but cell is blank", in)205280 }206281 })@@ -209,9 +284,9 @@ func FuzzParse(f *testing.F) {
209284 func TestFileRoundTrip(t *testing.T) {210285 s := New()211286 s.Set(at("A1"), "Total")212- s.Set(at("B1"), "@SUM(B2..B3)")287+ s.Set(at("B1"), "=SUM(B2:B3)")213288 s.Set(at("B2"), "2")214- s.Set(at("B3"), "+B2*10")289+ s.Set(at("B3"), "=B2*10")215290 s.SetColWidth(0, 14)216291 217292 var buf bytes.Buffer@@ -225,7 +300,7 @@ func TestFileRoundTrip(t *testing.T) {
225300 if v := got.Value(at("B1")).Num; v != 22 {226301 t.Errorf("B1 = %v, want 22", v)227302 }228- if got.Cell(at("A1")).Input != "'Total" || got.ColWidth(0) != 14 {303+ if got.Cell(at("A1")).Input != "Total" || got.ColWidth(0) != 14 {229304 t.Errorf("A1 = %q, width %d", got.Cell(at("A1")).Input, got.ColWidth(0))230305 }231306 }@@ -233,7 +308,7 @@ func TestFileRoundTrip(t *testing.T) {
233308 func TestStripsControlChars(t *testing.T) {234309 s := New()235310 s.Set(at("A1"), "evil\x1b]52;c;aGk=\x07label")236- if got := s.Cell(at("A1")).Input; got != "'evil]52;c;aGk=label" {311+ if got := s.Cell(at("A1")).Input; got != "evil]52;c;aGk=label" {237312 t.Errorf("Input = %q", got)238313 }239314 }