cat / 012
clone

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

Stream CSV and TSV exports to the file row by row, and read UTF-16 as a stream

Dami committed 0040bb1
Exports built every row as strings, then the whole file in memory,
before writing it: 104 MB allocated for a full 8192 x 256 sheet, now
30 MB. UTF-16 input was read whole and decoded twice; it now goes
through x/text's decoder like Windows-1252. All exports share one
atomic write-then-rename helper.
commit
0040bb1c716ae3f7daff9e90e3e670f86f098fd8
parent
b5ee00dd062dc2d9079704ab1e0648f79302acc8
author
Dami <dami.etoile@gmail.com>
5 files changed, +77 -54
Minternal/fileio/csv.go63++++++++++++------------------
Minternal/fileio/csv_test.go7+++
Minternal/fileio/fileio.go28+++++++++++++
Minternal/fileio/snapshot.go26+++++++-----
Minternal/fileio/xlsx.go7+--
M internal/fileio/csv.go +25 -38
@@ -9,10 +9,10 @@ import (
99 "fmt"1010 "io"1111 "os"12- "unicode/utf16"1312 "unicode/utf8"1413 1514 "golang.org/x/text/encoding/charmap"15+ "golang.org/x/text/encoding/unicode"1616 1717 "github.com/FelineStateMachine/012/internal/sheet"1818 )
@@ -107,21 +107,12 @@ func decode(r *bufio.Reader) (io.Reader, string, error) {
107107 r.Discard(3)108108 return r, "UTF-8", nil109109 case bytes.HasPrefix(head, []byte{0xFF, 0xFE}), bytes.HasPrefix(head, []byte{0xFE, 0xFF}):110- big := head[0] == 0xFE111- r.Discard(2)112- all, err := io.ReadAll(r)113- if err != nil {114- return nil, "", err115- }116- u := make([]uint16, len(all)/2)117- for i := range u {118- if big {119- u[i] = uint16(all[2*i])<<8 | uint16(all[2*i+1])120- } else {121- u[i] = uint16(all[2*i+1])<<8 | uint16(all[2*i])122- }110+ order := unicode.LittleEndian111+ if head[0] == 0xFE {112+ order = unicode.BigEndian123113 }124- return bytes.NewReader([]byte(string(utf16.Decode(u)))), "UTF-16", nil114+ r.Discard(2)115+ return unicode.UTF16(order, unicode.IgnoreBOM).NewDecoder().Reader(r), "UTF-16", nil125116 }126117 // A sample cut mid-character is still UTF-8.127118 check := head
@@ -258,14 +249,24 @@ func delimiterName(r rune) string {
258249 259250 // exportDelimited writes the snapshot's displayed values, as Sheets'260251 // Download as CSV does: formulas become their results, formats show.252+// Rows are written as they are formatted, never all held at once.261253 func exportDelimited(name string, k Kind, snap *Snapshot) (*ExportResult, error) {262- var buf bytes.Buffer263- w := csv.NewWriter(&buf)264- if k == TSV {265- w.Comma = '\t'266- }267- rows := snap.rows()268- if err := w.WriteAll(rows); err != nil {254+ rows := 0255+ err := writeFile(name, func(out io.Writer) error {256+ w := csv.NewWriter(out)257+ if k == TSV {258+ w.Comma = '\t'259+ }260+ for line := range snap.textRows() {261+ if err := w.Write(line); err != nil {262+ return err263+ }264+ rows++265+ }266+ w.Flush()267+ return w.Error()268+ })269+ if err != nil {269270 return nil, err270271 }271272 formulas := 0
@@ -274,23 +275,9 @@ func exportDelimited(name string, k Kind, snap *Snapshot) (*ExportResult, error)
274275 formulas++275276 }276277 }277- res := &ExportResult{Rows: len(rows)}278+ res := &ExportResult{Rows: rows}278279 if formulas > 0 {279280 res.Notes = append(res.Notes, count(formulas, "formula", "formulas")+" saved as values")280281 }281- return res, writeFile(name, buf.Bytes())282-}283-284-// writeFile writes data atomically: to a temporary file first, then285-// renamed over the target.286-func writeFile(name string, data []byte) error {287- tmp := name + ".tmp"288- if err := os.WriteFile(tmp, data, 0o644); err != nil {289- return err290- }291- if err := os.Rename(tmp, name); err != nil {292- os.Remove(tmp)293- return err294- }295- return nil282+ return res, nil296283 }
M internal/fileio/csv_test.go +7 -0
M internal/fileio/fileio.go +28 -0
M internal/fileio/snapshot.go +15 -11
M internal/fileio/xlsx.go +2 -5