A pivot finds its source's last data row once, not per row; selections and blocks of cells as large as the sheet's cells are many read the cell map rather than the index; XLSX imports look at the columns their data reaches (at least 256) for widths and styles, and read a style on both A and XFD as the sheet's.
- commit
- cf897e2efff656c63f87d94eafe493953160d368
- parent
- fb81461eeb96948648ef5db9cc265e8266a6140d
- author
- Dami <dami.etoile@gmail.com>
| M | internal/ | 5 | +++-- |
| M | internal/ | 33 | +++++++++++++++++++----------- |
| M | internal/ | 16 | +++++++++++++++ |
| M | internal/ | 2 | +- |
| M | internal/ | 3 | ++- |
| M | internal/ | 3 | ++- |
| M | internal/ | 2 | +- |
| M | internal/ | 18 | ++++++++++++++++ |
@@ -90,7 +90,7 @@ func importXLSXSheet(ctx context.Context, x *excelize.File, b *builder, ws strin
9090 return 0, err9191 }9292 defer rows.Close()93- row := 093+ row, width := 0, 09494 for ; rows.Next(); row++ {9595 if row%128 == 0 {9696 if err := ctx.Err(); err != nil {@@ -103,6 +103,7 @@ func importXLSXSheet(ctx context.Context, x *excelize.File, b *builder, ws strin
103103 return 0, err104104 }105105 importXLSXRowStyle(x, b, rows, row, styles)106+ width = max(width, len(cols))106107 for col, raw := range cols {107108 a := sheet.Addr{Col: col, Row: row}108109 if !b.fits(a) {@@ -115,7 +116,7 @@ func importXLSXSheet(ctx context.Context, x *excelize.File, b *builder, ws strin
115116 if err := rows.Error(); err != nil {116117 return 0, err117118 }118- importXLSXColumns(x, b, ws, styles)119+ importXLSXColumns(x, b, ws, styles, width)119120 return row, nil120121 }121122 @@ -46,25 +46,34 @@ func snapLines(s *sheet.Sheet, row bool) map[int]LineFormat {
4646 return out4747 }4848 49-// importXLSXColumns reads the sheet's column widths and styles; a style50-// on every column is the sheet's.51-func importXLSXColumns(x *excelize.File, b *builder, ws string, styles map[int]xlsxStyle) {49+// importXLSXColumns reads the widths and styles of the sheet's columns50+// up to its data's width (at least 256); a style on every column, the51+// last included, is the sheet's.52+func importXLSXColumns(x *excelize.File, b *builder, ws string, styles map[int]xlsxStyle, width int) {53+ styleOfCol := func(name string) (xlsxStyle, bool) {54+ if id, err := x.GetColStyle(ws, name); err == nil && id != 0 {55+ st := styleOf(x, id, styles)56+ return st, st != (xlsxStyle{})57+ }58+ return xlsxStyle{}, false59+ }60+ var whole xlsxStyle61+ if first, ok := styleOfCol("A"); ok {62+ if last, _ := styleOfCol("XFD"); last == first {63+ whole = first64+ b.s.LoadLineFormat(false, -1, first.format, first.style)65+ }66+ }5267 cols := map[int]xlsxStyle{}53- for c := range sheet.MaxCols {68+ for c := range min(max(width, 256), sheet.MaxCols) {5469 colName, _ := excelize.ColumnNumberToName(c + 1)5570 if w, err := x.GetColWidth(ws, colName); err == nil && math.Abs(w-excelDefaultWidth) >= 0.01 && math.Abs(w-9.140625) >= 0.01 {5671 b.s.SetColWidth(c, max(int(math.Round(w))+excelPadding, 1))5772 }58- if id, err := x.GetColStyle(ws, colName); err == nil && id != 0 {59- if st := styleOf(x, id, styles); st != (xlsxStyle{}) {60- cols[c] = st61- }73+ if st, ok := styleOfCol(colName); ok && st != whole {74+ cols[c] = st6275 }6376 }64- if st := cols[0]; len(cols) == sheet.MaxCols && !slices.ContainsFunc(slices.Collect(maps.Values(cols)), func(o xlsxStyle) bool { return o != st }) {65- b.s.LoadLineFormat(false, -1, st.format, st.style)66- return67- }6877 for c, st := range cols {6978 b.s.LoadLineFormat(false, c, st.format, st.style)7079 }@@ -47,4 +47,20 @@ func TestXLSXLineFormats(t *testing.T) {
4747 if n := len(got.ColFormats()); n != 2 {4848 t.Errorf("%d column formats, want 2", n)4949 }50+51+ // The whole sheet's format goes out on every column and comes back52+ // as the sheet's.53+ src.SetStyle(sheet.Rect{To: sheet.Addr{Col: sheet.MaxCols - 1, Row: sheet.MaxRows - 1}}, func(st *sheet.Style) { st.Underline = true })54+ if _, err := Export(context.Background(), name, XLSX, Snap(src, sheet.Rect{}, "Lines"), ExportOptions{}); err != nil {55+ t.Fatal(err)56+ }57+ if res, err = Import(context.Background(), name, Options{}); err != nil {58+ t.Fatal(err)59+ }60+ if _, st := res.Sheet.SheetFormat(); !st.Underline || !res.Sheet.CellStyle(addr(t, "ZZ9")).Underline {61+ t.Errorf("the sheet's style came back as %+v", st)62+ }63+ if f := res.Sheet.DisplayFormat(addr(t, "C9")); f.Kind != sheet.FmtCurrency || !res.Sheet.CellStyle(addr(t, "C9")).Underline {64+ t.Errorf("column C came back as %v", f.Kind)65+ }5066 }@@ -147,7 +147,7 @@ func (s *Sheet) RangeStats(r Rect) Stats {
147147 st.Sum += c.Value.Num148148 }149149 }150- for _, c := range s.cells.inRange(r) {150+ for _, c := range s.cells.anyInRange(r) {151151 if !c.Blank() {152152 add(c)153153 }@@ -353,7 +353,8 @@ func (w *Workbook) ValueTitle(p Pivot, v PivotValue) string {
353353 // Sheets: SUM when the column holds a number, COUNTA otherwise.354354 func (w *Workbook) DefaultSummarize(p Pivot, col int) Summarize {355355 if src := w.Lookup(p.Source); src != nil {356- for row := p.Range.From.Row + 1; row <= src.filterData(p.Range).To.Row; row++ {356+ last := src.filterData(p.Range).To.Row357+ for row := p.Range.From.Row + 1; row <= last; row++ {357358 if src.Value(Addr{Col: col, Row: row}).Kind == Number {358359 return SumBy359360 }@@ -209,7 +209,8 @@ func (c *pivotCalc) valueFormat(v PivotValue) Format {
209209 func (c *pivotCalc) gather() {210210 tests := pivotTests(c.p.Filters)211211 r := c.p.Range212- for row := r.From.Row + 1; row <= c.src.filterData(r).To.Row; row++ {212+ last := c.src.filterData(r).To.Row213+ for row := r.From.Row + 1; row <= last; row++ {213214 if c.src.rowBlank(row, r) || !c.src.rowPasses(row, tests) {214215 continue215216 }@@ -345,7 +345,7 @@ func (s *Sheet) EraseRange(r Rect) {
345345 // cellsIn returns the cells in r that have contents or formatting.346346 func (s *Sheet) cellsIn(r Rect) []Addr {347347 var out []Addr348- for a := range s.cells.inRange(r) {348+ for a := range s.cells.anyInRange(r) {349349 out = append(out, a)350350 }351351 return out@@ -98,6 +98,24 @@ func (st *cellStore) inRange(r Rect) iter.Seq2[Addr, *Cell] {
9898 }9999 }100100 101+// anyInRange yields the stored cells in r in no particular order,102+// visiting whichever is cheaper: the index, row by row, or every stored103+// cell (iterating the map costs a fraction of a lookup per cell) when r104+// is as large as the sheet's cells are many.105+func (st *cellStore) anyInRange(r Rect) iter.Seq2[Addr, *Cell] {106+ area := (r.To.Col - r.From.Col + 1) * (r.To.Row - r.From.Row + 1)107+ if area < len(st.m) {108+ return st.inRange(r)109+ }110+ return func(yield func(Addr, *Cell) bool) {111+ for a, c := range st.m {112+ if r.Contains(a) && !yield(a, c) {113+ return114+ }115+ }116+ }117+}118+101119 type colBlock struct {102120 col int103121 b *rowBlock