sheet.go was past 500 lines; recalculation is its own concern. Marking dirty cells becomes a small marking type whose readersOf queues the formulas reading a cell by reference, range, named range or sheet name, and whether any formula names a sheet is looked up once per recalculation instead of upper-casing its name for every marked cell.
- commit
- cbb6aa77a43924ec885a6f8eb53f2d599acc8b1e
- parent
- a7278b9d023217607598e946d7ac99fc55cc6ef1
- author
- Dami <dami.etoile@gmail.com>
| A | internal/ | 266 | ++++++++++++++++++++++++++++++ |
| M | internal/ | 225 | ------------------------- |
@@ -0,0 +1,266 @@
1+package sheet2+3+import (4+ "strings"5+6+ "github.com/FelineStateMachine/012/internal/formula"7+)8+9+// Recalculation: a change marks the changed cells and everything that10+// transitively reads them dirty, on any sheet, then evaluates the dirty11+// cells lazily in dependency order.12+13+// RecalcAll recomputes every formula in the workbook. Loaders call it14+// once the sheets are built, so it also starts a fresh undo history:15+// loading isn't undoable.16+func (s *Sheet) RecalcAll() { s.wb.RecalcAll() }17+18+// RecalcAll recomputes every formula and clears the undo history.19+func (w *Workbook) RecalcAll() {20+ w.recalcAll()21+ w.ClearHistory()22+}23+24+// recalcAll recomputes every formula on every sheet, as after loading or25+// after a sheet is added, renamed or deleted, when references by name may26+// resolve differently. Every cell is dirty, so there is nothing to27+// propagate: tracing dependents from each cell cost O(cells x range28+// users).29+func (w *Workbook) recalcAll() {30+ start := recalcStart()31+ n := 032+ for _, s := range w.sheets {33+ s.calc = make(map[Addr]int, len(s.cells))34+ for a := range s.cells {35+ s.calc[a] = dirty36+ }37+ n += len(s.calc)38+ }39+ w.evaluate()40+ w.observe(true, start, n)41+}42+43+// Recalculation states of a cell.44+const (45+ dirty = iota + 146+ visiting47+ done48+)49+50+// recalc recomputes the changed cells, volatile formulas, and everything51+// that transitively depends on them, on any sheet.52+func (w *Workbook) recalc(changed []loc) {53+ start := recalcStart()54+ n := w.affected(changed)55+ w.evaluate()56+ w.observe(false, start, n)57+}58+59+// affected marks dirty, in each sheet's calc, the changed cells, volatile60+// formulas, and every formula that transitively reads them, on any sheet,61+// and returns how many it marked.62+func (w *Workbook) affected(changed []loc) int {63+ m := marking{w: w, queue: append([]loc(nil), changed...), named: w.namedInUse(), byName: map[*Sheet]bool{}}64+ for _, s := range w.sheets {65+ s.calc = make(map[Addr]int)66+ m.byName[s] = w.crossKeys[formula.SheetKey(s.name)] > 067+ for a := range s.volatile {68+ m.queue = append(m.queue, loc{s, a})69+ }70+ }71+ n := 072+ for len(m.queue) > 0 {73+ l := m.queue[0]74+ m.queue = m.queue[1:]75+ if !l.s.live || l.s.calc[l.a] == dirty {76+ continue77+ }78+ l.s.calc[l.a] = dirty79+ n++80+ m.readersOf(l)81+ }82+ return n83+}84+85+// marking is the state of affected: the cells still to mark, and what it86+// looks up once rather than for every cell.87+type marking struct {88+ w *Workbook89+ queue []loc90+ named []namedUsers91+ byName map[*Sheet]bool // whether some formula names the sheet92+}93+94+// namedUsers are the formulas that use a named range, with its cells.95+type namedUsers struct {96+ s *Sheet97+ r Rect98+ users map[loc]struct{}99+}100+101+// namedInUse returns the defined named ranges that formulas use.102+func (w *Workbook) namedInUse() []namedUsers {103+ var named []namedUsers104+ for k, users := range w.nameUsers {105+ if nm, ok := w.names[k]; ok && !nm.Gone() {106+ named = append(named, namedUsers{nm.Sheet, nm.Range, users})107+ }108+ }109+ return named110+}111+112+// push queues the formula at u unless it's already marked, so each is113+// queued once rather than once per changed cell it reads.114+func (m *marking) push(u loc) {115+ if u.s.calc[u.a] != dirty {116+ m.queue = append(m.queue, u)117+ }118+}119+120+// readersOf queues the formulas that read the cell l: by reference, by a121+// range over it, through a named range, or by naming its sheet.122+func (m *marking) readersOf(l loc) {123+ s, a := l.s, l.a124+ for d := range s.dependents[a] {125+ m.push(loc{s, d})126+ }127+ for u := range s.rangeUsers.candidates(a.Col) {128+ if s.cells[u].rangeHas(a) {129+ m.push(loc{s, u})130+ }131+ }132+ for _, nu := range m.named {133+ if nu.s == s && nu.r.Contains(a) {134+ for u := range nu.users {135+ m.push(u)136+ }137+ }138+ }139+ if !m.byName[s] {140+ return // no formula names this sheet141+ }142+ for u := range m.w.crossUsers {143+ if m.w.crossReads(u, l) {144+ m.push(u)145+ }146+ }147+}148+149+// rangeHas reports whether one of the ranges the formula reads on its own150+// sheet contains a.151+func (c *Cell) rangeHas(a Addr) bool {152+ for _, r := range c.ranges {153+ if r.Contains(a) {154+ return true155+ }156+ }157+ return false158+}159+160+// evaluate computes the cells marked dirty in each sheet's calc. Cells161+// are evaluated lazily in dependency order: reading a dirty cell162+// evaluates it first. A cell that is reached again while it is still163+// being evaluated is part of a cycle and becomes ERR. The state lives on164+// the sheets, keyed by address as on one sheet, and each sheet's lookup165+// is made once, so evaluating a formula allocates nothing for sheets.166+func (w *Workbook) evaluate() {167+ w.Circular = false168+ var compute func(s *Sheet, a Addr) Value169+ compute = func(s *Sheet, a Addr) Value {170+ // Every cell a formula reads comes through here, so it looks171+ // each map up once: a SUM over 8192 cells makes 8192 calls.172+ c := s.cells[a]173+ switch st := s.calc[a]; {174+ case c == nil:175+ return Value{}176+ case st == visiting:177+ w.Circular = true178+ return ErrRef179+ case st != dirty:180+ return c.Value181+ }182+ s.calc[a] = visiting183+ c.auto = Format{}184+ switch {185+ case c.Input == "":186+ c.Value = Value{}187+ case c.expr == nil && c.Format.Kind == FmtText:188+ c.Value = Value{Kind: Text, Str: c.Input}189+ case c.expr == nil:190+ c.Value = Value{Kind: Text, Str: strings.TrimPrefix(c.Input, "'")}191+ default:192+ expr := s.bound(c)193+ c.Value = eval(w.arith(expr), s.calcGet)194+ if _, lit := expr.(formula.Num); !lit {195+ c.auto = inferFormat(expr, s.calcFmt)196+ }197+ }198+ s.calc[a] = done199+ return c.Value200+ }201+ for _, s := range w.sheets {202+ s.version++203+ s.hidden.valid = false // values may have changed what the filter hides204+ s.calcGet = w.lookupOn(s, compute)205+ s.calcFmt = w.formatFrom(s)206+ }207+ for _, s := range w.sheets {208+ for a := range s.calc {209+ compute(s, a)210+ }211+ }212+ for _, s := range w.sheets {213+ s.calc, s.calcGet, s.calcFmt = nil, nil, nil214+ }215+}216+217+// crossReads reports whether the formula at u reads the cell l through a218+// reference that names l's sheet.219+func (w *Workbook) crossReads(u, l loc) bool {220+ c := u.s.cells[u.a]221+ if c == nil {222+ return false223+ }224+ for _, x := range c.xrefs {225+ if x.r.Contains(l.a) && w.byKey[x.key] == l.s {226+ return true227+ }228+ }229+ return false230+}231+232+// lookupOn resolves references in a formula on s with get: references233+// without a sheet read s, others the sheet they name (#REF! when no sheet234+// has that name). The last sheet name is remembered, so a range on235+// another sheet resolves its name once.236+func (w *Workbook) lookupOn(s *Sheet, get func(*Sheet, Addr) Value) lookup {237+ var lastName string238+ var last *Sheet239+ return func(sheet string, a Addr) Value {240+ if sheet == "" {241+ return get(s, a)242+ }243+ if sheet != lastName || last == nil {244+ lastName, last = sheet, w.byKey[formula.SheetKey(sheet)]245+ }246+ if last == nil {247+ return ErrRef248+ }249+ return get(last, a)250+ }251+}252+253+// values reads current values for formulas on s, across sheets.254+func (w *Workbook) values(s *Sheet) lookup {255+ return w.lookupOn(s, func(t *Sheet, a Addr) Value { return t.Value(a) })256+}257+258+// formatFrom reads display formats for formulas on s, across sheets.259+func (w *Workbook) formatFrom(s *Sheet) func(string, Addr) Format {260+ return func(sheet string, a Addr) Format {261+ if t := w.resolve(s, sheet); t != nil {262+ return t.DisplayFormat(a)263+ }264+ return Format{}265+ }266+}@@ -357,228 +357,3 @@ func (s *Sheet) unlink(a Addr) {
357357 delete(s.volatile, a)358358 delete(s.cells, a)359359 }360-361-// RecalcAll recomputes every formula in the workbook. Loaders call it362-// once the sheets are built, so it also starts a fresh undo history:363-// loading isn't undoable.364-func (s *Sheet) RecalcAll() { s.wb.RecalcAll() }365-366-// RecalcAll recomputes every formula and clears the undo history.367-func (w *Workbook) RecalcAll() {368- w.recalcAll()369- w.ClearHistory()370-}371-372-// recalcAll recomputes every formula on every sheet, as after loading or373-// after a sheet is added, renamed or deleted, when references by name may374-// resolve differently. Every cell is dirty, so there is nothing to375-// propagate: tracing dependents from each cell cost O(cells x range376-// users).377-func (w *Workbook) recalcAll() {378- start := recalcStart()379- n := 0380- for _, s := range w.sheets {381- s.calc = make(map[Addr]int, len(s.cells))382- for a := range s.cells {383- s.calc[a] = dirty384- }385- n += len(s.calc)386- }387- w.evaluate()388- w.observe(true, start, n)389-}390-391-// Recalculation states of a cell.392-const (393- dirty = iota + 1394- visiting395- done396-)397-398-// recalc recomputes the changed cells, volatile formulas, and everything399-// that transitively depends on them, on any sheet.400-func (w *Workbook) recalc(changed []loc) {401- start := recalcStart()402- n := w.affected(changed)403- w.evaluate()404- w.observe(false, start, n)405-}406-407-// affected marks dirty, in each sheet's calc, the changed cells, volatile408-// formulas, and every formula that transitively reads them, on any sheet,409-// and returns how many it marked.410-func (w *Workbook) affected(changed []loc) int {411- for _, s := range w.sheets {412- s.calc = make(map[Addr]int)413- }414- queue := append([]loc(nil), changed...)415- for _, s := range w.sheets {416- for a := range s.volatile {417- queue = append(queue, loc{s, a})418- }419- }420- // The named ranges in use, looked up once rather than for every cell.421- type namedUsers struct {422- s *Sheet423- r Rect424- users map[loc]struct{}425- }426- var named []namedUsers427- for k, users := range w.nameUsers {428- if nm, ok := w.names[k]; ok && !nm.Gone() {429- named = append(named, namedUsers{nm.Sheet, nm.Range, users})430- }431- }432- // Queue each formula once, not once per changed cell it reads.433- push := func(u loc) {434- if u.s.calc[u.a] != dirty {435- queue = append(queue, u)436- }437- }438- n := 0439- for len(queue) > 0 {440- l := queue[0]441- queue = queue[1:]442- s, a := l.s, l.a443- if !s.live || s.calc[a] == dirty {444- continue445- }446- s.calc[a] = dirty447- n++448- for d := range s.dependents[a] {449- push(loc{s, d})450- }451- for u := range s.rangeUsers.candidates(a.Col) {452- for _, r := range s.cells[u].ranges {453- if r.Contains(a) {454- push(loc{s, u})455- break456- }457- }458- }459- for _, nu := range named {460- if nu.s == s && nu.r.Contains(a) {461- for u := range nu.users {462- push(u)463- }464- }465- }466- if w.crossKeys[formula.SheetKey(s.name)] == 0 {467- continue // no formula names this sheet468- }469- for u := range w.crossUsers {470- if w.crossReads(u, l) {471- push(u)472- }473- }474- }475- return n476-}477-478-// evaluate computes the cells marked dirty in each sheet's calc. Cells479-// are evaluated lazily in dependency order: reading a dirty cell480-// evaluates it first. A cell that is reached again while it is still481-// being evaluated is part of a cycle and becomes ERR. The state lives on482-// the sheets, keyed by address as on one sheet, and each sheet's lookup483-// is made once, so evaluating a formula allocates nothing for sheets.484-func (w *Workbook) evaluate() {485- w.Circular = false486- var compute func(s *Sheet, a Addr) Value487- compute = func(s *Sheet, a Addr) Value {488- // Every cell a formula reads comes through here, so it looks489- // each map up once: a SUM over 8192 cells makes 8192 calls.490- c := s.cells[a]491- switch st := s.calc[a]; {492- case c == nil:493- return Value{}494- case st == visiting:495- w.Circular = true496- return ErrRef497- case st != dirty:498- return c.Value499- }500- s.calc[a] = visiting501- c.auto = Format{}502- switch {503- case c.Input == "":504- c.Value = Value{}505- case c.expr == nil && c.Format.Kind == FmtText:506- c.Value = Value{Kind: Text, Str: c.Input}507- case c.expr == nil:508- c.Value = Value{Kind: Text, Str: strings.TrimPrefix(c.Input, "'")}509- default:510- expr := s.bound(c)511- c.Value = eval(w.arith(expr), s.calcGet)512- if _, lit := expr.(formula.Num); !lit {513- c.auto = inferFormat(expr, s.calcFmt)514- }515- }516- s.calc[a] = done517- return c.Value518- }519- for _, s := range w.sheets {520- s.version++521- s.hidden.valid = false // values may have changed what the filter hides522- s.calcGet = w.lookupOn(s, compute)523- s.calcFmt = w.formatFrom(s)524- }525- for _, s := range w.sheets {526- for a := range s.calc {527- compute(s, a)528- }529- }530- for _, s := range w.sheets {531- s.calc, s.calcGet, s.calcFmt = nil, nil, nil532- }533-}534-535-// crossReads reports whether the formula at u reads the cell l through a536-// reference that names l's sheet.537-func (w *Workbook) crossReads(u, l loc) bool {538- c := u.s.cells[u.a]539- if c == nil {540- return false541- }542- for _, x := range c.xrefs {543- if x.r.Contains(l.a) && w.byKey[x.key] == l.s {544- return true545- }546- }547- return false548-}549-550-// lookupOn resolves references in a formula on s with get: references551-// without a sheet read s, others the sheet they name (#REF! when no sheet552-// has that name). The last sheet name is remembered, so a range on553-// another sheet resolves its name once.554-func (w *Workbook) lookupOn(s *Sheet, get func(*Sheet, Addr) Value) lookup {555- var lastName string556- var last *Sheet557- return func(sheet string, a Addr) Value {558- if sheet == "" {559- return get(s, a)560- }561- if sheet != lastName || last == nil {562- lastName, last = sheet, w.byKey[formula.SheetKey(sheet)]563- }564- if last == nil {565- return ErrRef566- }567- return get(last, a)568- }569-}570-571-// values reads current values for formulas on s, across sheets.572-func (w *Workbook) values(s *Sheet) lookup {573- return w.lookupOn(s, func(t *Sheet, a Addr) Value { return t.Value(a) })574-}575-576-// formatFrom reads display formats for formulas on s, across sheets.577-func (w *Workbook) formatFrom(s *Sheet) func(string, Addr) Format {578- return func(sheet string, a Addr) Format {579- if t := w.resolve(s, sheet); t != nil {580- return t.DisplayFormat(a)581- }582- return Format{}583- }584-}