1package sheet23import (4 "errors"5 "fmt"6)78// Where a pivot table reads its source's rows: a sheet's cells, or a9// linked source's rows as they stream past (source.go), which a pivot10// over a source gathers in the background (GatherPivot) and lays out11// once the host answers.1213// pivotData is a pivot's source, read a row at a time.14type pivotData interface {15 value(a Addr) Value16 // format is what the value at a shows in.17 format(a Addr) Format18 // colFormat is the number format of column col's data in r.19 colFormat(col int, r Rect) Format20 // lastRow is r's last row that may hold data.21 lastRow(r Rect) int22 // blankRow reports whether row is blank across r's columns.23 blankRow(row int, r Rect) bool24 // passes reports whether row meets the filters' tests.25 passes(row int, tests []colTest) bool26}2728// sheetData is a sheet's cells as a pivot's source.29type sheetData struct{ s *Sheet }3031func (d sheetData) value(a Addr) Value { return d.s.Value(a) }32func (d sheetData) format(a Addr) Format { return d.s.DisplayFormat(a) }33func (d sheetData) lastRow(r Rect) int { return d.s.filterData(r).To.Row }34func (d sheetData) blankRow(row int, r Rect) bool { return d.s.rowBlank(row, r) }35func (d sheetData) passes(row int, tests []colTest) bool { return d.s.rowPasses(row, tests) }3637// colFormat is the column's own format when it has one, else the one38// most of its numbers show in, ties going to the one met first. So one39// cell typed differently ("$9,000" among "$2.50"s) doesn't restyle40// every result, as in Sheets.41func (d sheetData) colFormat(col int, r Rect) Format {42 s := d.s43 if f := s.lines.cols[col].Format; !f.IsZero() {44 return f45 }46 counts := map[Format]int{}47 var seen []Format // in the order first met48 for row, ok := s.cells.filled.nextRow(col, r.From.Row+1, 1); ok && row <= r.To.Row; row, ok = s.cells.filled.nextRow(col, row+1, 1) {49 a := Addr{Col: col, Row: row}50 if s.Value(a).Kind != Number {51 continue52 }53 f := s.DisplayFormat(a)54 if counts[f] == 0 {55 seen = append(seen, f)56 }57 counts[f]++58 }59 var best Format60 most := 061 for _, f := range seen {62 if counts[f] > most {63 best, most = f, counts[f]64 }65 }66 return best67}6869// rowData is a source's rows as a pivot's source: the row streaming70// past, its values by column.71type rowData struct {72 shape SourceShape73 row []LiveCell74}7576func (d *rowData) cell(col int) LiveCell {77 if col < len(d.row) {78 return d.row[col]79 }80 return LiveCell{}81}8283func (d *rowData) value(a Addr) Value { return d.cell(a.Col).V }8485func (d *rowData) format(a Addr) Format {86 if c := d.cell(a.Col); !c.F.IsZero() {87 return c.F88 }89 return d.colFormat(a.Col, Rect{})90}9192func (d *rowData) colFormat(col int, _ Rect) Format {93 if col < len(d.shape.Formats) {94 return d.shape.Formats[col]95 }96 return Format{}97}9899func (d *rowData) lastRow(Rect) int { return d.shape.Rows }100101func (d *rowData) blankRow(int, Rect) bool {102 for _, c := range d.row {103 if c.V.Kind != Empty {104 return false105 }106 }107 return true108}109110func (d *rowData) passes(_ int, tests []colTest) bool {111 for _, t := range tests {112 c := d.cell(t.col)113 f := c.F114 if f.IsZero() {115 f = d.colFormat(t.col, Rect{})116 }117 shown := ""118 if c.V.Kind != Empty {119 shown = FormatText(c.V, f)120 }121 if t.hidden[shown] || !t.cond(c.V, shown) {122 return false123 }124 }125 return true126}127128// PivotGroups is a pivot table's source rows gathered into groups, as129// GatherPivot leaves them for the layout.130type PivotGroups struct {131 calc *pivotCalc132}133134// Records counts the source rows the groups summarize.135func (g *PivotGroups) Records() int { return g.calc.records }136137// PivotCols are the source columns a pivot reads, for a source to138// stream no others.139func PivotCols(p Pivot) []int { return p.fieldCols() }140141// GatherPivot gathers a pivot's groups from a linked source of shape142// shape, whose rows scan streams: it calls its argument with each row143// (counting from 0, under the header) and its values by column (those144// PivotCols names, the rest blank), stopping when told. It touches no145// workbook, so it runs in the background.146func GatherPivot(p Pivot, shape SourceShape, scan func(each func(row int, vals []LiveCell) bool) error) (*PivotGroups, error) {147 d := &rowData{shape: shape}148 c := newPivotCalc(nil, &p, d)149 tests := pivotTests(p.Filters)150 err := scan(func(row int, vals []LiveCell) bool {151 d.row = vals152 c.gatherRow(row+1, tests)153 return true154 })155 if err != nil {156 return nil, err157 }158 d.row = nil159 c.sortRows(c.root, 0)160 c.sortCols()161 return &PivotGroups{calc: c}, nil162}163164// pivotKey identifies a pivot's definition, for the answers a source's165// host keeps.166func pivotKey(p Pivot) string { return fmt.Sprintf("%+v", p) }167168// sourcePivot is s's pivot over the linked source reg, as its host has169// gathered it: nil while it's on its way, s then waiting for it.170func (w *Workbook) sourcePivot(s *Sheet, reg Region, p *Pivot) (*pivotCalc, error) {171 if w.sources == nil {172 return nil, errors.New("Nothing reads linked sources here")173 }174 q := SourceQuestion{Kind: AskPivot, Source: reg.Name, Pivot: *p}175 ans, ok := w.sources.Answer(q)176 switch {177 case !ok:178 k := q.Key()179 if w.src.pivots == nil {180 w.src.pivots = map[string][]*Sheet{}181 }182 w.src.pivots[k] = append(w.src.pivots[k], s)183 return nil, nil184 case ans.Pivot == nil:185 why := ans.Why186 if why == "" {187 why = reg.Name + " can't be read"188 }189 return nil, errors.New(why)190 }191 c := *ans.Pivot.calc // the host keeps its own for the next layout192 c.w, c.p = w, p193 return &c, nil194}