| 1 | package store |
| 2 | |
| 3 | import ( |
| 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. |
| 25 | func 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. |
| 42 | func 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. |
| 64 | func 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. |
| 93 | func 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 | |