cat / 012
clone

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

main
012 / internal / sheet / lines.go
lines.go 10.2K
1package sheet23import (4 "maps"5 "slices"67 "src.rest/cat/012/internal/formula"8)910// Column and row formats, as in Sheets: formatting whole columns or rows11// stores the format once for the line instead of on each of its million12// 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// 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 a17// currency cell in a bold column shows bold currency. A cell whose18// formatting differs from its line's in a way the fallback can't express19// (Automatic in a currency column) keeps its own formatting whole, marked20// by Style.own.2122// lineFmt is the formatting of a column, a row, or what a cell shows.23type lineFmt struct {24 Format Format25 Style Style26}2728func (l lineFmt) IsZero() bool { return l == lineFmt{} }2930// lineFormats are a sheet's column and row formats, nil maps when none,31// and the whole sheet's.32type lineFormats struct {33 cols map[int]lineFmt34 rows map[int]lineFmt35 sheet lineFmt36}3738// none reports whether the sheet has no line formats at all.39func (l *lineFormats) none() bool { return l.cols == nil && l.rows == nil && l.sheet.IsZero() }4041// lineKey is a column (row false) or row of a sheet, for undo; column42// wholeSheet is the sheet's own format.43type lineKey struct {44 s *Sheet45 row bool46 n int47}4849const wholeSheet = -15051// line is a column's (row false) or row's formatting, or the sheet's.52func (s *Sheet) line(row bool, n int) lineFmt {53 switch {54 case row:55 return s.lines.rows[n]56 case n == wholeSheet:57 return s.lines.sheet58 }59 return s.lines.cols[n]60}6162// inherited is what a cell at a shows without formatting of its own: its63// row's format and style, else its column's, else the sheet's.64func (s *Sheet) inherited(a Addr) lineFmt {65 if s.lines.none() {66 return lineFmt{}67 }68 return fallback(s.lines.rows[a.Row], s.lines.cols[a.Col], s.lines.sheet)69}7071// fallback is what a cell without formatting shows in a row, column and72// sheet with these formats: each part from the row, else the column,73// else the sheet.74func fallback(row, col, sheet lineFmt) lineFmt {75 l := row76 for _, next := range [2]lineFmt{col, sheet} {77 if l.Format.IsZero() {78 l.Format = next.Format79 }80 if l.Style.IsZero() {81 l.Style = next.Style82 }83 }84 return l85}8687// SheetFormat returns the format and style of the whole sheet, which88// every cell without its own, its row's or its column's shows.89func (s *Sheet) SheetFormat() (Format, Style) { return s.lines.sheet.Format, s.lines.sheet.Style }9091// effective is the format (without what formulas infer) and style the92// cell at a shows: its own, falling back part by part on its line's.93func (s *Sheet) effective(a Addr) lineFmt {94 f, sty, ok := s.cells.look(a)95 if ok && sty.own {96 sty.own = false97 return lineFmt{f, sty}98 }99 l := s.inherited(a)100 if !f.IsZero() {101 l.Format = f102 }103 if !sty.IsZero() {104 l.Style = sty105 }106 return l107}108109// CellFormat returns the number format the cell at a has, its own or110// its row's or column's, without the one a formula infers.111func (s *Sheet) CellFormat(a Addr) Format { return s.effective(a).Format }112113// CellStyle returns the text style the cell at a shows: its own, or its114// row's or column's.115func (s *Sheet) CellStyle(a Addr) Style { return s.effective(a).Style }116117// own is the formatting a cell stores to show want where it would118// otherwise show inh.119func own(want, inh lineFmt) (Format, Style) {120 f, st := want.Format, want.Style121 switch {122 case want == inh:123 return Format{}, Style{}124 case f.IsZero() && !inh.Format.IsZero(), st.IsZero() && !inh.Style.IsZero():125 st.own = true // Automatic, or plain text, over a formatted line126 return f, st127 }128 if f == inh.Format {129 f = Format{}130 }131 if st == inh.Style {132 st = Style{}133 }134 return f, st135}136137// ColFormats and RowFormats return the columns and rows that have a138// format or style of their own, by index.139func (s *Sheet) ColFormats() map[int]Format { return lineFormatsOf(s.lines.cols) }140func (s *Sheet) RowFormats() map[int]Format { return lineFormatsOf(s.lines.rows) }141142// ColStyles and RowStyles are the same for text styles.143func (s *Sheet) ColStyles() map[int]Style { return lineStylesOf(s.lines.cols) }144func (s *Sheet) RowStyles() map[int]Style { return lineStylesOf(s.lines.rows) }145146func lineFormatsOf(m map[int]lineFmt) map[int]Format {147 out := map[int]Format{}148 for n, l := range m {149 if !l.Format.IsZero() {150 out[n] = l.Format151 }152 }153 return out154}155156func lineStylesOf(m map[int]lineFmt) map[int]Style {157 out := map[int]Style{}158 for n, l := range m {159 if !l.Style.IsZero() {160 out[n] = l.Style161 }162 }163 return out164}165166// LoadLineFormat gives a whole column (row false) or row a format and167// style, as an importer does, without recording undo or touching its168// cells; column -1 is the whole sheet.169func (s *Sheet) LoadLineFormat(row bool, n int, f Format, st Style) {170 if row && n >= 0 && n < MaxRows || !row && n >= wholeSheet && n < MaxCols {171 s.setLine(row, n, lineFmt{f, st})172 }173}174175func (s *Sheet) lineRect(row bool, from, to int) Rect {176 switch {177 case row:178 return rowRect(from, to)179 case from == wholeSheet:180 return colRect(0, MaxCols-1)181 }182 return colRect(from, to)183}184185// setLine sets a column's or row's formatting, recording it for undo and,186// within a step, marking its cells changed, so formulas reading them infer187// their format again.188func (s *Sheet) setLine(row bool, n int, l lineFmt) {189 if s.line(row, n) == l {190 return191 }192 if st := s.wb.hist.open; st != nil {193 k := lineKey{s, row, n}194 if _, seen := st.lines[k]; !seen {195 st.lines[k] = s.line(row, n)196 }197 }198 m := &s.lines.cols199 switch {200 case row:201 m = &s.lines.rows202 case n == wholeSheet:203 s.lines.sheet = l204 m = nil205 }206 switch {207 case m == nil:208 case l.IsZero():209 delete(*m, n)210 if len(*m) == 0 {211 *m = nil212 }213 default:214 if *m == nil {215 *m = map[int]lineFmt{}216 }217 (*m)[n] = l218 }219 s.version++220 if s.wb.hist.open != nil {221 s.wb.hist.dirty = append(s.wb.hist.dirty, s.lineChanged(row, n)...)222 }223}224225// lineChanged is what to recalculate when a column's or row's format226// changes: its stored cells, and the formulas reading any cell of it,227// which infer their format from blank cells too.228func (s *Sheet) lineChanged(row bool, n int) []loc {229 return append(s.lineCells(row, n), s.readersIn(s.lineRect(row, n, n))...)230}231232// lineCells are the stored cells of a column or row.233func (s *Sheet) lineCells(row bool, n int) []loc {234 var out []loc235 for _, a := range s.cellsIn(s.lineRect(row, n, n)) {236 out = append(out, loc{s, a})237 }238 return out239}240241// formatLines applies fn to the formatting of r, which is whole columns242// or rows: to their line formats and to the cells in r that show243// something else, with where each is (see lineAddr). It reports false,244// doing nothing, for any other range.245func (s *Sheet) formatLines(r Rect, fn func(Addr, *lineFmt)) bool {246 cols, rows := r.AllRows(), r.AllCols()247 if !cols && !rows {248 return false249 }250 // The cells that may show something other than their line: those251 // stored, and where a formatted line crosses r the other way.252 targets := s.cellsIn(r)253 crossing, across := s.lines.rows, Rect{From: Addr{Col: r.From.Col}, To: Addr{Col: r.To.Col}}254 if !cols {255 crossing, across = s.lines.cols, Rect{From: Addr{Row: r.From.Row}, To: Addr{Row: r.To.Row}}256 }257 if !(cols && rows) {258 for _, n := range slices.Sorted(maps.Keys(crossing)) {259 if len(targets) > materializeLimit {260 break261 }262 for i := range across.To.Col - across.From.Col + across.To.Row - across.From.Row + 1 {263 a := Addr{Col: across.From.Col + i, Row: n} // a column's cells in formatted row n264 if !cols {265 a = Addr{Col: n, Row: across.From.Row + i} // a row's cells in formatted column n266 }267 if !s.cells.has(a) {268 targets = append(targets, a)269 }270 }271 }272 }273 want := make([]lineFmt, len(targets))274 for i, a := range targets {275 want[i] = s.effective(a)276 fn(a, &want[i])277 }278 s.eachLine(r, fn)279 for i, a := range targets {280 s.placeFormat(a, want[i])281 }282 return true283}284285// lineAddr is where a line format applies, for functions given the286// address of what they format: a column's has Row -1, a row's Col -1,287// and the whole sheet's both.288func lineAddr(row bool, n int) Addr {289 switch {290 case row:291 return Addr{Col: -1, Row: n}292 case n == wholeSheet:293 return Addr{Col: -1, Row: -1}294 }295 return Addr{Col: n, Row: -1}296}297298// eachLine applies fn to the line formats of r's whole columns, or rows;299// for the whole sheet, to its own format and to the columns and rows300// that have one, so they show the change too.301func (s *Sheet) eachLine(r Rect, fn func(Addr, *lineFmt)) {302 apply := func(row bool, n int, l lineFmt) {303 fn(lineAddr(row, n), &l)304 s.setLine(row, n, l)305 }306 if r.AllRows() && r.AllCols() {307 apply(false, wholeSheet, s.lines.sheet)308 for _, n := range slices.Sorted(maps.Keys(s.lines.cols)) {309 apply(false, n, s.lines.cols[n])310 }311 for _, n := range slices.Sorted(maps.Keys(s.lines.rows)) {312 apply(true, n, s.lines.rows[n])313 }314 return315 }316 if r.AllRows() {317 for c := r.From.Col; c <= r.To.Col; c++ {318 apply(false, c, s.lines.cols[c])319 }320 return321 }322 for n := r.From.Row; n <= r.To.Row; n++ {323 apply(true, n, s.lines.rows[n])324 }325}326327// placeFormat has the cell at a show want, giving it formatting of its328// own only where its lines don't already show it, and placing a copy so329// the change can be undone. A cell left with neither contents,330// formatting nor a note is removed.331func (s *Sheet) placeFormat(a Addr, want lineFmt) {332 old := s.cells.get(a)333 c := old.clone()334 if c == nil {335 c = &Cell{}336 }337 c.Format, c.Style = own(want, s.inherited(a))338 switch {339 case c.Blank() && c.Format.IsZero() && c.Style.IsZero() && c.Note == "":340 if old != nil {341 s.place(a, nil)342 }343 case old == nil || c.Format != old.Format || c.Style != old.Style:344 s.place(a, c)345 }346}347348// shiftLines moves column (or row) formats along with inserted or349// deleted columns (rows).350func (s *Sheet) shiftLines(rows bool, sp formula.Span) {351 m := s.lines.cols352 if rows {353 m = s.lines.rows354 }355 next := map[int]lineFmt{}356 for n, l := range m {357 if to, ok := sp.Point(n); ok {358 next[to] = l359 }360 }361 for _, n := range slices.Collect(maps.Keys(m)) {362 if _, ok := next[n]; !ok {363 s.setLine(rows, n, lineFmt{})364 }365 }366 for n, l := range next {367 s.setLine(rows, n, l)368 }369}