1package sheet23import (4 "slices"5 "strconv"6 "strings"78 "src.rest/cat/012/internal/formula"9)1011// Keeping tables (table.go) in step with their cells. At the end of12// every change, before recalculating, a table grows over the rows just13// below it that the change filled, and its columns' names are read14// again from its header row when the change touched it: a name changed15// in place renames the column in the formulas that read it, and a16// blank or repeated header is given a name of its own. Undo puts back17// the tables with the view, and the formulas and cells with theirs, so18// it runs none of this.1920// settleTables brings the tables on the sheets the open step changed21// in step with their cells, within the step.22func (w *Workbook) settleTables() {23 var touched map[*Sheet][]Addr24 for _, l := range w.hist.dirty {25 if len(l.s.view.tables) == 0 || !l.s.live {26 continue27 }28 if touched == nil {29 touched = map[*Sheet][]Addr{}30 }31 touched[l.s] = append(touched[l.s], l.a)32 }33 for s, at := range touched {34 for _, t := range s.Tables() {35 s.growTable(t, at)36 }37 for _, t := range s.Tables() {38 if headerTouched(t, at) {39 s.syncHeader(t)40 }41 }42 }43}4445// headerTouched reports whether a cell of t's header row is among at,46// or t's columns don't match its width.47func headerTouched(t Table, at []Addr) bool {48 if len(t.Cols) != t.Range.To.Col-t.Range.From.Col+1 {49 return true50 }51 return slices.ContainsFunc(at, func(a Addr) bool {52 return a.Row == t.Range.From.Row && a.Col >= t.Range.From.Col && a.Col <= t.Range.To.Col53 })54}5556// growTable extends t over the rows just below it that cells among at,57// filled in t's columns, make a run from its last row.58func (s *Sheet) growTable(t Table, at []Addr) {59 filled := map[int]bool{}60 for _, a := range at {61 if a.Row > t.Range.To.Row && a.Col >= t.Range.From.Col && a.Col <= t.Range.To.Col && s.cells.filledAt(a) {62 filled[a.Row] = true63 }64 }65 end := t.Range.To.Row66 for filled[end+1] {67 end++68 }69 if end == t.Range.To.Row {70 return71 }72 r := t.Range73 r.To.Row = end74 if s.checkTableRange(r, nameKey(t.Name)) != nil {75 return76 }77 s.editTable(nameKey(t.Name), func(t *Table) { t.Range = r })78}7980// syncHeader reads t's columns' names again from its header row,81// renaming in formulas each column whose name changed in place.82func (s *Sheet) syncHeader(t Table) {83 names := s.headerNames(t)84 if slices.Equal(names, t.Cols) {85 return86 }87 k := nameKey(t.Name)88 for old, name := range renamedColumns(t.Cols, names) {89 s.wb.renameColumnInFormulas(k, old, name)90 }91 s.editTable(k, func(t *Table) { t.Cols = names })92}9394// renamedColumns pairs the names in before that are gone from after95// with the names in their place that are new, by position: a column96// renamed in its header. Names that moved, as when columns are inserted,97// deleted or moved, rename nothing.98func renamedColumns(before, after []string) map[string]string {99 out := map[string]string{}100 has := func(list []string, name string) bool {101 return slices.ContainsFunc(list, func(n string) bool { return strings.EqualFold(n, name) })102 }103 for i := range min(len(before), len(after)) {104 old, name := before[i], after[i]105 if old == "" || old == name || has(after, old) || has(before, name) && !strings.EqualFold(old, name) {106 continue107 }108 out[old] = name109 }110 return out111}112113// headerNames reads the names of t's columns from its header row (see114// columnNames), writing the names it gives blank or repeated headers115// into their cells, so the header shows what formulas use.116func (s *Sheet) headerNames(t Table) []string {117 texts := s.rowTexts(Rect{From: t.Range.From, To: Addr{Col: t.Range.To.Col, Row: t.Range.From.Row}})118 names := columnNames(texts, t.Cols)119 for i, name := range names {120 a := Addr{Col: t.Range.From.Col + i, Row: t.Range.From.Row}121 if name == texts[i] {122 continue123 }124 if _, spilled := s.SpillAnchor(a); !spilled {125 s.put(a, name)126 }127 }128 return names129}130131// columnNames are the names of a table's columns whose header cells132// show texts: each text, with a blank one named after its place133// (Column3) and a repeat numbered (Amount2). Of two headers the same, the134// one already a column's name in prev, the names before, keeps it.135func columnNames(texts, prev []string) []string {136 names := make([]string, len(texts))137 seen := map[string]bool{}138 for i, name := range texts {139 if k := nameKey(name); name != "" && i < len(prev) && nameKey(prev[i]) == k && !seen[k] {140 seen[k], names[i] = true, name141 }142 }143 for i, name := range texts {144 if k := nameKey(name); name != "" && names[i] == "" && !seen[k] {145 seen[k], names[i] = true, name146 }147 }148 for i, name := range names {149 if name != "" {150 continue151 }152 base := texts[i]153 if base == "" {154 base = "Column"155 }156 name = uniqueColumn(base, i+1, seen)157 seen[nameKey(name)], names[i] = true, name158 }159 return names160}161162// uniqueColumn is base numbered so no name in seen has it: base with163// the column's place for a blank header (Column3), else from 2 up.164func uniqueColumn(base string, place int, seen map[string]bool) string {165 from := 2166 if base == "Column" {167 from = place168 }169 for i := from; ; i++ {170 if name := base + strconv.Itoa(i); !seen[nameKey(name)] {171 return name172 }173 }174}175176// shiftTables moves tables with inserted or deleted rows or columns, as177// a range reference moves. A table whose header row or every column is178// deleted goes; one left without data rows keeps an empty one. Columns179// inserted into a table come in unnamed, to be named from their header180// cells when the change ends.181func shiftTables(ts []Table, rows bool, sp formula.Span) []Table {182 if len(ts) == 0 {183 return ts184 }185 _, rng := formula.AxisMaps(rows, sp)186 var out []Table187 for _, t := range ts {188 if rows && sp.N < 0 && t.Range.From.Row >= sp.At && t.Range.From.Row < sp.At-sp.N {189 continue // the header row went190 }191 r, ok := rng(t.Range)192 if !ok {193 continue194 }195 t = t.clone()196 if !rows {197 t.Cols = shiftColumns(t, r, sp)198 }199 t.Range = tableRange(r)200 out = append(out, t)201 }202 return out203}204205// shiftColumns are t's columns' names once its range is r, after206// columns were inserted or deleted: each kept column's name where the207// column went, and "" for columns inserted.208func shiftColumns(t Table, r Rect, sp formula.Span) []string {209 out := make([]string, r.To.Col-r.From.Col+1)210 for i, name := range t.Cols {211 if c, ok := sp.Point(t.Range.From.Col + i); ok && c >= r.From.Col && c <= r.To.Col {212 out[c-r.From.Col] = name213 }214 }215 return out216}217218// remapTables moves the tables wholly inside the cells moved with them,219// as cut and paste moves them in Sheets; rng maps a range that moved.220func (s *Sheet) remapTables(src Rect, rng func(Rect) (Rect, bool)) {221 if len(s.view.tables) == 0 {222 return223 }224 st := s.Tables()225 for i, t := range st {226 if src.Contains(t.Range.From) && src.Contains(t.Range.To) {227 if r, ok := rng(t.Range); ok {228 st[i].Range = r229 }230 }231 }232 s.putTables(st)233}234235// moveTablesTo moves the tables wholly inside src on s to dst, shifted236// by shift, as cutting them from one sheet and pasting on another does.237func (s *Sheet) moveTablesTo(dst *Sheet, src Rect, shift func(Addr) Addr) {238 var moved, kept []Table239 for _, t := range s.Tables() {240 if src.Contains(t.Range.From) && src.Contains(t.Range.To) {241 t.Range = Rect{From: shift(t.Range.From), To: shift(t.Range.To)}242 moved = append(moved, t)243 } else {244 kept = append(kept, t)245 }246 }247 if len(moved) == 0 {248 return249 }250 s.putTables(kept)251 dst.putTables(append(dst.Tables(), moved...))252}