oficina / sheets Public

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

Plain git — no account needed to clone.

Download

Download this file

1package xlsx
2
3import (
4 "fmt"
5 "io"
6 "strings"
7
8 "github.com/xuri/excelize/v2"
9
10 "amadan.net/oficina/sheets/internal/sheets/calc"
11 "amadan.net/oficina/sheets/internal/sheets/store"
12)
13
14// Export writes h's tab out as an XLSX workbook with one worksheet.
15//
16// A formula cell is re-emitted as a formula (its exact original text,
17// via Handle.RawInput), not flattened to its computed value — a person
18// who imported a workbook and re-exported it unchanged must still be
19// able to edit the formula, not just see a frozen number. No cached
20// result is written alongside it (excelize.SetCellFormula does not carry
21// one — verified directly against the library); Excel and LibreOffice
22// both recalculate a formula that has no cached value on open, which is
23// the same shape any programmatically generated formula file takes.
24func Export(h *store.Handle, w io.Writer) error {
25 f := excelize.NewFile()
26 defer f.Close()
27 sheetName := f.GetSheetList()[0]
28
29 rowPos := map[uint64]int{}
30 for _, r := range h.GridRows() {
31 rowPos[r.ID] = r.Pos
32 }
33 colPos := map[uint64]int{}
34 for _, c := range h.GridCols() {
35 colPos[c.ID] = c.Pos
36 }
37
38 cells, err := h.Cells()
39 if err != nil {
40 return fmt.Errorf("xlsx: %w", err)
41 }
42 for _, cell := range cells {
43 rp, ok := rowPos[cell.RowID]
44 if !ok {
45 continue // defensive: every cell's row was loaded by the same Open
46 }
47 cp, ok := colPos[cell.ColID]
48 if !ok {
49 continue
50 }
51 if err := exportCell(f, sheetName, h, cell.RowID, cell.ColID, rp, cp); err != nil {
52 return err
53 }
54 }
55
56 // Layout, so a round trip does not quietly return every column to
57 // the default width — the same obligation Format.FillHex carries.
58 if err := exportSizes(f, sheetName, h); err != nil {
59 return fmt.Errorf("xlsx: %w", err)
60 }
61
62 if err := f.Write(w); err != nil {
63 return fmt.Errorf("xlsx: write workbook: %w", err)
64 }
65 return nil
66}
67
68// exportCell writes one cell's content (if any) and format (if any) at
69// its spreadsheet position.
70func exportCell(f *excelize.File, sheetName string, h *store.Handle, rowID, colID uint64, rowPos, colPos int) error {
71 cellRef, err := excelize.CoordinatesToCellName(colPos+1, rowPos+1)
72 if err != nil {
73 return fmt.Errorf("xlsx: cell %s: %w", calc.A1(rowPos, colPos), err)
74 }
75
76 if raw, ok := h.RawInput(rowID, colID); ok && raw != "" {
77 if strings.HasPrefix(raw, "=") {
78 if err := f.SetCellFormula(sheetName, cellRef, raw[1:]); err != nil {
79 return fmt.Errorf("xlsx: write formula at %s: %w", calc.A1(rowPos, colPos), err)
80 }
81 } else if err := writeValue(f, sheetName, cellRef, h.Value(rowID, colID)); err != nil {
82 return fmt.Errorf("xlsx: write value at %s: %w", calc.A1(rowPos, colPos), err)
83 }
84 }
85
86 if format := h.Format(rowID, colID); format != nil {
87 if style, ok := styleFromFormat(format); ok {
88 styleID, err := f.NewStyle(&style)
89 if err != nil {
90 return fmt.Errorf("xlsx: build style at %s: %w", calc.A1(rowPos, colPos), err)
91 }
92 if err := f.SetCellStyle(sheetName, cellRef, cellRef, styleID); err != nil {
93 return fmt.Errorf("xlsx: apply style at %s: %w", calc.A1(rowPos, colPos), err)
94 }
95 }
96 }
97 return nil
98}
99
100// writeValue writes a literal cell's already-computed value. Cell values
101// are IEEE doubles end to end (spec §5): SetCellFloat's precision -1
102// means "the fewest digits that still round-trip exactly", the same
103// contract calc.Render leans on for the same reason.
104func writeValue(f *excelize.File, sheetName, cellRef string, v calc.Value) error {
105 switch v.Kind {
106 case calc.KindNum:
107 return f.SetCellFloat(sheetName, cellRef, v.Num, -1, 64)
108 case calc.KindStr:
109 return f.SetCellStr(sheetName, cellRef, v.Str)
110 case calc.KindBool:
111 return f.SetCellBool(sheetName, cellRef, v.Bool)
112 default:
113 // KindEmpty/KindErr never reach here for a literal (non-formula)
114 // cell: literalNode (store/handle_codec.go) never produces
115 // either from raw text. A formula cell's error result is
116 // preserved by re-emitting the formula above, not by writing a
117 // value at all.
118 return nil
119 }
120}
121