cat / 012
clone

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

Split the remaining complex engine functions into named steps

Dami committed 5869f19
MoveTo becomes a sheetMove with steps for the moved cells, the readers
it rewrites and applying them, in move.go with Move. Explaining an
error follows it with an errorSearch type, one method per kind of node.
Pushing an undo step drops what didn't change and joins width previews
in their own functions. parseTime reads the meridiem and the clock
parts separately, and sorting compares rows in compareRows. Behavior is
unchanged.
commit
5869f197c1119b93da28dfa5d59e0af8d51954d0
parent
77e797dc42ed3d0ffa7af10d691860dcf702843b
author
Dami <dami.etoile@gmail.com>
6 files changed, +361 -259
Minternal/sheet/date.go78+++++++------
Minternal/sheet/explain.go111++++++++++++------
Minternal/sheet/history.go71++++++++----
Ainternal/sheet/move.go176++++++++++++++++++++++++++++++
Minternal/sheet/ops.go142------------------------
Minternal/sheet/sort.go42++++---
M internal/sheet/date.go +43 -35
@@ -56,44 +56,14 @@ func parseDateTime(s string) (float64, Format, bool) {
5656 // parseTime recognizes H:MM, H:MM:SS(.fff) and H AM/PM forms. Hours of5757 // 24 or more make a duration.5858 func parseTime(s string) (float64, Format, bool) {59- up := strings.ToUpper(strings.TrimSpace(s))60- meridiem := ""61- for _, m := range []string{"AM", "PM"} {62- if strings.HasSuffix(up, m) {63- meridiem, up = m[:1], strings.TrimSpace(strings.TrimSuffix(up, m))64- break65- }66- }67- parts := strings.Split(up, ":")59+ clock, meridiem := cutMeridiem(strings.ToUpper(strings.TrimSpace(s)))60+ parts := strings.Split(clock, ":")6861 if len(parts) > 3 || (len(parts) == 1 && meridiem == "") {6962 return 0, Format{}, false7063 }71- var h, m int72- var sec float6473- for i, p := range parts {74- if p == "" || !isDigit(p[0]) {75- return 0, Format{}, false76- }77- if i == 2 {78- v, err := strconv.ParseFloat(p, 64)79- if err != nil || v >= 60 || strings.ContainsAny(p, "eE+-") {80- return 0, Format{}, false81- }82- sec = v83- continue84- }85- if len(p) > 2 && i > 0 || strings.Trim(p, "0123456789") != "" {86- return 0, Format{}, false87- }88- n, _ := strconv.Atoi(p)89- if i == 0 {90- h = n91- } else {92- if n >= 60 || len(p) != 2 {93- return 0, Format{}, false94- }95- m = n96- }64+ h, m, sec, ok := clockParts(parts)65+ if !ok {66+ return 0, Format{}, false9767 }9868 f := Format{Kind: FmtTime, Pattern: "h:mm:ss"}9969 switch {
@@ -112,6 +82,44 @@ func parseTime(s string) (float64, Format, bool) {
11282 return numfmt.TimeSerial(h, m, sec), f, true11383 }11484 85+// cutMeridiem cuts a trailing AM or PM off an upper-case time, returning86+// the rest and "A", "P" or "".87+func cutMeridiem(up string) (string, string) {88+ for _, m := range [...]string{"AM", "PM"} {89+ if rest, ok := strings.CutSuffix(up, m); ok {90+ return strings.TrimSpace(rest), m[:1]91+ }92+ }93+ return up, ""94+}95+96+// clockParts reads the parts of H:MM:SS: hours of any number of digits,97+// then minutes of two digits and seconds of two, which may have decimals.98+func clockParts(parts []string) (h, m int, sec float64, ok bool) {99+ for i, p := range parts {100+ if p == "" || !isDigit(p[0]) {101+ return 0, 0, 0, false102+ }103+ whole := strings.Trim(p, "0123456789") == ""104+ switch i {105+ case 0:106+ h, _ = strconv.Atoi(p)107+ ok = whole108+ case 1:109+ m, _ = strconv.Atoi(p)110+ ok = whole && len(p) == 2 && m < 60111+ default:112+ v, err := strconv.ParseFloat(p, 64)113+ sec = v114+ ok = err == nil && v < 60 && !strings.ContainsAny(p, "eE+-")115+ }116+ if !ok {117+ return 0, 0, 0, false118+ }119+ }120+ return h, m, sec, true121+}122+115123 // parseDate recognizes the date forms listed at parseDateTime.116124 func parseDate(s string) (float64, Format, bool) {117125 s = strings.TrimSpace(s)
M internal/sheet/explain.go +73 -38
@@ -72,60 +72,95 @@ func (s *Sheet) explain(a Addr, path []loc) string {
7272 // itself rather than passing it on. When the error comes from a7373 // referenced cell, it returns that cell.7474 func (s *Sheet) errorOrigin(n Node, want Value) (Node, *loc) {75- same := func(v Value) bool { return v.Kind == Error && v.Str == want.Str }76- get := s.wb.values(s)77- at := func(sheet string, a Addr) *loc {78- if t := s.wb.resolve(s, sheet); t != nil {79- return &loc{t, a}80- }81- return nil // an unresolved sheet name is the error itself75+ return errorSearch{s: s, want: want, get: s.wb.values(s)}.find(n)76+}77+78+// errorSearch follows an error through a formula on s to where it starts.79+type errorSearch struct {80+ s *Sheet81+ want Value82+ get lookup83+}84+85+func (e errorSearch) same(v Value) bool { return v.Kind == Error && v.Str == e.want.Str }86+87+// at is the cell a reference points at, or nil when its sheet name is88+// unresolved: then the reference is the error itself.89+func (e errorSearch) at(sheet string, a Addr) *loc {90+ if t := e.s.wb.resolve(e.s, sheet); t != nil {91+ return &loc{t, a}8292 }93+ return nil94+}95+96+func (e errorSearch) find(n Node) (Node, *loc) {8397 switch n := n.(type) {8498 case formula.Ref:85- if same(get.cell(n.Sheet, n.Addr)) {86- return n, at(n.Sheet, n.Addr)99+ if e.same(e.get.cell(n.Sheet, n.Addr)) {100+ return n, e.at(n.Sheet, n.Addr)87101 }88102 case formula.Range:89- if n.Rect.From == n.Rect.To && same(get.cell(n.Sheet, n.Rect.From)) {90- return n, at(n.Sheet, n.Rect.From)103+ if n.Rect.From == n.Rect.To && e.same(e.get.cell(n.Sheet, n.Rect.From)) {104+ return n, e.at(n.Sheet, n.Rect.From)91105 }92106 case formula.Unary:93- if same(eval(n.X, get)) {94- return s.errorOrigin(n.X, want)95- }107+ return e.operand(n, n.X)96108 case formula.Binary:97- for _, x := range []Node{n.L, n.R} {98- if same(eval(x, get)) {99- return s.errorOrigin(x, want)100- }101- }109+ return e.operand(n, n.L, n.R)102110 case formula.Call:103- if funcOf(n).remote != nil {104- return n, nil // JEV explains its own answers111+ return e.inCall(n)112+ }113+ return n, nil114+}115+116+// operand follows the first of n's operands that has the error, or stops117+// at n when none has.118+func (e errorSearch) operand(n Node, operands ...Node) (Node, *loc) {119+ for _, x := range operands {120+ if e.same(eval(x, e.get)) {121+ return e.find(x)105122 }106- for _, arg := range n.Args {107- if r, ok := arg.(formula.Range); ok && r.Rect.From != r.Rect.To {108- t := s.wb.resolve(s, r.Sheet)109- if t == nil {110- return arg, nil111- }112- cells := t.cellsIn(r.Rect)113- sortAddrs(cells)114- for _, a := range cells {115- if same(t.Value(a)) {116- return arg, &loc{t, a}117- }118- }119- continue120- }121- if same(eval(arg, get)) {122- return s.errorOrigin(arg, want)123+ }124+ return n, nil125+}126+127+// inCall follows the first argument of a call that has the error: for a128+// range, its first cell with it.129+func (e errorSearch) inCall(n formula.Call) (Node, *loc) {130+ if funcOf(n).remote != nil {131+ return n, nil // JEV explains its own answers132+ }133+ for _, arg := range n.Args {134+ if r, ok := arg.(formula.Range); ok && r.Rect.From != r.Rect.To {135+ if l, found := e.inRange(r); found {136+ return arg, l123137 }138+ continue139+ }140+ if e.same(eval(arg, e.get)) {141+ return e.find(arg)124142 }125143 }126144 return n, nil127145 }128146 147+// inRange finds the first cell of r, row by row, with the error. An148+// unresolved sheet name is found, as the error itself, with no cell.149+func (e errorSearch) inRange(r formula.Range) (*loc, bool) {150+ t := e.s.wb.resolve(e.s, r.Sheet)151+ if t == nil {152+ return nil, true153+ }154+ cells := t.cellsIn(r.Rect)155+ sortAddrs(cells)156+ for _, a := range cells {157+ if e.same(t.Value(a)) {158+ return &loc{t, a}, true159+ }160+ }161+ return nil, false162+}163+129164 // lookupFuncs report #N/A when they find no match.130165 var lookupFuncs = []string{"VLOOKUP", "HLOOKUP", "XLOOKUP", "LOOKUP", "MATCH", "XMATCH"}131166
M internal/sheet/history.go +46 -25
@@ -239,7 +239,28 @@ func (l *sheetList) equal(m *sheetList) bool {
239239 // push adds a finished step to the undo stack and clears redo, dropping240240 // no-op changes.241241 func (w *Workbook) push(st *step) {242+ w.dropUnchanged(st)243+ if st.empty() {244+ return245+ }242246 h := &w.hist247+ h.redo = nil248+ h.lastID++249+ widthOnly := st.widthOnly()250+ if top := h.top(); widthOnly && h.mergeWidths && top != nil && top.widthOnly() {251+ h.joinWidths(top, st)252+ return253+ }254+ st.id = h.lastID255+ h.undo = append(h.undo, st)256+ if len(h.undo) > MaxUndo {257+ h.undo = h.undo[len(h.undo)-MaxUndo:]258+ }259+ h.mergeWidths = widthOnly260+}261+262+// dropUnchanged removes from st what ended the step as it began.263+func (w *Workbook) dropUnchanged(st *step) {243264 for k, width := range st.widths {244265 if k.s.widths[k.col] == width {245266 delete(st.widths, k)
@@ -251,7 +272,7 @@ func (w *Workbook) push(st *step) {
251272 }252273 }253274 for k, n := range st.names {254- if cur := w.namePtr(k); n == nil && cur == nil || n != nil && cur != nil && *n == *cur {275+ if sameName(n, w.namePtr(k)) {255276 delete(st.names, k)256277 }257278 }
@@ -271,35 +292,35 @@ func (w *Workbook) push(st *step) {
271292 if st.decimal != nil && *st.decimal == w.decimal {272293 st.decimal = nil273294 }274- if st.empty() {275- return295+}296+297+// sameName reports whether two named ranges (nil for undefined) are the298+// same.299+func sameName(a, b *Name) bool {300+ if a == nil || b == nil {301+ return a == b276302 }277- h.redo = nil278- h.lastID++279- widthOnly := st.widthOnly()280- if top := h.top(); widthOnly && h.mergeWidths && top != nil && top.widthOnly() {281- for k, width := range st.widths {282- if _, ok := top.widths[k]; !ok {283- top.widths[k] = width284- }285- }286- for k, width := range top.widths {287- if k.s.widths[k.col] == width {288- delete(top.widths, k)289- }303+ return *a == *b304+}305+306+// joinWidths folds the width-only step st into the width-only step on307+// top, so a live preview of a column width and its final value (or its308+// cancellation) are one step.309+func (h *history) joinWidths(top, st *step) {310+ for k, width := range st.widths {311+ if _, ok := top.widths[k]; !ok {312+ top.widths[k] = width290313 }291- top.id, top.focus = h.lastID, union(top.focus, st.focus)292- if top.empty() {293- h.undo = h.undo[:len(h.undo)-1]314+ }315+ for k, width := range top.widths {316+ if k.s.widths[k.col] == width {317+ delete(top.widths, k)294318 }295- return296319 }297- st.id = h.lastID298- h.undo = append(h.undo, st)299- if len(h.undo) > MaxUndo {300- h.undo = h.undo[len(h.undo)-MaxUndo:]320+ top.id, top.focus = h.lastID, union(top.focus, st.focus)321+ if top.empty() {322+ h.undo = h.undo[:len(h.undo)-1]301323 }302- h.mergeWidths = widthOnly303324 }304325 305326 func (h *history) top() *step {
A internal/sheet/move.go +176 -0
@@ -0,0 +1,176 @@
1+package sheet2+3+import "github.com/FelineStateMachine/012/internal/formula"4+5+// Moving cells, as cut and paste does: formulas that read the moved cells6+// follow them.7+8+// Move moves the cells in src so its top-left corner lands on to, as9+// cut and paste does in Sheets. Formulas anywhere that referred to the10+// moved cells follow them; references to cells the move overwrote become11+// #REF!. It returns the destination range.12+func (s *Sheet) Move(src Rect, to Addr) (Rect, error) {13+ dc, dr := to.Col-src.From.Col, to.Row-src.From.Row14+ dst := Rect{From: to, To: Addr{Col: src.To.Col + dc, Row: src.To.Row + dr}}15+ if !dst.To.Valid() {16+ return Rect{}, ErrPasteEdge17+ }18+ shift := func(a Addr) Addr { return Addr{Col: a.Col + dc, Row: a.Row + dr} }19+ cell := func(a Addr) (Addr, bool) {20+ switch {21+ case src.Contains(a):22+ return shift(a), true23+ case dst.Contains(a):24+ return a, false25+ }26+ return a, true27+ }28+ rng := func(r Rect) (Rect, bool) {29+ if src.Contains(r.From) && src.Contains(r.To) {30+ return Rect{From: shift(r.From), To: shift(r.To)}, true31+ }32+ return r, true33+ }34+ label := "move " + src.String() + " to " + dst.String()35+ s.change(label, dst, func() {36+ s.remap(label, dst, cell, rng)37+ s.remapNames(rng) // a name for exactly the moved cells follows them38+ })39+ return dst, nil40+}41+42+// MoveTo moves the cells in src to sheet dst, src's top-left corner43+// landing on to, as cutting on one sheet and pasting on another does in44+// Sheets. Formulas anywhere that read the moved cells follow them to dst,45+// naming its sheet where they need to; the moved formulas keep reading46+// what they read, naming this sheet for cells that stayed behind.47+// References to cells the move overwrote become #REF!.48+func (s *Sheet) MoveTo(dst *Sheet, src Rect, to Addr) (Rect, error) {49+ if dst == s {50+ return s.Move(src, to)51+ }52+ dc, dr := to.Col-src.From.Col, to.Row-src.From.Row53+ d := Rect{From: to, To: Addr{Col: src.To.Col + dc, Row: src.To.Row + dr}}54+ if !d.To.Valid() {55+ return Rect{}, ErrPasteEdge56+ }57+ mv := sheetMove{from: s, to: dst, src: src, d: d}58+ moved, readers := mv.movedCells(), mv.readers()59+ label := "move " + src.String() + " to " + formula.QuoteSheet(dst.name) + "!" + d.String()60+ dst.change(label, d, func() { mv.apply(moved, readers) })61+ return d, nil62+}63+64+// sheetMove is the cells in src on one sheet moving to d on another.65+type sheetMove struct {66+ from, to *Sheet67+ src, d Rect68+}69+70+func (mv sheetMove) shift(a Addr) Addr {71+ return Addr{Col: a.Col + mv.d.From.Col - mv.src.From.Col, Row: a.Row + mv.d.From.Row - mv.src.From.Row}72+}73+74+// movedCells are the cells that move, by where they land, with their75+// references rewritten.76+func (mv sheetMove) movedCells() map[Addr]*Cell {77+ moved := map[Addr]*Cell{}78+ for _, a := range mv.from.cellsIn(mv.src) {79+ moved[mv.shift(a)] = mv.from.cells.get(a).rewritten(mv.rewrite(mv.from, mv.to))80+ }81+ return moved82+}83+84+// readers are the formulas that stay where they are, rewritten, among85+// those whose references the move changes: on either sheet, or on another86+// sheet naming one of them.87+func (mv sheetMove) readers() map[loc]*Cell {88+ out := map[loc]*Cell{}89+ add := func(l loc) {90+ if mv.src.Contains(l.a) && l.s == mv.from || mv.d.Contains(l.a) && l.s == mv.to {91+ return // moved or overwritten92+ }93+ c := l.s.cells.get(l.a)94+ if nc := c.rewritten(mv.rewrite(l.s, l.s)); nc != c {95+ out[l] = nc96+ }97+ }98+ for _, t := range []*Sheet{mv.from, mv.to} {99+ for a := range t.cells.all() {100+ add(loc{t, a})101+ }102+ }103+ for _, l := range mv.from.wb.crossList() {104+ if l.s != mv.from && l.s != mv.to {105+ add(l)106+ }107+ }108+ return out109+}110+111+// apply clears the source and destination, places the moved cells and112+// the rewritten readers, and moves the named ranges wholly inside the113+// source along with it.114+func (mv sheetMove) apply(moved map[Addr]*Cell, readers map[loc]*Cell) {115+ for _, a := range mv.from.cellsIn(mv.src) {116+ mv.from.place(a, nil)117+ }118+ for _, a := range mv.to.cellsIn(mv.d) {119+ mv.to.place(a, nil)120+ }121+ for a, c := range moved {122+ mv.to.place(a, c)123+ }124+ for l, c := range readers {125+ l.s.place(l.a, c)126+ }127+ w := mv.from.wb128+ for k, n := range w.names {129+ if n.Sheet == mv.from && !n.Lost && mv.src.Contains(n.Range.From) && mv.src.Contains(n.Range.To) {130+ n.Sheet, n.Range = mv.to, Rect{From: mv.shift(n.Range.From), To: mv.shift(n.Range.To)}131+ w.putName(k, &n)132+ }133+ }134+}135+136+// rewrite maps the references of a formula that was on orig and is on137+// home after the move. A reference names its sheet unless it points at138+// home.139+func (mv sheetMove) rewrite(orig, home *Sheet) formula.Rewriter {140+ w := mv.from.wb141+ // written is how a reference names now, having named was as written.142+ written := func(as string, was, now *Sheet) string {143+ switch {144+ case now == home:145+ return ""146+ case now == was && as != "":147+ return as148+ }149+ return now.name150+ }151+ return formula.Rewriter{152+ Ref: func(n formula.Ref) Node {153+ t := w.resolve(orig, n.Sheet)154+ switch {155+ case t == nil:156+ return n157+ case t == mv.from && mv.src.Contains(n.Addr):158+ return formula.Ref{Addr: mv.shift(n.Addr), Abs: n.Abs, Sheet: written(n.Sheet, t, mv.to)}159+ case t == mv.to && mv.d.Contains(n.Addr):160+ return formula.RefErr{}161+ }162+ return formula.Ref{Addr: n.Addr, Abs: n.Abs, Sheet: written(n.Sheet, t, t)}163+ },164+ Range: func(n formula.Range) Node {165+ t := w.resolve(orig, n.Sheet)166+ switch {167+ case t == nil:168+ return n169+ case t == mv.from && mv.src.Contains(n.Rect.From) && mv.src.Contains(n.Rect.To):170+ r := Rect{From: mv.shift(n.Rect.From), To: mv.shift(n.Rect.To)}171+ return formula.Range{Rect: r, Abs: n.Abs, Sheet: written(n.Sheet, t, mv.to)}172+ }173+ return formula.Range{Rect: n.Rect, Abs: n.Abs, Sheet: written(n.Sheet, t, t)}174+ },175+ }176+}
M internal/sheet/ops.go +0 -142
@@ -147,148 +147,6 @@ func (s *Sheet) shiftWidths(sp formula.Span) {
147147 }148148 }149149 150-// Move moves the cells in src so its top-left corner lands on to, as151-// cut and paste does in Sheets. Formulas anywhere that referred to the152-// moved cells follow them; references to cells the move overwrote become153-// #REF!. It returns the destination range.154-func (s *Sheet) Move(src Rect, to Addr) (Rect, error) {155- dc, dr := to.Col-src.From.Col, to.Row-src.From.Row156- dst := Rect{From: to, To: Addr{Col: src.To.Col + dc, Row: src.To.Row + dr}}157- if !dst.To.Valid() {158- return Rect{}, ErrPasteEdge159- }160- shift := func(a Addr) Addr { return Addr{Col: a.Col + dc, Row: a.Row + dr} }161- cell := func(a Addr) (Addr, bool) {162- switch {163- case src.Contains(a):164- return shift(a), true165- case dst.Contains(a):166- return a, false167- }168- return a, true169- }170- rng := func(r Rect) (Rect, bool) {171- if src.Contains(r.From) && src.Contains(r.To) {172- return Rect{From: shift(r.From), To: shift(r.To)}, true173- }174- return r, true175- }176- label := "move " + src.String() + " to " + dst.String()177- s.change(label, dst, func() {178- s.remap(label, dst, cell, rng)179- s.remapNames(rng) // a name for exactly the moved cells follows them180- })181- return dst, nil182-}183-184-// MoveTo moves the cells in src to sheet dst, src's top-left corner185-// landing on to, as cutting on one sheet and pasting on another does in186-// Sheets. Formulas anywhere that read the moved cells follow them to dst,187-// naming its sheet where they need to; the moved formulas keep reading188-// what they read, naming this sheet for cells that stayed behind.189-// References to cells the move overwrote become #REF!.190-func (s *Sheet) MoveTo(dst *Sheet, src Rect, to Addr) (Rect, error) {191- if dst == s {192- return s.Move(src, to)193- }194- dc, dr := to.Col-src.From.Col, to.Row-src.From.Row195- d := Rect{From: to, To: Addr{Col: src.To.Col + dc, Row: src.To.Row + dr}}196- if !d.To.Valid() {197- return Rect{}, ErrPasteEdge198- }199- shift := func(a Addr) Addr { return Addr{Col: a.Col + dc, Row: a.Row + dr} }200- rw := func(orig, home *Sheet) formula.Rewriter { return s.moveRewrite(dst, src, d, orig, home) }201-202- moved := map[Addr]*Cell{}203- for _, a := range s.cellsIn(src) {204- moved[shift(a)] = s.cells.get(a).rewritten(rw(s, dst))205- }206- rest := map[loc]*Cell{}207- keep := func(l loc) {208- if src.Contains(l.a) && l.s == s || d.Contains(l.a) && l.s == dst {209- return210- }211- c := l.s.cells.get(l.a)212- if nc := c.rewritten(rw(l.s, l.s)); nc != c {213- rest[l] = nc214- }215- }216- for _, t := range []*Sheet{s, dst} {217- for a := range t.cells.all() {218- keep(loc{t, a})219- }220- }221- for _, l := range s.wb.crossList() {222- if l.s != s && l.s != dst {223- keep(l)224- }225- }226- label := "move " + src.String() + " to " + formula.QuoteSheet(dst.name) + "!" + d.String()227- dst.change(label, d, func() {228- for _, a := range s.cellsIn(src) {229- s.place(a, nil)230- }231- for _, a := range dst.cellsIn(d) {232- dst.place(a, nil)233- }234- for a, c := range moved {235- dst.place(a, c)236- }237- for l, c := range rest {238- l.s.place(l.a, c)239- }240- for k, n := range s.wb.names {241- if n.Sheet == s && !n.Lost && src.Contains(n.Range.From) && src.Contains(n.Range.To) {242- n.Sheet, n.Range = dst, Rect{From: shift(n.Range.From), To: shift(n.Range.To)}243- s.wb.putName(k, &n)244- }245- }246- })247- return d, nil248-}249-250-// moveRewrite maps the references of a formula that was on orig and is251-// on home after cells in src on s move to d on dst. A reference names its252-// sheet unless it points at home.253-func (s *Sheet) moveRewrite(dst *Sheet, src, d Rect, orig, home *Sheet) formula.Rewriter {254- dc, dr := d.From.Col-src.From.Col, d.From.Row-src.From.Row255- shift := func(a Addr) Addr { return Addr{Col: a.Col + dc, Row: a.Row + dr} }256- // written is how a reference names now, having named was as written.257- written := func(as string, was, now *Sheet) string {258- switch {259- case now == home:260- return ""261- case now == was && as != "":262- return as263- }264- return now.name265- }266- return formula.Rewriter{267- Ref: func(n formula.Ref) Node {268- t := s.wb.resolve(orig, n.Sheet)269- switch {270- case t == nil:271- return n272- case t == s && src.Contains(n.Addr):273- return formula.Ref{Addr: shift(n.Addr), Abs: n.Abs, Sheet: written(n.Sheet, t, dst)}274- case t == dst && d.Contains(n.Addr):275- return formula.RefErr{}276- }277- return formula.Ref{Addr: n.Addr, Abs: n.Abs, Sheet: written(n.Sheet, t, t)}278- },279- Range: func(n formula.Range) Node {280- t := s.wb.resolve(orig, n.Sheet)281- switch {282- case t == nil:283- return n284- case t == s && src.Contains(n.Rect.From) && src.Contains(n.Rect.To):285- return formula.Range{Rect: Rect{From: shift(n.Rect.From), To: shift(n.Rect.To)}, Abs: n.Abs, Sheet: written(n.Sheet, t, dst)}286- }287- return formula.Range{Rect: n.Rect, Abs: n.Abs, Sheet: written(n.Sheet, t, t)}288- },289- }290-}291-292150 // Clip is a copied range: snapshots of its cells taken at copy time.293151 type Clip struct {294152 Src Rect
M internal/sheet/sort.go +23 -19
@@ -50,6 +50,28 @@ func sortCompare(a, b Value) int {
5050 return 05151 }5252 53+// compareRows orders rows i and j by keys, first key first. Blanks go54+// last in either direction.55+func (s *Sheet) compareRows(keys []SortKey, i, j int) int {56+ for _, k := range keys {57+ a, b := s.Value(Addr{Col: k.Col, Row: i}), s.Value(Addr{Col: k.Col, Row: j})58+ if (a.Kind == Empty) != (b.Kind == Empty) {59+ if a.Kind == Empty {60+ return 161+ }62+ return -163+ }64+ d := sortCompare(a, b)65+ if k.Desc {66+ d = -d67+ }68+ if d != 0 {69+ return d70+ }71+ }72+ return 073+}74+5375 // SortRange sorts the rows of r by keys, first key first, as Sheets' Data5476 // > Sort range does (r excludes any header row). The sort is stable, so5577 // rows that tie keep their order. Only the cells inside r move. Formulas
@@ -67,25 +89,7 @@ func (s *Sheet) SortRange(r Rect, keys []SortKey) {
6789 for i := range order {6890 order[i] = r.From.Row + i6991 }70- slices.SortStableFunc(order, func(i, j int) int {71- for _, k := range keys {72- a, b := s.Value(Addr{Col: k.Col, Row: i}), s.Value(Addr{Col: k.Col, Row: j})73- if (a.Kind == Empty) != (b.Kind == Empty) { // blanks last either way74- if a.Kind == Empty {75- return 176- }77- return -178- }79- d := sortCompare(a, b)80- if k.Desc {81- d = -d82- }83- if d != 0 {84- return d85- }86- }87- return 088- })92+ slices.SortStableFunc(order, func(i, j int) int { return s.compareRows(keys, i, j) })8993 old := map[Addr]*Cell{}9094 for _, a := range s.cellsIn(r) {9195 old[a] = s.cells.get(a)