cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

main
012 / internal / diff / raw.go
raw.go 5.8K
1package diff23import (4 "bytes"5 "encoding/json"6 "fmt"7 "maps"8 "slices"9 "strconv"10 "strings"1112 "src.rest/cat/012/internal/sheet"13)1415// A .012 file is JSON whose sheets hold their cells as a map by address16// and their other fields (widths, rules, regions) beside them; see17// docs/files/format.md. Diffing and merging read it as that JSON, field18// by field, so every field is compared and kept, including ones this19// package knows nothing about, and the workbook the fields make is read20// with the engine only for the values formulas compute.2122// rawBook is a file's fields: the workbook's, and each sheet's.23type rawBook struct {24 top map[string]json.RawMessage // the workbook's fields, compact, without "sheets"25 sheets []*rawSheet26}2728// rawSheet is one sheet's fields, compact.29type rawSheet struct {30 name string31 fields map[string]json.RawMessage // everything but name and cells32 cells map[string]json.RawMessage // by address33}3435// bookKeys are the workbook's fields; in a file of one sheet (versions36// 1 to 3) every other top-level field is the sheet's.37var bookKeys = map[string]bool{"version": true, "names": true, "active": true, "arithmetic": true,38 "locale": true, "macroOrigin": true, "shellHistory": true, "macros": true, "sheets": true}3940// parseRaw reads a file's fields. An empty file (git's stand-in for a41// file that isn't there) is a workbook without sheets.42func parseRaw(data []byte) (*rawBook, error) {43 b := &rawBook{top: map[string]json.RawMessage{}}44 if len(bytes.TrimSpace(data)) == 0 {45 return b, nil46 }47 var top map[string]json.RawMessage48 if err := json.Unmarshal(data, &top); err != nil {49 return nil, err50 }51 if raw, ok := top["sheets"]; ok {52 var sheets []map[string]json.RawMessage53 if err := json.Unmarshal(raw, &sheets); err != nil {54 return nil, fmt.Errorf("sheets: %w", err)55 }56 for _, fields := range sheets {57 s, err := parseSheet(fields)58 if err != nil {59 return nil, err60 }61 b.sheets = append(b.sheets, s)62 }63 } else {64 fields := map[string]json.RawMessage{}65 for k, v := range top {66 if !bookKeys[k] {67 fields[k] = v68 }69 }70 s, err := parseSheet(fields)71 if err != nil {72 return nil, err73 }74 if s.name == "" {75 s.name = "Sheet1"76 }77 b.sheets = []*rawSheet{s}78 if err := qualifyNames(top, s.name); err != nil {79 return nil, err80 }81 }82 for k, v := range top {83 if bookKeys[k] && k != "sheets" {84 b.top[k] = compact(v)85 }86 }87 return b, nil88}8990// parseSheet splits a sheet's fields into its name, cells and the rest.91func parseSheet(fields map[string]json.RawMessage) (*rawSheet, error) {92 s := &rawSheet{fields: map[string]json.RawMessage{}, cells: map[string]json.RawMessage{}}93 if raw, ok := fields["name"]; ok {94 if err := json.Unmarshal(raw, &s.name); err != nil {95 return nil, fmt.Errorf("sheet name: %w", err)96 }97 }98 if raw, ok := fields["cells"]; ok && string(raw) != "null" {99 if err := json.Unmarshal(raw, &s.cells); err != nil {100 return nil, fmt.Errorf("sheet %s: cells: %w", s.name, err)101 }102 }103 for k, v := range s.cells {104 s.cells[k] = compact(v)105 }106 for k, v := range fields {107 if k != "name" && k != "cells" {108 s.fields[k] = compact(v)109 }110 }111 return s, nil112}113114// qualifyNames writes a one-sheet file's named ranges with their sheet,115// as files of several sheets have them, so files of both shapes compare.116func qualifyNames(top map[string]json.RawMessage, sheetName string) error {117 raw, ok := top["names"]118 if !ok {119 return nil120 }121 var names map[string]string122 if err := json.Unmarshal(raw, &names); err != nil {123 return fmt.Errorf("names: %w", err)124 }125 for k, ref := range names {126 if ref != "#REF!" && !strings.Contains(ref, "!") {127 names[k] = sheet.QuoteSheet(sheetName) + "!" + ref128 }129 }130 top["names"], _ = json.Marshal(names)131 return nil132}133134// compact is raw without insignificant space, so equal values compare135// equal byte for byte.136func compact(raw json.RawMessage) json.RawMessage {137 var b bytes.Buffer138 if json.Compact(&b, raw) != nil {139 return raw140 }141 return b.Bytes()142}143144// marshal writes the fields as a file of several sheets, which the145// engine reads whatever the count; the engine's own writer then gives146// the file its usual shape.147func (b *rawBook) marshal() []byte {148 var out bytes.Buffer149 out.WriteString("{\n")150 version := json.RawMessage("4")151 if n, err := strconv.Atoi(string(b.top["version"])); err == nil && n > 4 {152 version = b.top["version"]153 }154 fmt.Fprintf(&out, "%q: %s", "version", version)155 for _, k := range slices.Sorted(maps.Keys(b.top)) {156 if k != "version" {157 fmt.Fprintf(&out, ",\n%q: %s", k, b.top[k])158 }159 }160 out.WriteString(",\n\"sheets\": [")161 for i, s := range b.sheets {162 if i > 0 {163 out.WriteByte(',')164 }165 name, _ := json.Marshal(s.name)166 fmt.Fprintf(&out, "\n{\"name\": %s", name)167 for _, k := range slices.Sorted(maps.Keys(s.fields)) {168 fmt.Fprintf(&out, ",\n%q: %s", k, s.fields[k])169 }170 out.WriteString(",\n\"cells\": {")171 for j, k := range slices.Sorted(maps.Keys(s.cells)) {172 if j > 0 {173 out.WriteByte(',')174 }175 fmt.Fprintf(&out, "\n%q: %s", k, s.cells[k])176 }177 out.WriteString("}}")178 }179 out.WriteString("]\n}\n")180 return out.Bytes()181}182183// normalize reads merged fields with the engine and writes them as 012184// saves a workbook, checking that they make one.185func normalize(b *rawBook) ([]byte, error) {186 w, err := sheet.ReadBook(bytes.NewReader(b.marshal()))187 if err != nil {188 return nil, err189 }190 var out bytes.Buffer191 if err := w.Write(&out); err != nil {192 return nil, err193 }194 return out.Bytes(), nil195}196197// equal reports whether two compact values are the same; nil is a198// field or cell that isn't there.199func equal(a, b json.RawMessage) bool {200 return (a == nil) == (b == nil) && bytes.Equal(a, b)201}202203// sheetIndex finds a sheet by name, ignoring case as the engine does.204func (b *rawBook) sheetIndex(name string) int {205 for i, s := range b.sheets {206 if strings.EqualFold(s.name, name) {207 return i208 }209 }210 return -1211}