cat / 012
clone

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

cat/module-path
012 / internal / fileio / snapshot.go
snapshot.go 8.5K
1package fileio23import (4 "iter"5 "strconv"67 "src.rest/cat/012/internal/locale"8 "src.rest/cat/012/internal/sheet"9)1011// Snapshot is a copy of the part of a sheet being exported, taken on the12// UI goroutine so the file can be written in the background while the13// sheet keeps changing.14type Snapshot struct {15 // Range is what's exported. Whole-sheet exports start at A1, as16 // Sheets' downloads do, so cells keep their addresses.17 Range sheet.Rect18 Cells map[sheet.Addr]SnapCell19 Widths map[int]int // non-default column widths20 Name string // what to call the data: a sheet or table name21 // Locale is the sheet's, which text formats (CSV) are written in.22 Locale *locale.Locale2324 // ColFormats and RowFormats are the formats of whole columns and25 // rows, for formats that keep them (XLSX).26 ColFormats, RowFormats map[int]LineFormat2728 // Sheets are every sheet of the workbook, in order, for formats that29 // hold several (XLSX); the snapshot itself is one of them, the one30 // shown. Nil exports just this snapshot. Names are the workbook's31 // named ranges.32 Sheets []*Snapshot33 Names []SnapName34 Hidden bool // a hidden sheet of Sheets, written hidden3536 // FrozenRows and FrozenCols are the frozen panes; Filter is the37 // sheet's filter, if any, and HiddenRows the rows of Range it hides,38 // for formats that keep them (XLSX).39 FrozenRows, FrozenCols int40 Filter *sheet.Filter41 HiddenRows map[int]bool4243 // Spills are the cells each formula whose array spills covers, by44 // the formula's cell, for formats that keep formulas (XLSX).45 Spills map[sheet.Addr]sheet.Rect4647 // CondFormats and Validations are the sheet's rules, for formats48 // that keep them (XLSX).49 CondFormats []sheet.CondFormat50 Validations []sheet.Validation51 // Notes are the cells' notes in the range, for formats that keep52 // them (XLSX, as comments). A note may be on a cell with no contents.53 Notes map[sheet.Addr]string5455 // Heights are the rows' heights set by hand, in lines, and Merges56 // the merged cells in the range, for formats that keep them (XLSX).57 Heights map[int]int58 Merges []sheet.Rect5960 // Tables are the tables wholly in the range, for formats that keep61 // them (XLSX).62 Tables []sheet.Table6364 // Looks are how the sheet's rules draw the range's cells, and Charts65 // the charts whose top-left cell is in it, for formats that show66 // what the screen shows (HTML); see snapdrawn.go.67 Looks map[sheet.Addr]sheet.Look68 Charts []SnapChart69}7071// SnapName is a named range: its name, and the range on the sheet72// named Sheet.73type SnapName struct {74 Name, Sheet string75 Range sheet.Rect76}7778// SnapCell is one non-blank cell of a snapshot.79type SnapCell struct {80 Input string81 Value sheet.Value82 Format sheet.Format // as displayed, including one inferred from a formula83 Own sheet.Format // the cell's own format84 Style sheet.Style85 Formula bool86 Sheets []string // the sheets a formula names, as written87 Tables []string // the tables a formula reads, as written88}8990// Text is the cell as displayed, without a width limit.91func (c SnapCell) Text() string {92 return sheet.FormatText(c.Value, c.Format)93}9495// Snap copies the cells of s in r. A zero r means the whole sheet, from96// A1 to the last cell with contents; any other r is trimmed to its last97// row and column with contents, so exporting whole columns writes their98// data rather than a million blank lines.99func Snap(s *sheet.Sheet, r sheet.Rect, name string) *Snapshot {100 notes, whole := r, r == (sheet.Rect{})101 if whole {102 notes = sheet.Rect{To: sheet.Addr{Col: sheet.MaxCols - 1, Row: sheet.MaxRows - 1}}103 r, _ = s.UsedRange()104 } else if data, ok := s.FilledBounds(r); ok {105 r.To = data.To106 } else {107 r.To = r.From108 }109 snap := &Snapshot{Range: r, Cells: map[sheet.Addr]SnapCell{}, Widths: s.Widths(), Name: name, Locale: s.Locale(),110 ColFormats: snapLines(s, false), RowFormats: snapLines(s, true), Filter: s.Filter(),111 CondFormats: s.CondFormats(), Validations: s.Validations()}112 snap.FrozenRows, snap.FrozenCols = s.Frozen()113 snap.Heights, snap.Merges = heightsMergesIn(s, r)114 snapTables(snap, s, whole)115 for _, a := range s.NotesIn(notes) {116 if snap.Notes == nil {117 snap.Notes = map[sheet.Addr]string{}118 }119 snap.Notes[a] = s.Note(a)120 }121 if f := snap.Filter; f != nil {122 snap.HiddenRows = map[int]bool{}123 for row := max(f.Range.From.Row, r.From.Row); row <= min(f.Range.To.Row, r.To.Row); row++ {124 if s.RowHidden(row) {125 snap.HiddenRows[row] = true126 }127 }128 }129 for _, a := range s.Addrs() {130 if !r.Contains(a) {131 continue132 }133 c := s.Cell(a)134 if area, ok := s.SpillArea(a); ok {135 if snap.Spills == nil {136 snap.Spills = map[sheet.Addr]sheet.Rect{}137 }138 snap.Spills[a] = area139 }140 snap.Cells[a] = SnapCell{141 Input: c.Input,142 Value: c.Value,143 Format: s.DisplayFormat(a),144 Own: s.CellFormat(a),145 Style: s.CellStyle(a),146 Formula: c.IsFormula(),147 Sheets: s.NamedSheets(a),148 Tables: s.NamedTables(a),149 }150 }151 snapDrawn(snap, s, whole)152 return snap153}154155// snapTables adds the sheet's tables in the snapshot's range to it; a156// whole sheet's range grows to hold its tables, blank rows and all.157func snapTables(snap *Snapshot, s *sheet.Sheet, whole bool) {158 for _, t := range s.Tables() {159 if whole {160 snap.Range.To.Col, snap.Range.To.Row = max(snap.Range.To.Col, t.Range.To.Col), max(snap.Range.To.Row, t.Range.To.Row)161 }162 if snap.Range.Contains(t.Range.From) && snap.Range.Contains(t.Range.To) {163 snap.Tables = append(snap.Tables, t)164 }165 }166}167168// heightsMergesIn are the heights of the rows of r and the merges inside169// it.170func heightsMergesIn(s *sheet.Sheet, r sheet.Rect) (map[int]int, []sheet.Rect) {171 var heights map[int]int172 for row, h := range s.Heights() {173 if row >= r.From.Row && row <= r.To.Row {174 if heights == nil {175 heights = map[int]int{}176 }177 heights[row] = h178 }179 }180 var merges []sheet.Rect181 for _, m := range s.Merges() {182 if r.Contains(m.From) && r.Contains(m.To) {183 merges = append(merges, m)184 }185 }186 return heights, merges187}188189// snapLayout adds to a whole sheet's snapshot what its layout needs in190// a workbook, growing the range to hold it: rows with heights, merged191// cells, and blank cells that draw borders, which count as contents192// there.193func snapLayout(snap *Snapshot, s *sheet.Sheet) {194 snap.Heights, snap.Merges = s.Heights(), s.Merges()195 grow := func(x sheet.Rect) {196 if len(snap.Cells) == 0 && snap.Range.From == snap.Range.To {197 snap.Range = sheet.Rect{To: x.To}198 }199 snap.Range.To.Col, snap.Range.To.Row = max(snap.Range.To.Col, x.To.Col), max(snap.Range.To.Row, x.To.Row)200 }201 for _, m := range snap.Merges {202 grow(m)203 }204 for _, a := range s.BorderedIn(sheet.Rect{To: sheet.Addr{Col: sheet.MaxCols - 1, Row: sheet.MaxRows - 1}}) {205 if _, ok := snap.Cells[a]; !ok {206 snap.Cells[a] = SnapCell{Style: s.CellStyle(a)}207 }208 grow(sheet.Rect{From: a, To: a})209 }210}211212// SnapBook copies every sheet of s's workbook, whole, with the named213// ranges, for formats that hold several sheets. The result is s's214// snapshot.215func SnapBook(s *sheet.Sheet) *Snapshot {216 var out *Snapshot217 all := []*Snapshot{}218 for _, t := range s.Book().Sheets() {219 sn := Snap(t, sheet.Rect{}, t.Name())220 snapLayout(sn, t)221 sn.Hidden = t.Hidden() && t != s222 if t == s {223 out = sn224 }225 all = append(all, sn)226 }227 out.Sheets = all228 for _, n := range s.Book().Names() {229 if !n.Gone() {230 out.Names = append(out.Names, SnapName{Name: n.Name, Sheet: n.Sheet.Name(), Range: n.Range})231 }232 }233 return out234}235236// excelRange writes a range as Excel's defined names hold it: absolute,237// with its sheet, Q3!$B$2:$B$9.238func excelRange(ws string, r sheet.Rect) string {239 abs := func(a sheet.Addr) string {240 return "$" + sheet.ColName(a.Col) + "$" + strconv.Itoa(a.Row+1)241 }242 ref := abs(r.From)243 if r.From != r.To {244 ref += ":" + abs(r.To)245 }246 return sheet.QuoteSheet(sheetName(ws)) + "!" + ref247}248249// textRows yields the snapshot's displayed text row by row across its250// range as shown in its locale, for text formats: the rows the filter251// shows, as Sheets copies a filtered range. The slice is reused from row252// to row.253func (s *Snapshot) textRows() iter.Seq[[]string] {254 return func(yield func([]string) bool) {255 r := s.Range256 line := make([]string, r.To.Col-r.From.Col+1)257 for row := r.From.Row; row <= r.To.Row; row++ {258 if s.HiddenRows[row] {259 continue260 }261 for col := r.From.Col; col <= r.To.Col; col++ {262 line[col-r.From.Col] = ""263 if c, ok := s.Cells[sheet.Addr{Col: col, Row: row}]; ok {264 line[col-r.From.Col] = sheet.FormatTextIn(c.Value, c.Format, s.Locale.Or())265 }266 }267 if !yield(line) {268 return269 }270 }271 }272}