1package diff23import (4 "encoding/json"5 "strings"67 "src.rest/cat/012/internal/sheet"8)910// sheets merges the workbooks' sheets. A sheet is matched across the11// three by name or, renamed, by its cells; one removed on one side goes12// when the other left it as it was, and stays (a conflict) when ours13// changed it, or goes (a conflict) when only theirs did. Sheets added14// on both sides under one name merge cell by cell. The order is ours',15// with theirs' additions after the sheet they follow in theirs.16func (m *merger) sheets(b, o, t *rawBook) []*rawSheet {17 oOf, oBase := matchIndex(matchSheets(b, o))18 tOf, tBase := matchIndex(matchSheets(b, t))19 tDone := make([]bool, len(t.sheets))20 var out []*rawSheet21 for j, so := range o.sheets {22 i, inBase := oBase[j]23 switch {24 case !inBase:25 out = append(out, m.added(so, t, tBase, tDone))26 case hasKey(tOf, i):27 tDone[tOf[i]] = true28 out = append(out, m.sheet(b.sheets[i], so, t.sheets[tOf[i]]))29 case sameSheet(b.sheets[i], so):30 // removed in theirs, unchanged in ours: removed31 default:32 m.said("sheet "+so.name, "", "changed in ours, removed in theirs")33 out = append(out, so)34 }35 }36 for k, st := range t.sheets {37 if tDone[k] {38 continue39 }40 i, inBase := tBase[k]41 switch {42 case !inBase:43 out = insertAfter(out, st, t, k)44 case !hasKey(oOf, i) && !sameSheet(b.sheets[i], st):45 m.said("sheet "+st.name, "", "removed in ours, changed in theirs")46 }47 }48 return m.unique(out)49}5051// matchIndex is a pairing as maps: a's index to b's, and back.52func matchIndex(p pairing) (toB, toA map[int]int) {53 toB, toA = map[int]int{}, map[int]int{}54 for _, pr := range p.pairs {55 toB[pr[0]], toA[pr[1]] = pr[1], pr[0]56 }57 return toB, toA58}5960func hasKey(m map[int]int, k int) bool { _, ok := m[k]; return ok }6162// added is a sheet ours added, merged with one theirs added under the63// same name, if any, as if both started empty.64func (m *merger) added(so *rawSheet, t *rawBook, tBase map[int]int, tDone []bool) *rawSheet {65 k := t.sheetIndex(so.name)66 if _, inBase := tBase[k]; k < 0 || inBase || tDone[k] {67 return so68 }69 tDone[k] = true70 return m.sheet(&rawSheet{name: so.name, fields: map[string]json.RawMessage{}, cells: map[string]json.RawMessage{}}, so, t.sheets[k])71}7273// insertAfter puts s, theirs' k-th sheet, after the sheet before it in74// theirs, or first.75func insertAfter(out []*rawSheet, s *rawSheet, t *rawBook, k int) []*rawSheet {76 at := 077 for p := k - 1; p >= 0; p-- {78 if i := indexNamed(out, t.sheets[p].name); i >= 0 {79 at = i + 180 break81 }82 }83 out = append(out, nil)84 copy(out[at+1:], out[at:])85 out[at] = s86 return out87}8889func indexNamed(list []*rawSheet, name string) int {90 for i, s := range list {91 if strings.EqualFold(s.name, name) {92 return i93 }94 }95 return -196}9798// unique renames sheets that came out with one name, ignoring case, as99// when ours renamed a sheet to the name of one theirs added.100func (m *merger) unique(list []*rawSheet) []*rawSheet {101 for i, s := range list {102 if indexNamed(list[:i], s.name) < 0 {103 continue104 }105 name := s.name + " (theirs)"106 for n := 2; indexNamed(list, name) >= 0; n++ {107 name = s.name + " (theirs " + itoa(n) + ")"108 }109 m.said("sheet "+s.name, "name", "a second sheet of this name, from theirs, is named "+name)110 s.name = name111 }112 return list113}114115func itoa(n int) string {116 raw, _ := json.Marshal(n)117 return string(raw)118}119120func str(s string) json.RawMessage {121 raw, _ := json.Marshal(s)122 return raw123}124125// notebook merges a notebook tab's cells as one part: their sources,126// with the outputs of the side taken.127func (m *merger) notebook(sheetName string, b, o, t json.RawMessage) json.RawMessage {128 sb, so, st := sources(b), sources(o), sources(t)129 switch {130 case equal(so, st), equal(sb, st):131 return o132 case equal(sb, so):133 return t134 }135 m.said(sheetName+" notebook", "", "cells changed on both sides")136 return o137}138139// sheet merges one sheet: its name, each cell and each field.140func (m *merger) sheet(b, o, t *rawSheet) *rawSheet {141 out := &rawSheet{fields: map[string]json.RawMessage{}, cells: map[string]json.RawMessage{}}142 json.Unmarshal(m.value("sheet "+o.name, "name", str(b.name), str(o.name), str(t.name)), &out.name)143 for _, k := range unionKeys3(b.cells, o.cells, t.cells) {144 if v := m.cell(sheet.QuoteSheet(out.name)+"!"+k, b.cells[k], o.cells[k], t.cells[k]); v != nil {145 out.cells[k] = v146 }147 }148 for _, k := range unionKeys3(b.fields, o.fields, t.fields) {149 var v json.RawMessage150 switch k {151 case "regions":152 v = m.list(b.fields[k], o.fields[k], t.fields[k], func(key string) string { return out.name + " region " + key })153 case "tables":154 v = m.list(b.fields[k], o.fields[k], t.fields[k], func(key string) string { return out.name + " table " + key })155 case "notebookCells":156 v = m.notebook(out.name, b.fields[k], o.fields[k], t.fields[k])157 case "widths", "heights", "lines":158 v = m.object(b.fields[k], o.fields[k], t.fields[k], func(key string) (string, string) { return out.name + " " + k, key })159 default:160 v = m.value(out.name+" "+k, "", b.fields[k], o.fields[k], t.fields[k])161 }162 if v != nil {163 out.fields[k] = v164 }165 }166 return out167}