cat / 012
clone

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

main
012 / internal / sheet / insertbook.go
insertbook.go 5.9K
1package sheet23import (4 "errors"5 "fmt"6 "slices"7)89// Importing into a workbook, as Sheets' Import location: "Insert new10// sheet(s)" moves every sheet of an imported workbook into this one, and11// "Replace current sheet" puts the imported sheet in the place of one.12// The imported workbook is built on its own (fileio) and given up to13// these; either is one undo step.1415// ImportResult says what an import into a workbook did.16type ImportResult struct {17 Sheets []*Sheet // the sheets added, in tab order18 Renamed map[string]string // imported names changed to fit, old to new19 Names int // named ranges left out, their names taken20}2122// InsertBook moves the sheets of src, an imported workbook, into w at23// index at (clamped to the ends; the UI puts them after the sheet shown),24// with src's named ranges, as one undo step labelled label. A sheet whose name w already has gets a number ("Sales 2"), and25// src's formulas follow the new name; a named range whose name w already26// has is left out. src is used up.27func (w *Workbook) InsertBook(src *Workbook, at int, label string) (ImportResult, error) {28 var res ImportResult29 if src == w || len(src.sheets) == 0 {30 return res, errors.New("Nothing to import")31 }32 sheets := slices.Clone(src.sheets)33 for _, s := range sheets {34 if w.Lookup(s.name) == nil {35 continue36 }37 name := freeIn(s.name, w, src)38 if res.Renamed == nil {39 res.Renamed = map[string]string{}40 }41 res.Renamed[s.name] = name42 if err := src.RenameSheet(s, name); err != nil {43 return res, err44 }45 }46 names := src.Names()47 for _, s := range sheets {48 src.detach(s)49 s.wb = w50 }51 src.sheets = nil52 // Undo shows the sheet the new ones follow, where the import started.53 at = clampInt(at, 0, len(w.sheets))54 shown := sheets[0]55 if at > 0 {56 shown = w.sheets[at-1]57 }58 w.change(shown, label, Rect{}, func() {59 w.recordSheets()60 for i, s := range sheets {61 w.insert(s, at+i)62 }63 for _, n := range names {64 if _, taken := w.LookupName(n.Name); taken {65 res.Names++66 continue67 }68 w.putName(nameKey(n.Name), &n)69 }70 })71 res.Sheets = sheets72 return res, nil73}7475// ReplaceSheet puts s, the sheet of an imported workbook, in the place of76// dst, as one undo step labelled label: it takes dst's name and position,77// so formulas reading dst read it, and named ranges on dst move to it.78// dst's charts that fit s's data stay, drawing it, and the rest go (see79// keepCharts); it returns what became of each. dst's cells and the rest80// go; undo brings them back, charts too.81func (w *Workbook) ReplaceSheet(dst, s *Sheet, label string) ([]ChartFate, error) {82 switch {83 case !dst.live || dst.wb != w:84 return nil, errors.New("That sheet was deleted")85 case s.wb == w:86 return nil, errors.New("That sheet is already in the spreadsheet")87 }88 fates := s.keepCharts(dst)89 s.wb.detach(s)90 s.wb.sheets = slices.DeleteFunc(s.wb.sheets, func(t *Sheet) bool { return t == s })91 s.wb, s.tabHidden = w, dst.tabHidden92 w.change(dst, label, Rect{}, func() {93 w.recordSheets()94 i := w.Index(dst)95 w.remove(dst)96 s.name = dst.name97 w.insert(s, i)98 for _, n := range w.Names() {99 if n.Sheet == dst {100 n.Sheet = s101 w.putName(nameKey(n.Name), &n)102 }103 }104 })105 return fates, nil106}107108// ChartFate says what replacing a sheet did to one of its charts.109type ChartFate struct {110 Name string // the chart's title, or Chart 1, Chart 2 by position111 Was, Now Rect // the range it drew, and draws112 // Removed is set when the range no longer fits the new data: it113 // holds nothing now, or it was a whole table and the new sheet's114 // table there has other series.115 Removed bool116}117118// keepCharts gives s, replacing old, old's charts that fit its data. A119// chart that drew a whole block of old's data is re-pointed to the block120// s has at the same corner when it has as many series (columns, or rows121// for a chart by row), so a chart of last month's table draws this122// month's, however many rows it has; when s's block there has other123// series the chart goes. A chart of whole columns or rows, or of part of124// a block, keeps its range. A chart whose range holds nothing now goes125// too. ChartFate says which.126func (s *Sheet) keepCharts(old *Sheet) []ChartFate {127 fates := make([]ChartFate, 0, len(old.charts))128 for i, c := range old.charts {129 name := c.Title130 if name == "" {131 name = fmt.Sprintf("Chart %d", i+1)132 }133 was := c.Data134 now, fits := s.repoint(old, was, c.ByRow)135 _, filled := s.FilledBounds(now)136 f := ChartFate{Name: name, Was: was, Now: now, Removed: !fits || !filled}137 fates = append(fates, f)138 if !f.Removed {139 c.Data = now140 s.charts = append(s.charts, c)141 }142 }143 return fates144}145146// repoint is the range on s a chart drawing d on old draws: s's block of147// data at d's corner when d was old's whole block there and the two have148// as many series, else d. Whole columns or rows, and part of a block,149// read any data as is. It reports false when d was a whole block and s150// has none like it there.151func (s *Sheet) repoint(old *Sheet, d Rect, byRow bool) (Rect, bool) {152 if d.AllRows() || d.AllCols() || old.Region(d.From) != d {153 return d, true154 }155 now := s.Region(d.From)156 if now.From != d.From || !s.cells.filledAt(now.From) && now.From == now.To {157 return d, false158 }159 if byRow && now.To.Row-now.From.Row != d.To.Row-d.From.Row ||160 !byRow && now.To.Col-now.From.Col != d.To.Col-d.From.Col {161 return d, false162 }163 return now, true164}165166// freeIn returns base, or base with a number added ("Sales 2"), so that167// none of books has a sheet of that name, within the length limit.168func freeIn(base string, books ...*Workbook) string {169 trim := func(s string, n int) string {170 if r := []rune(s); len(r) > n {171 return string(r[:n])172 }173 return s174 }175 taken := func(name string) bool {176 return slices.ContainsFunc(books, func(w *Workbook) bool { return w.Lookup(name) != nil })177 }178 name := trim(base, maxSheetName)179 for n := 2; taken(name); n++ {180 suffix := fmt.Sprintf(" %d", n)181 name = trim(base, maxSheetName-len(suffix)) + suffix182 }183 return name184}