cat / 012
clone

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

Import XLSX with 012's streaming reader instead of excelize

Dami committed b704e10
The excelize importer moves to a test file as the differential test's
reference; excelize still writes XLSX exports.
commit
b704e10b38d0c7f5f4443b6673ac6b181a9ca274
parent
2d9d5cf90e74888334b00e07fd265d1c50a859bd
author
Dami <dami.etoile@gmail.com>
4 files changed, +211 -197
Minternal/fileio/xlsx.go193----------------------------
Minternal/fileio/xlsx_diff_test.go4+-
Ainternal/fileio/xlsx_excelize_test.go207++++++++++++++++++++++++++++++
Minternal/fileio/xlsximport.go4+-
M internal/fileio/xlsx.go +0 -193
@@ -4,8 +4,6 @@ import (
44 "context"55 "fmt"66 "io"7- "math"8- "strconv"97 "strings"108 119 "github.com/xuri/excelize/v2"
@@ -20,205 +18,14 @@ const (
2018 excelPadding = 12119 )2220 23-// importXLSX reads every sheet of a workbook, with its named ranges, and24-// returns the sheet that was active in Excel.25-func importXLSX(ctx context.Context, name string, opt Options) (*Result, error) {26- prog := opt.Progress27- x, err := excelize.OpenFile(name)28- if err != nil {29- return nil, err30- }31- defer x.Close()32- names := x.GetSheetList()33- if len(names) == 0 {34- return nil, fmt.Errorf("the workbook has no sheets")35- }36- // Rows across all sheets, for the progress bar.37- dims := make([]int, len(names))38- total := 039- for i, ws := range names {40- if dim, err := x.GetSheetDimension(ws); err == nil {41- if _, to, ok := strings.Cut(dim, ":"); ok {42- if _, r, err := excelize.CellNameToCoordinates(to); err == nil {43- dims[i] = r44- }45- }46- }47- total += dims[i]48- }49-50- b := newBuilder()51- book := b.s.Book()52- styles := map[int]xlsxStyle{}53- var notes []string54- done := 055- for i, ws := range names {56- if i == 0 {57- err = book.RenameSheet(b.s, ws)58- } else {59- b.s, err = book.AddSheet(ws, i)60- }61- if err != nil {62- return nil, fmt.Errorf("sheet %s: %w", ws, err)63- }64- rows, err := importXLSXSheet(ctx, x, b, ws, styles, func(row int) {65- prog.setRows(done + row)66- prog.setFrac(int64(done+row), int64(total))67- })68- if err != nil {69- return nil, err70- }71- done += rows72- }73- notes = append(notes, importXLSXNames(x, book)...)74- active := book.Sheet(clamp(x.GetActiveSheetIndex(), 0, book.Len()-1))75- book.SetActive(active)76- b.s = active77- prog.setRows(done)78- s, notes := b.finish(notes)79- return &Result{Sheet: s, Rows: done, Notes: notes}, nil80-}81-8221 func clamp(v, lo, hi int) int { return max(lo, min(v, hi)) }8322 84-// importXLSXSheet reads one sheet into b.s, reporting rows read, and85-// returns how many rows it read.86-func importXLSXSheet(ctx context.Context, x *excelize.File, b *builder, ws string, styles map[int]xlsxStyle, report func(int)) (int, error) {87- rows, err := x.Rows(ws)88- if err != nil {89- return 0, err90- }91- defer rows.Close()92- row := 093- for ; rows.Next(); row++ {94- if row%128 == 0 {95- if err := ctx.Err(); err != nil {96- return 0, err97- }98- report(row)99- }100- cols, err := rows.Columns(excelize.Options{RawCellValue: true})101- if err != nil {102- return 0, err103- }104- for col, raw := range cols {105- a := sheet.Addr{Col: col, Row: row}106- if !b.fits(a) {107- break108- }109- cell, _ := excelize.CoordinatesToCellName(col+1, row+1)110- importXLSXCell(x, b, ws, cell, a, raw, styles)111- }112- }113- if err := rows.Error(); err != nil {114- return 0, err115- }116- for c := range sheet.MaxCols {117- colName, _ := excelize.ColumnNumberToName(c + 1)118- w, err := x.GetColWidth(ws, colName)119- if err != nil || math.Abs(w-excelDefaultWidth) < 0.01 || math.Abs(w-9.140625) < 0.01 {120- continue121- }122- b.s.SetColWidth(c, max(int(math.Round(w))+excelPadding, 1))123- }124- return row, nil125-}126-127-// importXLSXNames defines the workbook's named ranges that are a range on128-// one sheet, such as Sales = Q3!$B$2:$B$20, and notes the others (sheet129-// scoped names, constants, formulas).130-func importXLSXNames(x *excelize.File, book *sheet.Workbook) []string {131- skipped := 0132- example := ""133- for _, dn := range x.GetDefinedName() {134- if strings.HasPrefix(dn.Name, "_xlnm.") {135- continue // print areas and the like136- }137- ref := strings.TrimPrefix(dn.RefersTo, "=")138- ws, rest := sheet.SplitSheet(ref)139- r, ok := sheet.ParseRange(rest)140- s := book.Lookup(ws)141- if dn.Scope != "" && dn.Scope != "Workbook" || !ok || s == nil || book.DefineName(dn.Name, s, r) != nil {142- skipped++143- if example == "" {144- example = dn.Name145- }146- }147- }148- if skipped > 0 {149- return []string{fmt.Sprintf("%s left out, e.g. %s", count(skipped, "named range", "named ranges"), example)}150- }151- return nil152-}153-15423 // xlsxStyle is what 012 keeps of an Excel cell style.15524 type xlsxStyle struct {15625 format sheet.Format15726 style sheet.Style15827 }15928 160-func styleOf(x *excelize.File, id int, cache map[int]xlsxStyle) xlsxStyle {161- if st, ok := cache[id]; ok {162- return st163- }164- var out xlsxStyle165- if st, err := x.GetStyle(id); err == nil && st != nil {166- code := ""167- if st.CustomNumFmt != nil {168- code = *st.CustomNumFmt169- }170- out.format = formatOf(st.NumFmt, code)171- if f := st.Font; f != nil {172- out.style.Bold, out.style.Italic, out.style.Strikethrough = f.Bold, f.Italic, f.Strike173- out.style.Underline = f.Underline != "" && f.Underline != "none"174- }175- if al := st.Alignment; al != nil {176- switch al.Horizontal {177- case "left":178- out.style.Align = sheet.AlignLeft179- case "center", "centerContinuous":180- out.style.Align = sheet.AlignCenter181- case "right":182- out.style.Align = sheet.AlignRight183- }184- }185- }186- cache[id] = out187- return out188-}189-190-func importXLSXCell(x *excelize.File, b *builder, ws, cell string, a sheet.Addr, raw string, styles map[int]xlsxStyle) {191- var st xlsxStyle192- if id, err := x.GetCellStyle(ws, cell); err == nil && id != 0 {193- st = styleOf(x, id, styles)194- }195- typ, _ := x.GetCellType(ws, cell)196- keep := func() {197- switch typ {198- case excelize.CellTypeBool:199- b.boolean(a, raw == "1" || strings.EqualFold(raw, "TRUE"), st.style)200- return201- case excelize.CellTypeSharedString, excelize.CellTypeInlineString, excelize.CellTypeFormula, excelize.CellTypeError:202- b.text(a, raw, st.format, st.style)203- return204- }205- if v, err := strconv.ParseFloat(raw, 64); err == nil {206- b.number(a, fromExcelSerial(v, st.format), st.format, st.style)207- return208- }209- b.text(a, raw, st.format, st.style)210- }211- if f, err := x.GetCellFormula(ws, cell); err == nil && f != "" {212- b.formula(a, fromExcelFormula(f), st.format, st.style, keep)213- return214- }215- if raw == "" {216- b.put(a, "", st.format, st.style)217- return218- }219- keep()220-}221-22229 // exportXLSX writes a workbook: the snapshot's sheets (or just the22330 // snapshot), each with values, formulas in Excel's syntax with their22431 // results cached, number formats, text styles, alignment and column
M internal/fileio/xlsx_diff_test.go +2 -2
A internal/fileio/xlsx_excelize_test.go +207 -0
@@ -0,0 +1,207 @@
1+package fileio2+3+// The XLSX importer 012 used before its own reader, on excelize, kept4+// as the reference for TestXLSXDifferential.5+6+import (7+ "context"8+ "fmt"9+ "math"10+ "strconv"11+ "strings"12+13+ "github.com/xuri/excelize/v2"14+15+ "github.com/FelineStateMachine/012/internal/sheet"16+)17+18+// importXLSXExcelize reads every sheet of a workbook, with its named ranges, and19+// returns the sheet that was active in Excel.20+func importXLSXExcelize(ctx context.Context, name string, opt Options) (*Result, error) {21+ prog := opt.Progress22+ x, err := excelize.OpenFile(name)23+ if err != nil {24+ return nil, err25+ }26+ defer x.Close()27+ names := x.GetSheetList()28+ if len(names) == 0 {29+ return nil, fmt.Errorf("the workbook has no sheets")30+ }31+ // Rows across all sheets, for the progress bar.32+ dims := make([]int, len(names))33+ total := 034+ for i, ws := range names {35+ if dim, err := x.GetSheetDimension(ws); err == nil {36+ if _, to, ok := strings.Cut(dim, ":"); ok {37+ if _, r, err := excelize.CellNameToCoordinates(to); err == nil {38+ dims[i] = r39+ }40+ }41+ }42+ total += dims[i]43+ }44+45+ b := newBuilder()46+ book := b.s.Book()47+ styles := map[int]xlsxStyle{}48+ var notes []string49+ done := 050+ for i, ws := range names {51+ if i == 0 {52+ err = book.RenameSheet(b.s, ws)53+ } else {54+ b.s, err = book.AddSheet(ws, i)55+ }56+ if err != nil {57+ return nil, fmt.Errorf("sheet %s: %w", ws, err)58+ }59+ rows, err := excelizeSheet(ctx, x, b, ws, styles, func(row int) {60+ prog.setRows(done + row)61+ prog.setFrac(int64(done+row), int64(total))62+ })63+ if err != nil {64+ return nil, err65+ }66+ done += rows67+ }68+ notes = append(notes, excelizeNames(x, book)...)69+ active := book.Sheet(clamp(x.GetActiveSheetIndex(), 0, book.Len()-1))70+ book.SetActive(active)71+ b.s = active72+ prog.setRows(done)73+ s, notes := b.finish(notes)74+ return &Result{Sheet: s, Rows: done, Notes: notes}, nil75+}76+77+// excelizeSheet reads one sheet into b.s, reporting rows read, and78+// returns how many rows it read.79+func excelizeSheet(ctx context.Context, x *excelize.File, b *builder, ws string, styles map[int]xlsxStyle, report func(int)) (int, error) {80+ rows, err := x.Rows(ws)81+ if err != nil {82+ return 0, err83+ }84+ defer rows.Close()85+ row := 086+ for ; rows.Next(); row++ {87+ if row%128 == 0 {88+ if err := ctx.Err(); err != nil {89+ return 0, err90+ }91+ report(row)92+ }93+ cols, err := rows.Columns(excelize.Options{RawCellValue: true})94+ if err != nil {95+ return 0, err96+ }97+ for col, raw := range cols {98+ a := sheet.Addr{Col: col, Row: row}99+ if !b.fits(a) {100+ break101+ }102+ cell, _ := excelize.CoordinatesToCellName(col+1, row+1)103+ excelizeCell(x, b, ws, cell, a, raw, styles)104+ }105+ }106+ if err := rows.Error(); err != nil {107+ return 0, err108+ }109+ for c := range sheet.MaxCols {110+ colName, _ := excelize.ColumnNumberToName(c + 1)111+ w, err := x.GetColWidth(ws, colName)112+ if err != nil || math.Abs(w-excelDefaultWidth) < 0.01 || math.Abs(w-9.140625) < 0.01 {113+ continue114+ }115+ b.s.SetColWidth(c, max(int(math.Round(w))+excelPadding, 1))116+ }117+ return row, nil118+}119+120+// excelizeNames defines the workbook's named ranges that are a range on121+// one sheet, such as Sales = Q3!$B$2:$B$20, and notes the others (sheet122+// scoped names, constants, formulas).123+func excelizeNames(x *excelize.File, book *sheet.Workbook) []string {124+ skipped := 0125+ example := ""126+ for _, dn := range x.GetDefinedName() {127+ if strings.HasPrefix(dn.Name, "_xlnm.") {128+ continue // print areas and the like129+ }130+ ref := strings.TrimPrefix(dn.RefersTo, "=")131+ ws, rest := sheet.SplitSheet(ref)132+ r, ok := sheet.ParseRange(rest)133+ s := book.Lookup(ws)134+ if dn.Scope != "" && dn.Scope != "Workbook" || !ok || s == nil || book.DefineName(dn.Name, s, r) != nil {135+ skipped++136+ if example == "" {137+ example = dn.Name138+ }139+ }140+ }141+ if skipped > 0 {142+ return []string{fmt.Sprintf("%s left out, e.g. %s", count(skipped, "named range", "named ranges"), example)}143+ }144+ return nil145+}146+147+func excelizeStyleOf(x *excelize.File, id int, cache map[int]xlsxStyle) xlsxStyle {148+ if st, ok := cache[id]; ok {149+ return st150+ }151+ var out xlsxStyle152+ if st, err := x.GetStyle(id); err == nil && st != nil {153+ code := ""154+ if st.CustomNumFmt != nil {155+ code = *st.CustomNumFmt156+ }157+ out.format = formatOf(st.NumFmt, code)158+ if f := st.Font; f != nil {159+ out.style.Bold, out.style.Italic, out.style.Strikethrough = f.Bold, f.Italic, f.Strike160+ out.style.Underline = f.Underline != "" && f.Underline != "none"161+ }162+ if al := st.Alignment; al != nil {163+ switch al.Horizontal {164+ case "left":165+ out.style.Align = sheet.AlignLeft166+ case "center", "centerContinuous":167+ out.style.Align = sheet.AlignCenter168+ case "right":169+ out.style.Align = sheet.AlignRight170+ }171+ }172+ }173+ cache[id] = out174+ return out175+}176+177+func excelizeCell(x *excelize.File, b *builder, ws, cell string, a sheet.Addr, raw string, styles map[int]xlsxStyle) {178+ var st xlsxStyle179+ if id, err := x.GetCellStyle(ws, cell); err == nil && id != 0 {180+ st = excelizeStyleOf(x, id, styles)181+ }182+ typ, _ := x.GetCellType(ws, cell)183+ keep := func() {184+ switch typ {185+ case excelize.CellTypeBool:186+ b.boolean(a, raw == "1" || strings.EqualFold(raw, "TRUE"), st.style)187+ return188+ case excelize.CellTypeSharedString, excelize.CellTypeInlineString, excelize.CellTypeFormula, excelize.CellTypeError:189+ b.text(a, raw, st.format, st.style)190+ return191+ }192+ if v, err := strconv.ParseFloat(raw, 64); err == nil {193+ b.number(a, fromExcelSerial(v, st.format), st.format, st.style)194+ return195+ }196+ b.text(a, raw, st.format, st.style)197+ }198+ if f, err := x.GetCellFormula(ws, cell); err == nil && f != "" {199+ b.formula(a, fromExcelFormula(f), st.format, st.style, keep)200+ return201+ }202+ if raw == "" {203+ b.put(a, "", st.format, st.style)204+ return205+ }206+ keep()207+}
M internal/fileio/xlsximport.go +2 -2