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 / sheet / rulemove.go
rulemove.go 7.1K
1package sheet23import (4 "strings"56 "src.rest/cat/012/internal/formula"7)89// Conditional formats and validation move with cut and paste, as in10// Sheets: the cut cells take their rules to where they land, on the same11// sheet or another, the cells they land on lose theirs, and formulas in12// the rules follow the cells they read.1314// moveRules moves the sheet's rules with the cells in src, landing on15// dst; cell and rng are the move's, for the rules' formulas.16func (s *Sheet) moveRules(src, dst Rect, cell func(Addr) (Addr, bool), rng func(Rect) (Rect, bool)) {17 if s.rules.empty() {18 return19 }20 next := s.rules.moved(src, dst, formula.Relocate(s.onThis(s), cell, rng))21 if !next.equal(s.rules) {22 s.recordRules()23 s.rules = next24 }25}2627// moved is the rules after the cells in src move to dst, with their28// formulas rewritten by rw. A rule whose cells are partly moved keeps one29// range list, or, when its formula is relative to its first cell, splits30// into a rule for the cells that stayed and one for those that moved,31// each with the formula written for its own first cell.32func (r rulesState) moved(src, dst Rect, rw formula.Rewriter) rulesState {33 d := Addr{Col: dst.From.Col - src.From.Col, Row: dst.From.Row - src.From.Row}34 var out rulesState35 for _, f := range r.formats {36 for _, p := range movedParts(f.Ranges, src, dst, d, f.Op == RuleFormula) {37 g := f38 g.Ranges = p.ranges39 if f.Op == RuleFormula {40 g.Args[0] = reanchor(f.Args[0], p.shift, rw)41 }42 out.formats = append(out.formats, g)43 }44 }45 for _, v := range r.validations {46 for _, p := range movedParts(v.Ranges, src, dst, d, v.Kind == ValidFormula) {47 w := v.clone()48 w.Ranges = p.ranges49 switch v.Kind {50 case ValidFormula:51 w.Args[0] = reanchor(v.Args[0], p.shift, rw)52 case ValidRange:53 w.Source = strings.TrimPrefix(shiftFormula("="+v.Source, rw), "=")54 }55 out.validations = append(out.validations, w)56 }57 }58 return out59}6061// rulePart is the ranges of a rule after a move, and how far its62// formula's relative references shift: from the rule's first cell to63// where the part's first cell was before the move.64type rulePart struct {65 ranges []Rect66 shift Addr67}6869// movedParts splits a rule's ranges rs by the move of src to dst (d70// away): the cells that stay, outside both, and those that move. split71// keeps the two apart, for a rule with a relative formula.72func movedParts(rs []Rect, src, dst Rect, d Addr, split bool) []rulePart {73 keptPart, movedPart := splitMoved(rs, src, dst, d)74 kept, moved := keptPart.ranges, movedPart.ranges75 switch {76 case len(moved) == 0 && len(kept) == 0:77 return nil78 case len(moved) == 0:79 return []rulePart{keptPart}80 case len(kept) == 0:81 return []rulePart{movedPart}82 case split:83 return []rulePart{keptPart, movedPart}84 }85 return []rulePart{{ranges: append(kept, moved...)}}86}8788// splitMoved is the part of ranges rs that stays when src moves to dst89// (d away), outside both, and the part that moves, each with the shift90// from rs's first cell to where the part's first cell was.91func splitMoved(rs []Rect, src, dst Rect, d Addr) (kept, moved rulePart) {92 for _, r := range rs {93 for _, k := range subtract(r, src) {94 kept.ranges = append(kept.ranges, subtract(k, dst)...)95 }96 if in, ok := overlap(r, src); ok {97 moved.ranges = append(moved.ranges, Rect{From: plus(in.From, d), To: plus(in.To, d)})98 }99 }100 from := anchor(rs)101 kept.shift = minus(anchor(kept.ranges), from)102 moved.shift = minus(minus(anchor(moved.ranges), d), from)103 return kept, moved104}105106// moveRules moves the rules of the cells in src to the sheet they land107// on: they leave the source sheet's rules, the cells they land on leave108// the destination's, and their parts of the source's rules join the109// destination's, each rule after the destination's own, as a copy's110// rules would. Formulas in every sheet's rules follow the cells they111// read, as the formulas in cells do, and the moved rules' name the112// source sheet for cells that stayed behind.113func (mv sheetMove) moveRules() {114 var landed rulesState115 for _, t := range mv.from.wb.sheets {116 if t.rules.empty() {117 continue118 }119 next := mv.splitRules(t, &landed)120 if t == mv.to {121 next.formats = append(next.formats, landed.formats...)122 next.validations = append(next.validations, landed.validations...)123 landed = rulesState{}124 }125 if !next.equal(t.rules) {126 t.recordRules()127 t.rules = next128 }129 }130 if !landed.empty() { // the destination had no rules of its own131 mv.to.recordRules()132 mv.to.rules = landed133 }134}135136// splitRules is t's rules after the move: what stays on t, rewritten for137// the cells that moved, with the parts that move added to landed.138func (mv sheetMove) splitRules(t *Sheet, landed *rulesState) rulesState {139 d := minus(mv.d.From, mv.src.From)140 nowhere := Rect{From: Addr{Col: -1, Row: -1}, To: Addr{Col: -1, Row: -1}}141 src, dst := nowhere, nowhere142 switch t {143 case mv.from:144 src = mv.src145 case mv.to:146 dst = mv.d147 }148 rw, away := mv.rewrite(t, t), mv.rewrite(mv.from, mv.to)149 var next rulesState150 for _, f := range t.rules.formats {151 kept, moved := splitMoved(f.Ranges, src, dst, d)152 if len(kept.ranges) > 0 {153 next.formats = append(next.formats, movedFormat(f, kept, rw))154 }155 if len(moved.ranges) > 0 {156 landed.formats = append(landed.formats, movedFormat(f, moved, away))157 }158 }159 for _, v := range t.rules.validations {160 kept, moved := splitMoved(v.Ranges, src, dst, d)161 if len(kept.ranges) > 0 {162 next.validations = append(next.validations, movedValidation(v, kept, rw))163 }164 if len(moved.ranges) > 0 {165 landed.validations = append(landed.validations, movedValidation(v, moved, away))166 }167 }168 return next169}170171// movedFormat is f over part p's ranges, its formula written for their172// first cell and rewritten by rw.173func movedFormat(f CondFormat, p rulePart, rw formula.Rewriter) CondFormat {174 g := f.clone()175 g.Ranges = p.ranges176 if f.Op == RuleFormula {177 g.Args[0] = reanchor(f.Args[0], p.shift, rw)178 } else {179 for k := range f.Op.Args() {180 if IsFormulaEntry(strings.TrimSpace(f.Args[k])) {181 g.Args[k] = reanchor(f.Args[k], p.shift, rw)182 }183 }184 }185 return g186}187188// movedValidation is movedFormat for a validation rule.189func movedValidation(v Validation, p rulePart, rw formula.Rewriter) Validation {190 w := v.clone()191 w.Ranges = p.ranges192 switch v.Kind {193 case ValidFormula:194 w.Args[0] = reanchor(v.Args[0], p.shift, rw)195 case ValidRange:196 w.Source = strings.TrimPrefix(shiftFormula("="+v.Source, rw), "=")197 }198 return w199}200201// reanchor rewrites a rule's formula (with its "=") for a first cell202// shift away, then for the move (rw).203func reanchor(src string, shift Addr, rw formula.Rewriter) string {204 if shift != (Addr{}) {205 src = shiftFormula(src, formula.Shift(shift.Col, shift.Row))206 }207 return shiftFormula(src, rw)208}209210func plus(a, d Addr) Addr { return Addr{Col: a.Col + d.Col, Row: a.Row + d.Row} }211func minus(a, d Addr) Addr { return Addr{Col: a.Col - d.Col, Row: a.Row - d.Row} }212213// overlap is the cells a and b share, and whether they share any.214func overlap(a, b Rect) (Rect, bool) {215 r := Rect{216 From: Addr{Col: max(a.From.Col, b.From.Col), Row: max(a.From.Row, b.From.Row)},217 To: Addr{Col: min(a.To.Col, b.To.Col), Row: min(a.To.Row, b.To.Row)},218 }219 return r, r.From.Col <= r.To.Col && r.From.Row <= r.To.Row220}