- commit
- 97caaa7353423a0942e07d05227e1f05837def93
- parent
- cb58592dcc47269d9ee3e5416fa8b3aa695a8dd1
- author
- Dami <dami.etoile@gmail.com>
| M | docs/ | 2 | +- |
| M | internal/ | 2 | + |
| M | internal/ | 71 | ++++++++++++++++++++++++------ |
| M | internal/ | 29 | ++++++++++++ |
| M | internal/ | 20 | ++++++++ |
@@ -10,7 +10,7 @@ bar; Esc cancels. Saving an imported sheet asks whether to save it as a
1010 | Format | Import | Download |1111 |---|---|---|1212 | CSV, TSV | Delimiter (`,` `;` tab `\|`), UTF-8 BOM, UTF-16 and Windows-1252 detected; entries become numbers, dates, currency and percentages as if typed; formulas stay text | Values as shown, as Sheets' Download does |13-| Excel `.xlsx` | Every sheet, opening on the one Excel showed: values, formulas (references between sheets too), workbook named ranges, number formats, bold, italic, underline, strikethrough, alignment, column widths. Formulas 012 can't read (unknown functions) keep their values; Excel pivot tables come in as the values they showed | Every sheet, the same, with formulas in Excel's syntax and their results cached. JEV functions and `#AND#` save as values, and so do pivot tables: Excel gets the results, not a pivot |13+| Excel `.xlsx` | Every sheet, opening on the one Excel showed: values, formulas (references between sheets too), workbook named ranges, number formats, bold, italic, underline, strikethrough, alignment, column widths. Formulas 012 can't read (unknown functions) keep their values; Excel pivot tables come in as the values they showed | Every sheet, the same, with formulas in Excel's syntax and their results cached. JEV functions and `#AND#` save as values, and so do formulas naming a sheet that doesn't exist (their `#REF!`; Excel would refuse the reference) and pivot tables: Excel gets the results, not a pivot. The download's result counts the formulas saved as values, with an example |1414 | SQLite | Pick a table or view, or type a query; a header row names the columns | The sheet shown or the selection as a table, first row as column names; a table of that name is replaced |1515 | Parquet | Every column, with dates and timestamps; lists joined with commas | |1616 | Lotus 1-2-3 `.wk1`, `.wks` | Numbers, labels with their alignment, formats, column widths, formulas translated (references, operators, `@SUM`, `@AVG`, `@IF`, `@ROUND`, `@PMT` and 60 more) or kept as values | |@@ -34,6 +34,7 @@ type SnapCell struct {
3434 Own sheet.Format // the cell's own format3535 Style sheet.Style3636 Formula bool37+ Sheets []string // the sheets a formula names, as written3738 }3839 3940 // Text is the cell as displayed, without a width limit.@@ -60,6 +61,7 @@ func Snap(s *sheet.Sheet, r sheet.Rect, name string) *Snapshot {
6061 Own: c.Format,6162 Style: c.Style,6263 Formula: c.IsFormula(),64+ Sheets: s.NamedSheets(a),6365 }6466 }6567 return snap@@ -10,6 +10,7 @@ import (
1010 1111 "github.com/xuri/excelize/v2"1212 13+ "github.com/FelineStateMachine/012/internal/formula"1314 "github.com/FelineStateMachine/012/internal/sheet"1415 )1516 @@ -230,7 +231,10 @@ func exportXLSX(_ context.Context, name string, snap *Snapshot, _ ExportOptions)
230231 if len(sheets) == 0 {231232 sheets = []*Snapshot{snap}232233 }233- w := &xlsxWriter{x: x, styleIDs: map[xlsxStyle]int{}, multi: len(sheets) > 1}234+ w := &xlsxWriter{x: x, styleIDs: map[xlsxStyle]int{}, multi: len(sheets) > 1, known: map[string]bool{}}235+ for _, sn := range sheets {236+ w.known[formula.SheetKey(sn.Name)] = true237+ }234238 res := &ExportResult{}235239 for i, sn := range sheets {236240 ws := sheetName(sn.Name)@@ -261,9 +265,13 @@ func exportXLSX(_ context.Context, name string, snap *Snapshot, _ ExportOptions)
261265 if err := x.SetCalcProps(&excelize.CalcPropsOptions{FullCalcOnLoad: &yes}); err != nil {262266 return nil, err263267 }264- if w.values > 0 {268+ if w.values.n > 0 {265269 res.Notes = append(res.Notes, fmt.Sprintf("%s with no Excel equivalent saved as values, e.g. %s",266- count(w.values, "formula", "formulas"), w.example))270+ count(w.values.n, "formula", "formulas"), w.values.example))271+ }272+ if w.missing.n > 0 {273+ res.Notes = append(res.Notes, fmt.Sprintf("%s naming a sheet that doesn't exist saved as values, e.g. %s (%s)",274+ count(w.missing.n, "formula", "formulas"), w.missing.example, sheet.QuoteSheet(w.missingSheet)))267275 }268276 return res, writeFile(name, func(w io.Writer) error { return x.Write(w) })269277 }@@ -272,9 +280,42 @@ func exportXLSX(_ context.Context, name string, snap *Snapshot, _ ExportOptions)
272280 type xlsxWriter struct {273281 x *excelize.File274282 styleIDs map[xlsxStyle]int275- values int // formulas written as values276- example string // the first of them277- multi bool // examples name their sheet283+ multi bool // examples name their sheet284+285+ // Formulas are written as their values when Excel has no equivalent,286+ // or when they name a sheet the file doesn't have, which Excel would287+ // refuse.288+ values valueCount289+ missing valueCount290+ missingSheet string // the sheet missing's example names291+ known map[string]bool // keys of the sheets written292+}293+294+// valueCount counts formulas written as values, keeping the first one's295+// address as an example.296+type valueCount struct {297+ n int298+ example string299+}300+301+func (v *valueCount) add(multi bool, ws string, a sheet.Addr) {302+ v.n++303+ if v.example == "" {304+ v.example = a.String()305+ if multi {306+ v.example = sheet.QuoteSheet(ws) + "!" + a.String()307+ }308+ }309+}310+311+// unknownSheet returns a sheet c's formula names that isn't written.312+func (w *xlsxWriter) unknownSheet(c SnapCell) (string, bool) {313+ for _, name := range c.Sheets {314+ if !w.known[formula.SheetKey(name)] {315+ return name, true316+ }317+ }318+ return "", false278319 }279320 280321 func (w *xlsxWriter) styleFor(f sheet.Format, st sheet.Style) (int, error) {@@ -335,7 +376,8 @@ func (w *xlsxWriter) sheet(ws string, snap *Snapshot) (int, error) {
335376 }336377 337378 // cell is the snapshot cell c at a as excelize writes it. A formula with338-// no Excel equivalent is written as its value, and counted.379+// no Excel equivalent, or naming a sheet that isn't written, is written380+// as its value, and counted.339381 func (w *xlsxWriter) cell(ws string, a sheet.Addr, c SnapCell) (excelize.Cell, error) {340382 // A formula's inferred format is written too, so its result shows341383 // the same in Excel.@@ -350,17 +392,18 @@ func (w *xlsxWriter) cell(ws string, a sheet.Addr, c SnapCell) (excelize.Cell, e
350392 if !c.Formula {351393 return xc, nil352394 }395+ if name, ok := w.unknownSheet(c); ok {396+ if w.missing.n == 0 {397+ w.missingSheet = name398+ }399+ w.missing.add(w.multi, ws, a)400+ return xc, nil401+ }353402 if fx, ok := toExcelFormula(c.Input); ok {354403 xc.Formula = fx355404 return xc, nil356405 }357- w.values++358- if w.example == "" {359- w.example = a.String()360- if w.multi {361- w.example = sheet.QuoteSheet(ws) + "!" + a.String()362- }363- }406+ w.values.add(w.multi, ws, a)364407 return xc, nil365408 }366409 @@ -328,3 +328,32 @@ func TestXLSXWorkbookRoundTrip(t *testing.T) {
328328 t.Errorf("name Rent: %v %s", ok, n.Ref())329329 }330330 }331+332+// A formula naming a sheet the workbook doesn't have goes out as its333+// value (#REF!), with a note, since Excel would refuse the reference.334+func TestXLSXMissingSheetAsValue(t *testing.T) {335+ src := build(t, map[string]string{"A1": "2", "B1": "=Gone!A1+1", "B2": "=A1*2", "B3": "=SUM('Old plan'!A1:A3, gone!B1)"})336+ name := filepath.Join(t.TempDir(), "missing.xlsx")337+ res, err := Export(context.Background(), name, XLSX, SnapBook(src), ExportOptions{})338+ if err != nil {339+ t.Fatal(err)340+ }341+ want := "2 formulas naming a sheet that doesn't exist saved as values, e.g. B1 (Gone)"342+ if len(res.Notes) != 1 || res.Notes[0] != want {343+ t.Errorf("notes %q, want %q", res.Notes, want)344+ }345+ x, err := excelize.OpenFile(name)346+ if err != nil {347+ t.Fatal(err)348+ }349+ defer x.Close()350+ ws := x.GetSheetName(0)351+ for cell, formula := range map[string]string{"B1": "", "B2": "A1*2", "B3": ""} {352+ if got, _ := x.GetCellFormula(ws, cell); got != formula {353+ t.Errorf("%s formula %q, want %q", cell, got, formula)354+ }355+ }356+ if got, _ := x.GetCellValue(ws, "B1"); got != "#REF!" {357+ t.Errorf("B1 value %q", got)358+ }359+}@@ -27,6 +27,26 @@ func (c *Cell) rewritten(rw formula.Rewriter) *Cell {
2727 return c.withFormula(n)2828 }2929 30+// NamedSheets returns the sheet names the formula at a names, as written31+// and without repeats (in any case), or nil for anything else. Names of32+// sheets that don't exist are included: exporters check them.33+func (s *Sheet) NamedSheets(a Addr) []string {34+ c := s.cells.get(a)35+ if c == nil || len(c.xrefs) == 0 {36+ return nil37+ }38+ var out []string39+ seen := map[string]bool{}40+ add := func(name string) {41+ if k := formula.SheetKey(name); name != "" && !seen[k] {42+ seen[k] = true43+ out = append(out, name)44+ }45+ }46+ formula.WalkRefs(c.expr, func(n string, _ Addr) { add(n) }, func(n string, _ Rect) { add(n) })47+ return out48+}49+3050 // ShiftEntry returns an entry as if it were typed in one cell and copied3151 // dc columns and dr rows away: a formula's relative references move and3252 // $absolute ones stay, as in a paste. Other entries come back unchanged.