1// Package value holds what a cell computes to and how it is shown: the2// Value of a cell, its number Format, and the parsing of typed text into3// numbers, dates and times. It sits below the function library4// (internal/functions) and the engine (internal/sheet), which both work5// in these terms; the engine's API names them too, so callers see one6// package.7package value89import (10 "cmp"11 "math"12 "strconv"13 "strings"1415 "src.rest/cat/012/internal/numfmt"16)1718// Kind is the type of a computed cell value.19type Kind int2021const (22 Empty Kind = iota23 Number24 Text25 Bool26 Error27 // Array marks an array or LAMBDA while a formula is evaluated28 // (internal/functions keeps them; Num says which). No cell ever holds29 // one: a formula computing an array shows its first value and spills30 // the rest.31 Array32)3334// Value is the computed contents of a cell.35type Value struct {36 Kind Kind37 Num float64 // numbers, and booleans as 1 or 038 Str string // text, or the error code such as #DIV/0!39}4041// String renders a value the way a cell shows it in General format.42func (v Value) String() string {43 switch v.Kind {44 case Number:45 return numfmt.General(v.Num)46 case Bool:47 if v.Num != 0 {48 return "TRUE"49 }50 return "FALSE"51 }52 return v.Str53}5455// Error values, using Google Sheets codes.56var (57 ErrDiv0 = Value{Kind: Error, Str: "#DIV/0!"}58 ErrValue = Value{Kind: Error, Str: "#VALUE!"}59 ErrName = Value{Kind: Error, Str: "#NAME?"}60 ErrNA = Value{Kind: Error, Str: "#N/A"}61 ErrNum = Value{Kind: Error, Str: "#NUM!"}62 ErrRef = Value{Kind: Error, Str: "#REF!"} // also circular references63)6465// Num is the number v, or #NUM! for NaN and infinities.66func Num(v float64) Value {67 if math.IsNaN(v) || math.IsInf(v, 0) {68 return ErrNum69 }70 return Value{Kind: Number, Num: v}71}7273// Boolean is TRUE or FALSE.74func Boolean(b bool) Value {75 if b {76 return Value{Kind: Bool, Num: 1}77 }78 return Value{Kind: Bool}79}8081// Str is the text s.82func Str(s string) Value { return Value{Kind: Text, Str: s} }8384// ErrOf returns a pointer to a copy of v, for the *Value error results of85// argument helpers. Taking &v of a parameter directly would move it to86// the heap on every call, erroneous or not: one allocation per cell read87// by SUM over a range.88func ErrOf(v Value) *Value { return &v }8990// ToNum coerces v for arithmetic as Sheets does: blanks are 0, booleans91// 1 or 0, numeric text (including dates such as "2026-09-26") is its92// number, other text is #VALUE!.93func ToNum(v Value) (float64, *Value) {94 switch v.Kind {95 case Empty:96 return 0, nil97 case Number, Bool:98 return v.Num, nil99 case Text:100 if n, _, ok := ParseValue(v.Str); ok {101 return n, nil102 }103 return 0, &ErrValue104 }105 return 0, ErrOf(v)106}107108// AsText converts a value for string concatenation.109func AsText(v Value) string {110 if v.Kind == Empty {111 return ""112 }113 return v.String()114}115116// compareText orders text ignoring case, as upper case: byte by byte117// while both are ASCII, which most text is, so comparing a column of it118// allocates nothing.119func compareText(a, b string) int {120 for i := 0; i < len(a) && i < len(b); i++ {121 x, y := a[i], b[i]122 if x >= utf8RuneSelf || y >= utf8RuneSelf {123 return strings.Compare(strings.ToUpper(a[i:]), strings.ToUpper(b[i:]))124 }125 if x >= 'a' && x <= 'z' {126 x -= 'a' - 'A'127 }128 if y >= 'a' && y <= 'z' {129 y -= 'a' - 'A'130 }131 if x != y {132 return cmp.Compare(x, y)133 }134 }135 return cmp.Compare(len(a), len(b))136}137138// utf8RuneSelf is where multi-byte UTF-8 starts.139const utf8RuneSelf = 0x80140141// Compare orders values like Sheets: numbers < text < booleans, text is142// case-insensitive, and a blank equals 0 or "".143func Compare(l, r Value) int {144 rank := func(v Value) int {145 switch v.Kind {146 case Text:147 return 1148 case Bool:149 return 2150 }151 return 0152 }153 if l.Kind == Empty && r.Kind == Text {154 l = Value{Kind: Text}155 }156 if r.Kind == Empty && l.Kind == Text {157 r = Value{Kind: Text}158 }159 if d := rank(l) - rank(r); d != 0 {160 return d161 }162 if l.Kind == Text {163 return compareText(l.Str, r.Str)164 }165 switch {166 case l.Num < r.Num:167 return -1168 case l.Num > r.Num:169 return 1170 }171 return 0172}173174// CompareShown is Compare as the comparison operators and criteria see175// numbers: at the 15 significant digits a cell shows, as Sheets and176// Excel do, so =0.1+0.2=0.3 is TRUE though the sum is177// 0.30000000000000004. Sorting and lookups use Compare, which is exact178// and so orders values consistently.179func CompareShown(l, r Value) int {180 d := Compare(l, r)181 if d != 0 && l.Kind == Number && r.Kind == Number && SameShown(l.Num, r.Num) {182 return 0183 }184 return d185}186187// SameShown reports whether a and b agree to 15 significant digits.188func SameShown(a, b float64) bool {189 switch {190 case a == b:191 return true192 case a == 0 || b == 0 || (a < 0) != (b < 0):193 return false194 case math.Abs(a-b) > 1e-14*math.Max(math.Abs(a), math.Abs(b)):195 return false // differ before the 15th digit: the common case, at once196 }197 return strconv.FormatFloat(a, 'e', 14, 64) == strconv.FormatFloat(b, 'e', 14, 64)198}