Formatting the whole sheet sets a sheet-wide format that cells fall back on after their row's and column's, and changes the column and row formats that exist so they show it too, instead of setting 16,384 column formats. It is saved as the A:XFD line, and exported to XLSX on every column.
- commit
- 460847ca0ffc9481d94b393d3ab2b8f4f31609e0
- parent
- 3d2692a64d85168f42afd8d15460ea1345e4cc77
- author
- Dami <dami.etoile@gmail.com>
| M | internal/ | 22 | ++++++- |
| M | internal/ | 2 | +- |
| M | internal/ | 18 | +++++- |
| M | internal/ | 92 | ++++++++++++++++++++---------- |
| M | internal/ | 28 | +++++++-- |
| M | internal/ | 7 | ++- |
@@ -19,13 +19,20 @@ type LineFormat struct {
1919 Style sheet.Style2020 }2121 22-// snapLines copies a sheet's column (row false) or row formats.22+// snapLines copies a sheet's column (row false) or row formats. Excel23+// has no format for the whole sheet, so with one every column gets it,24+// under its own.2325 func snapLines(s *sheet.Sheet, row bool) map[int]LineFormat {2426 fs, ss := s.ColFormats(), s.ColStyles()2527 if row {2628 fs, ss = s.RowFormats(), s.RowStyles()2729 }2830 out := map[int]LineFormat{}31+ if f, st := s.SheetFormat(); !row && (!f.IsZero() || !st.IsZero()) {32+ for c := range sheet.MaxCols {33+ out[c] = LineFormat{f, st}34+ }35+ }2936 for n, f := range fs {3037 l := out[n]3138 l.Format = f@@ -39,8 +46,10 @@ func snapLines(s *sheet.Sheet, row bool) map[int]LineFormat {
3946 return out4047 }4148 42-// importXLSXColumns reads the sheet's column widths and styles.49+// importXLSXColumns reads the sheet's column widths and styles; a style50+// on every column is the sheet's.4351 func importXLSXColumns(x *excelize.File, b *builder, ws string, styles map[int]xlsxStyle) {52+ cols := map[int]xlsxStyle{}4453 for c := range sheet.MaxCols {4554 colName, _ := excelize.ColumnNumberToName(c + 1)4655 if w, err := x.GetColWidth(ws, colName); err == nil && math.Abs(w-excelDefaultWidth) >= 0.01 && math.Abs(w-9.140625) >= 0.01 {@@ -48,10 +57,17 @@ func importXLSXColumns(x *excelize.File, b *builder, ws string, styles map[int]x
4857 }4958 if id, err := x.GetColStyle(ws, colName); err == nil && id != 0 {5059 if st := styleOf(x, id, styles); st != (xlsxStyle{}) {51- b.s.LoadLineFormat(false, c, st.format, st.style)60+ cols[c] = st5261 }5362 }5463 }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+ }68+ for c, st := range cols {69+ b.s.LoadLineFormat(false, c, st.format, st.style)70+ }5571 }5672 5773 // importXLSXRowStyle keeps the style of row, if it has one of its own.@@ -19,7 +19,7 @@ func (s *Sheet) DisplayFormat(a Addr) Format {
1919 switch {2020 case c != nil && !c.Format.IsZero():2121 return c.Format22- case s.lines.cols == nil && s.lines.rows == nil:22+ case s.lines.none():2323 case c != nil && c.Style.own:2424 default:2525 if f := s.inherited(a).Format; !f.IsZero() {@@ -15,18 +15,28 @@ import (
1515 // keyed by columns or rows as a range writes them, runs of lines with the1616 // same formatting together, each value a formatted blank cell:1717 //18-// "lines": {"B:B": {"format": "currency", "decimals": 2}, "D:F": {"bold": true}, "1:1": {"bold": true}},18+// "lines": {"A:XFD": {"italic": true}, "B:B": {"format": "currency", "decimals": 2}, "1:1": {"bold": true}},19+//20+// A:XFD, every column, is the whole sheet's format.1921 //2022 // Earlier builds ignore the field and show those cells unformatted, so it2123 // needs no new version.2224 2325 // writeLines writes the "lines" field, if the sheet has line formats.2426 func (s *Sheet) writeLines(b *bytes.Buffer, indent string) error {25- if s.lines.cols == nil && s.lines.rows == nil {27+ if s.lines.none() {2628 return nil2729 }2830 b.WriteString(indent + `"lines": {`)2931 first := true32+ if l := s.lines.sheet; !l.IsZero() {33+ raw, err := encodeCell(&Cell{Format: l.Format, Style: l.Style})34+ if err != nil {35+ return err36+ }37+ fmt.Fprintf(b, "%q: %s", s.lineRect(false, wholeSheet, wholeSheet).String(), raw)38+ first = false39+ }3040 for _, row := range []bool{false, true} {3141 m := s.lines.cols3242 if row {@@ -70,6 +80,10 @@ func (s *Sheet) readLines(lines map[string]json.RawMessage) error {
7080 if err != nil || input != "" {7181 return fmt.Errorf("lines %s: invalid format", key)7282 }83+ if r.AllRows() && r.AllCols() {84+ s.setLine(false, wholeSheet, lineFmt{f, st})85+ continue86+ }7387 row := !r.AllRows()7488 n0, n1 := r.From.Col, r.To.Col7589 if row {@@ -10,8 +10,10 @@ import (
1010 // Column and row formats, as in Sheets: formatting whole columns or rows1111 // stores the format once for the line instead of on each of its million1212 // cells, and cells take it when they have none of their own. A cell's13-// format and style come from the cell, else its row, else its column;14-// each part (number format, text style) falls back on its own, so a13+// format and style come from the cell, else its row, else its column,14+// else the sheet's (formatting the whole sheet sets that, once, rather15+// than 16,384 column formats); each part (number format, text style)16+// falls back on its own, so a1517 // currency cell in a bold column shows bold currency. A cell whose1618 // formatting differs from its line's in a way the fallback can't express1719 // (Automatic in a currency column) keeps its own formatting whole, marked@@ -25,45 +27,60 @@ type lineFmt struct {
2527 2628 func (l lineFmt) IsZero() bool { return l == lineFmt{} }2729 28-// lineFormats are a sheet's column and row formats; nil maps when none.30+// lineFormats are a sheet's column and row formats, nil maps when none,31+// and the whole sheet's.2932 type lineFormats struct {30- cols map[int]lineFmt31- rows map[int]lineFmt33+ cols map[int]lineFmt34+ rows map[int]lineFmt35+ sheet lineFmt3236 }3337 34-// lineKey is a column (row false) or row of a sheet, for undo.38+// none reports whether the sheet has no line formats at all.39+func (l *lineFormats) none() bool { return l.cols == nil && l.rows == nil && l.sheet.IsZero() }40+41+// lineKey is a column (row false) or row of a sheet, for undo; column42+// wholeSheet is the sheet's own format.3543 type lineKey struct {3644 s *Sheet3745 row bool3846 n int3947 }4048 41-// line is a column's (row false) or row's formatting.49+const wholeSheet = -150+51+// line is a column's (row false) or row's formatting, or the sheet's.4252 func (s *Sheet) line(row bool, n int) lineFmt {43- if row {53+ switch {54+ case row:4455 return s.lines.rows[n]56+ case n == wholeSheet:57+ return s.lines.sheet4558 }4659 return s.lines.cols[n]4760 }4861 4962 // inherited is what a cell at a shows without formatting of its own: its50-// row's format and style, else its column's.63+// row's format and style, else its column's, else the sheet's.5164 func (s *Sheet) inherited(a Addr) lineFmt {52- if s.lines.rows == nil && s.lines.cols == nil {65+ if s.lines.none() {5366 return lineFmt{}5467 }5568 l := s.lines.rows[a.Row]56- if c, ok := s.lines.cols[a.Col]; ok {69+ for _, next := range [2]lineFmt{s.lines.cols[a.Col], s.lines.sheet} {5770 if l.Format.IsZero() {58- l.Format = c.Format71+ l.Format = next.Format5972 }6073 if l.Style.IsZero() {61- l.Style = c.Style74+ l.Style = next.Style6275 }6376 }6477 return l6578 }6679 80+// SheetFormat returns the format and style of the whole sheet, which81+// every cell without its own, its row's or its column's shows.82+func (s *Sheet) SheetFormat() (Format, Style) { return s.lines.sheet.Format, s.lines.sheet.Style }83+6784 // effective is the format (without what formulas infer) and style the6885 // cell at a shows: its own, falling back part by part on its line's.6986 func (s *Sheet) effective(a Addr) lineFmt {@@ -144,16 +161,19 @@ func lineStylesOf(m map[int]lineFmt) map[int]Style {
144161 145162 // LoadLineFormat gives a whole column (row false) or row a format and146163 // style, as an importer does, without recording undo or touching its147-// cells.164+// cells; column -1 is the whole sheet.148165 func (s *Sheet) LoadLineFormat(row bool, n int, f Format, st Style) {149- if row && n >= 0 && n < MaxRows || !row && n >= 0 && n < MaxCols {166+ if row && n >= 0 && n < MaxRows || !row && n >= wholeSheet && n < MaxCols {150167 s.setLine(row, n, lineFmt{f, st})151168 }152169 }153170 154171 func (s *Sheet) lineRect(row bool, from, to int) Rect {155- if row {172+ switch {173+ case row:156174 return rowRect(from, to)175+ case from == wholeSheet:176+ return colRect(0, MaxCols-1)157177 }158178 return colRect(from, to)159179 }@@ -162,25 +182,31 @@ func (s *Sheet) lineRect(row bool, from, to int) Rect {
162182 // within a step, marking its cells changed, so formulas reading them infer163183 // their format again.164184 func (s *Sheet) setLine(row bool, n int, l lineFmt) {165- m := &s.lines.cols166- if row {167- m = &s.lines.rows168- }169- if (*m)[n] == l {185+ if s.line(row, n) == l {170186 return171187 }172188 if st := s.wb.hist.open; st != nil {173189 k := lineKey{s, row, n}174190 if _, seen := st.lines[k]; !seen {175- st.lines[k] = (*m)[n]191+ st.lines[k] = s.line(row, n)176192 }177193 }178- if l.IsZero() {194+ m := &s.lines.cols195+ switch {196+ case row:197+ m = &s.lines.rows198+ case n == wholeSheet:199+ s.lines.sheet = l200+ m = nil201+ }202+ switch {203+ case m == nil:204+ case l.IsZero():179205 delete(*m, n)180206 if len(*m) == 0 {181207 *m = nil182208 }183- } else {209+ default:184210 if *m == nil {185211 *m = map[int]lineFmt{}186212 }@@ -245,21 +271,27 @@ func (s *Sheet) formatLines(r Rect, fn func(*lineFmt)) bool {
245271 }246272 247273 // eachLine applies fn to the line formats of r's whole columns, or rows;248-// for the whole sheet, to every column and to the rows that have one.274+// for the whole sheet, to its own format and to the columns and rows275+// that have one, so they show the change too.249276 func (s *Sheet) eachLine(r Rect, fn func(*lineFmt)) {250277 apply := func(row bool, n int, l lineFmt) {251278 fn(&l)252279 s.setLine(row, n, l)253280 }281+ if r.AllRows() && r.AllCols() {282+ apply(false, wholeSheet, s.lines.sheet)283+ for _, n := range slices.Sorted(maps.Keys(s.lines.cols)) {284+ apply(false, n, s.lines.cols[n])285+ }286+ for _, n := range slices.Sorted(maps.Keys(s.lines.rows)) {287+ apply(true, n, s.lines.rows[n])288+ }289+ return290+ }254291 if r.AllRows() {255292 for c := r.From.Col; c <= r.To.Col; c++ {256293 apply(false, c, s.lines.cols[c])257294 }258- if r.AllCols() {259- for _, n := range slices.Sorted(maps.Keys(s.lines.rows)) {260- apply(true, n, s.lines.rows[n])261- }262- }263295 return264296 }265297 for n := r.From.Row; n <= r.To.Row; n++ {@@ -102,26 +102,42 @@ func TestLineFormatsUndoAndInsert(t *testing.T) {
102102 }103103 }104104 105-// Formatting the whole sheet formats each column once and makes no106-// cells.105+// Formatting the whole sheet sets the sheet's own format, once, and106+// changes the columns and rows that have one so they show it too.107107 func TestWholeSheetFormat(t *testing.T) {108108 s := New()109109 s.Set(at("XFD1048576"), "1")110+ s.SetStyle(col(3), func(st *Style) { st.Italic = true })110111 all := Rect{To: Addr{Col: MaxCols - 1, Row: MaxRows - 1}}111112 s.SetStyle(all, func(st *Style) { st.Bold = true })112- if len(s.lines.cols) != MaxCols || s.cells.len() != 1 {113- t.Errorf("%d column formats, %d cells", len(s.lines.cols), s.cells.len())113+ if len(s.lines.cols) != 1 || s.cells.len() != 1 || !s.lines.sheet.Style.Bold {114+ t.Errorf("%d column formats, %d cells, sheet %+v", len(s.lines.cols), s.cells.len(), s.lines.sheet)114115 }115116 if !s.CellStyle(at("XFD1048576")).Bold || !s.CellStyle(at("Q77")).Bold {116117 t.Error("not bold everywhere")117118 }119+ if st := s.CellStyle(at("D9")); !st.Bold || !st.Italic {120+ t.Errorf("D9 = %+v, want column D's italic and the sheet's bold", st)121+ }118122 var buf bytes.Buffer119123 s.Write(&buf)120124 if !bytes.Contains(buf.Bytes(), []byte(`"A:XFD": {"bold":true}`)) {121- t.Errorf("the columns aren't written as one run:\n%.300s", buf.String())125+ t.Errorf("the sheet's format isn't written:\n%.300s", buf.String())126+ }127+ back, err := Read(&buf)128+ if err != nil {129+ t.Fatal(err)122130 }131+ if !back.CellStyle(at("Q77")).Bold || !back.CellStyle(at("D9")).Italic || len(back.lines.cols) != 1 {132+ t.Error("the sheet's format didn't load")133+ }134+ s.Undo()135+ if s.CellStyle(at("Q77")).Bold || s.CellStyle(at("D9")).Bold {136+ t.Error("undo left bold")137+ }138+ s.Redo()123139 s.ClearFormatting(all)124- if len(s.lines.cols) != 0 {140+ if len(s.lines.cols) != 0 || !s.lines.sheet.IsZero() {125141 t.Errorf("%d column formats after clearing", len(s.lines.cols))126142 }127143 }@@ -191,7 +191,9 @@ func (c *Clip) Size() (cols, rows int) {
191191 192192 // Text returns the clip's values as displayed in General format, row by193193 // row, for the system clipboard. Blank rows and columns past the last194-// value are left off, so copying whole columns gives their data.194+// value are left off, so copying whole columns gives their data; a block195+// of more than MaxCells cells, blanks between values included, is too196+// big for text, and Text returns nil.195197 func (c *Clip) Text() [][]string {196198 cols, rows := 0, 0197199 for off, cell := range c.cells {@@ -199,6 +201,9 @@ func (c *Clip) Text() [][]string {
199201 cols, rows = max(cols, off.Col+1), max(rows, off.Row+1)200202 }201203 }204+ if cols*rows > MaxCells() {205+ return nil206+ }202207 out := make([][]string, rows)203208 for r := range out {204209 out[r] = make([]string, cols)