oficina / sheets Public

Clone
git clone https://amadan.net/oficina/sheets

Plain git — no account needed to clone.

Download

Download this file

1package store
2
3import (
4 "encoding/binary"
5 "errors"
6 "fmt"
7 "math"
8 "strconv"
9 "strings"
10
11 "amadan.net/oficina/sheets/internal/sheets/calc"
12)
13
14// contentNode classifies a cell's raw input the way the grid does: empty
15// text is a blank cell, text starting with "=" is a formula (parsed with
16// the leading "=" stripped, matching calc.Parse's own contract), and
17// anything else is a literal — a number, a bare TRUE/FALSE, or otherwise
18// text.
19//
20// Every non-blank result is a calc.Node, so Set and Open register literal
21// and formula cells with the graph the same way (Graph.SetFormula),
22// letting Graph's own caching and dependency tracking do the work for
23// both instead of the handle keeping a second, parallel path for values
24// that happen not to reference anything.
25func contentNode(raw string) (node calc.Node, blank bool, err error) {
26 if raw == "" {
27 return nil, true, nil
28 }
29 if strings.HasPrefix(raw, "=") {
30 n, perr := calc.Parse(raw[1:])
31 if perr != nil {
32 return nil, false, perr
33 }
34 return n, false, nil
35 }
36 return literalNode(raw), false, nil
37}
38
39// literalNode parses raw the way a spreadsheet's own input coercion
40// would: a number if it parses as one, TRUE/FALSE (case-insensitive) as
41// a boolean, otherwise verbatim text.
42func literalNode(raw string) calc.Node {
43 if n, err := strconv.ParseFloat(raw, 64); err == nil {
44 return &calc.Num{V: n}
45 }
46 if strings.EqualFold(raw, "TRUE") {
47 return &calc.Bool{V: true}
48 }
49 if strings.EqualFold(raw, "FALSE") {
50 return &calc.Bool{V: false}
51 }
52 return &calc.Str{V: raw}
53}
54
55// encodeValue and decodeValue are CachedSealed's plaintext codec — sealed
56// under purpose "cell.cached" before this ever reaches disk, never
57// written or read anywhere else. Cell values are IEEE doubles end to end
58// (spec §5): a number is stored as its raw 8-byte bit pattern rather than
59// a decimal reformatting, so it survives round-trip even for the values a
60// text codec cannot represent — encoding/json, for one, rejects NaN and
61// Inf outright, and this evaluator has at least one path (POWER with a
62// fractional exponent on a negative base) that can produce a KindNum NaN
63// with no earlier guard against it reaching this codec.
64func encodeValue(v calc.Value) []byte {
65 switch v.Kind {
66 case calc.KindNum:
67 b := make([]byte, 9)
68 b[0] = byte(calc.KindNum)
69 binary.BigEndian.PutUint64(b[1:], math.Float64bits(v.Num))
70 return b
71 case calc.KindStr:
72 b := make([]byte, 5+len(v.Str))
73 b[0] = byte(calc.KindStr)
74 binary.BigEndian.PutUint32(b[1:5], uint32(len(v.Str)))
75 copy(b[5:], v.Str)
76 return b
77 case calc.KindBool:
78 bit := byte(0)
79 if v.Bool {
80 bit = 1
81 }
82 return []byte{byte(calc.KindBool), bit}
83 case calc.KindErr:
84 return []byte{byte(calc.KindErr), byte(v.Err)}
85 default: // calc.KindEmpty
86 return []byte{byte(calc.KindEmpty)}
87 }
88}
89
90// decodeValue is encodeValue's inverse. An empty byte slice — the shape a
91// freshly-created, never-recomputed cell's CachedSealed would seal — also
92// decodes to KindEmpty rather than erroring.
93func decodeValue(b []byte) (calc.Value, error) {
94 if len(b) == 0 {
95 return calc.Value{}, nil
96 }
97 kind := calc.ValueKind(b[0])
98 rest := b[1:]
99 switch kind {
100 case calc.KindEmpty:
101 return calc.Value{}, nil
102 case calc.KindNum:
103 if len(rest) < 8 {
104 return calc.Value{}, errors.New("store: truncated cached number")
105 }
106 return calc.Value{Kind: calc.KindNum, Num: math.Float64frombits(binary.BigEndian.Uint64(rest))}, nil
107 case calc.KindStr:
108 if len(rest) < 4 {
109 return calc.Value{}, errors.New("store: truncated cached string length")
110 }
111 n := binary.BigEndian.Uint32(rest[:4])
112 rest = rest[4:]
113 if uint32(len(rest)) < n {
114 return calc.Value{}, errors.New("store: truncated cached string")
115 }
116 return calc.Value{Kind: calc.KindStr, Str: string(rest[:n])}, nil
117 case calc.KindBool:
118 if len(rest) < 1 {
119 return calc.Value{}, errors.New("store: truncated cached bool")
120 }
121 return calc.Value{Kind: calc.KindBool, Bool: rest[0] != 0}, nil
122 case calc.KindErr:
123 if len(rest) < 1 {
124 return calc.Value{}, errors.New("store: truncated cached error")
125 }
126 return calc.Value{Kind: calc.KindErr, Err: calc.ErrKind(rest[0])}, nil
127 default:
128 return calc.Value{}, fmt.Errorf("store: unknown cached value kind %d", kind)
129 }
130}
131