From 556c59a68cbc169936194d1aeccafdd03181252f Mon Sep 17 00:00:00 2001 From: Sergey Date: Thu, 22 Dec 2022 00:33:56 +0500 Subject: [PATCH] Generic slice diff --- core/block/editor/state/event.go | 6 +- core/block/simple/dataview/dataview.go | 4 +- util/pbtypes/copy.go | 10 +- util/slice/diff.go | 126 ++++++++++++++++--------- util/slice/diff_test.go | 36 +++---- util/slice/slice.go | 8 +- 6 files changed, 114 insertions(+), 76 deletions(-) diff --git a/core/block/editor/state/event.go b/core/block/editor/state/event.go index 79e320d84..4c5776572 100644 --- a/core/block/editor/state/event.go +++ b/core/block/editor/state/event.go @@ -255,10 +255,10 @@ func (s *State) applyEvent(ev *pb.EventMessage) (err error) { event := o.BlockDataViewObjectOrderUpdate if err = apply(event.Id, func(b simple.Block) error { if dvBlock, ok := b.(dataview.Block); ok { - var existOrder []string + var existOrder []slice.ID for _, order := range dvBlock.Model().GetDataview().ObjectOrders { if order.ViewId == event.ViewId && order.GroupId == event.GroupId { - existOrder = order.ObjectIds + existOrder = slice.StringsToIDs(order.ObjectIds) } } @@ -266,7 +266,7 @@ func (s *State) applyEvent(ev *pb.EventMessage) (err error) { changedIds := slice.ApplyChanges(existOrder, pbtypes.EventsToSliceChange(changes)) dvBlock.SetViewObjectOrder([]*model.BlockContentDataviewObjectOrder{ - {ViewId: event.ViewId, GroupId: event.GroupId, ObjectIds: changedIds}, + {ViewId: event.ViewId, GroupId: event.GroupId, ObjectIds: slice.IDsToStrings(changedIds)}, }) return nil diff --git a/core/block/simple/dataview/dataview.go b/core/block/simple/dataview/dataview.go index fa8c89e48..4cefaed34 100644 --- a/core/block/simple/dataview/dataview.go +++ b/core/block/simple/dataview/dataview.go @@ -119,11 +119,11 @@ func (d *Dataview) Diff(b simple.Block) (msgs []simple.EventMessage, err error) for _, order2 := range dv.content.ObjectOrders { var found bool - var changes []slice.Change + var changes []slice.Change[slice.ID] for _, order1 := range d.content.ObjectOrders { if order1.ViewId == order2.ViewId && order1.GroupId == order2.GroupId { found = true - changes = slice.Diff(order1.ObjectIds, order2.ObjectIds) + changes = slice.Diff(slice.StringsToIDs(order1.ObjectIds), slice.StringsToIDs(order2.ObjectIds)) break } } diff --git a/util/pbtypes/copy.go b/util/pbtypes/copy.go index 9a80621dd..5ff249810 100644 --- a/util/pbtypes/copy.go +++ b/util/pbtypes/copy.go @@ -211,7 +211,7 @@ func StructNotNilKeys(st *types.Struct) (keys []string) { return } -func EventsToSliceChange(changes []*pb.EventBlockDataviewSliceChange) []slice.Change { +func EventsToSliceChange(changes []*pb.EventBlockDataviewSliceChange) []slice.Change[slice.ID] { sliceOpMap := map[pb.EventBlockDataviewSliceOperation]slice.DiffOperation{ pb.EventBlockDataview_SliceOperationNone: slice.OperationNone, pb.EventBlockDataview_SliceOperationAdd: slice.OperationAdd, @@ -220,15 +220,15 @@ func EventsToSliceChange(changes []*pb.EventBlockDataviewSliceChange) []slice.Ch pb.EventBlockDataview_SliceOperationReplace: slice.OperationReplace, } - var res []slice.Change + var res []slice.Change[slice.ID] for _, eventCh := range changes { - res = append(res, slice.Change{Op: sliceOpMap[eventCh.Op], Ids: eventCh.Ids, AfterId: eventCh.AfterId}) + res = append(res, slice.Change[slice.ID]{Op: sliceOpMap[eventCh.Op], Items: slice.StringsToIDs(eventCh.Ids), AfterId: eventCh.AfterId}) } return res } -func SliceChangeToEvents(changes []slice.Change) []*pb.EventBlockDataviewSliceChange { +func SliceChangeToEvents(changes []slice.Change[slice.ID]) []*pb.EventBlockDataviewSliceChange { eventsOpMap := map[slice.DiffOperation]pb.EventBlockDataviewSliceOperation{ slice.OperationNone: pb.EventBlockDataview_SliceOperationNone, slice.OperationAdd: pb.EventBlockDataview_SliceOperationAdd, @@ -239,7 +239,7 @@ func SliceChangeToEvents(changes []slice.Change) []*pb.EventBlockDataviewSliceCh var res []*pb.EventBlockDataviewSliceChange for _, sliceCh := range changes { - res = append(res, &pb.EventBlockDataviewSliceChange{Op: eventsOpMap[sliceCh.Op], Ids: sliceCh.Ids, AfterId: sliceCh.AfterId}) + res = append(res, &pb.EventBlockDataviewSliceChange{Op: eventsOpMap[sliceCh.Op], Ids: slice.IDsToStrings(sliceCh.Items), AfterId: sliceCh.AfterId}) } return res diff --git a/util/slice/diff.go b/util/slice/diff.go index 110c0c7e4..5d4851101 100644 --- a/util/slice/diff.go +++ b/util/slice/diff.go @@ -7,128 +7,164 @@ import ( type DiffOperation int const ( - OperationNone DiffOperation = iota + OperationNone DiffOperation = iota OperationAdd OperationMove OperationRemove OperationReplace ) -type Change struct { - Op DiffOperation - Ids []string +type Change[T IDGetter] struct { + Op DiffOperation + // TODO rename + Items []T AfterId string } -type MixedInput struct { - A []string - B []string +type IDGetter interface { + GetId() string } -func (m *MixedInput) Equal(a, b int) bool { - return m.A[a] == m.B[b] +type MixedInput[T IDGetter] struct { + A []T + B []T } -func Diff(origin, changed []string) []Change { - m := &MixedInput{ +func (m *MixedInput[T]) Equal(a, b int) bool { + return m.A[a].GetId() == m.B[b].GetId() +} + +type ID string + +func (id ID) GetId() string { return string(id) } + +func StringsToIDs(ss []string) []ID { + ids := make([]ID, 0, len(ss)) + for _, s := range ss { + ids = append(ids, ID(s)) + } + return ids +} + +func IDsToStrings(ids []ID) []string { + ss := make([]string, 0, len(ids)) + for _, id := range ids { + ss = append(ss, string(id)) + } + return ss +} + +func Diff[T IDGetter](origin, changed []T) []Change[T] { + m := &MixedInput[T]{ origin, changed, } - var result []Change + var result []Change[T] changes := diff.Diff(len(m.A), len(m.B), m) - delMap := make(map[string]bool) + delMap := make(map[string]T) for _, c := range changes { if c.Del > 0 { - for _, id := range m.A[c.A:c.A+c.Del] { - delMap[id] = true + for _, id := range m.A[c.A : c.A+c.Del] { + delMap[id.GetId()] = id } } } for _, c := range changes { if c.Ins > 0 { - inserts := m.B[c.B:c.B+c.Ins] + inserts := m.B[c.B : c.B+c.Ins] afterId := "" - if c.A > 0 { - afterId = m.A[c.A-1] + if c.A > 0 { + afterId = m.A[c.A-1].GetId() } - var oneCh Change - for _, id := range inserts { - if delMap[id] { // move + var oneCh Change[T] + for _, it := range inserts { + id := it.GetId() + if _, ok := delMap[id]; ok { // move if oneCh.Op != OperationMove { - if len(oneCh.Ids) > 0 { + if len(oneCh.Items) > 0 { result = append(result, oneCh) } - oneCh = Change{Op: OperationMove, AfterId: afterId} + oneCh = Change[T]{Op: OperationMove, AfterId: afterId} } - oneCh.Ids = append(oneCh.Ids, id) + oneCh.Items = append(oneCh.Items, it) delete(delMap, id) } else { // insert new if oneCh.Op != OperationAdd { - if len(oneCh.Ids) > 0 { + if len(oneCh.Items) > 0 { result = append(result, oneCh) } - oneCh = Change{Op: OperationAdd, AfterId: afterId} + oneCh = Change[T]{Op: OperationAdd, AfterId: afterId} } - oneCh.Ids = append(oneCh.Ids, id) + oneCh.Items = append(oneCh.Items, it) } afterId = id } - if len(oneCh.Ids) > 0 { + if len(oneCh.Items) > 0 { result = append(result, oneCh) } } } if len(delMap) > 0 { // remove - delIds := make([]string, 0, len(delMap)) - for id := range delMap { - delIds = append(delIds, id) + delIds := make([]T, 0, len(delMap)) + for _, it := range delMap { + delIds = append(delIds, it) } - result = append(result, Change{Op: OperationRemove, Ids: delIds}) + // TODO maybe just use ID wrapper, don't store WHOLE items + result = append(result, Change[T]{Op: OperationRemove, Items: delIds}) } return result } -func ApplyChanges(origin []string, changes []Change) []string { - result := make([]string, len(origin)) - copy(result, origin) +func findPos[T IDGetter](s []T, id string) int { + for i, sv := range s { + if sv.GetId() == id { + return i + } + } + return -1 +} + +func ApplyChanges[T IDGetter](origin []T, changes []Change[T]) []T { + res := make([]T, len(origin)) + copy(res, origin) for _, ch := range changes { switch ch.Op { case OperationAdd: pos := -1 if ch.AfterId != "" { - pos = FindPos(result, ch.AfterId) + pos = findPos(res, ch.AfterId) if pos < 0 { continue } } - result = Insert(result, pos+1, ch.Ids...) + res = Insert(res, pos+1, ch.Items...) case OperationMove: - withoutMoved := Filter(result, func(id string) bool { - return FindPos(ch.Ids, id) < 0 + withoutMoved := Filter(res, func(id T) bool { + return findPos(ch.Items, id.GetId()) < 0 }) pos := -1 if ch.AfterId != "" { - pos = FindPos(withoutMoved, ch.AfterId) + pos = findPos(withoutMoved, ch.AfterId) if pos < 0 { continue } } - result = Insert(withoutMoved, pos+1, ch.Ids...) + res = Insert(withoutMoved, pos+1, ch.Items...) case OperationRemove: - result = Filter(result, func(id string) bool{ - return FindPos(ch.Ids, id) < 0 + res = Filter(res, func(id T) bool { + return findPos(ch.Items, id.GetId()) < 0 }) case OperationReplace: - result = ch.Ids + res = ch.Items } } - return result + return res } diff --git a/util/slice/diff_test.go b/util/slice/diff_test.go index 832a170e4..b4789e78e 100644 --- a/util/slice/diff_test.go +++ b/util/slice/diff_test.go @@ -1,37 +1,39 @@ package slice import ( - "github.com/globalsign/mgo/bson" - "github.com/stretchr/testify/assert" "math/rand" "testing" "time" + + "github.com/globalsign/mgo/bson" + "github.com/stretchr/testify/assert" ) func Test_Diff(t *testing.T) { origin := []string{"000", "001", "002", "003", "004", "005", "006", "007", "008", "009"} changed := []string{"000", "008", "001", "002", "003", "005", "006", "007", "009", "004"} - chs := Diff(origin, changed) + chs := Diff(StringsToIDs(origin), StringsToIDs(changed)) - assert.Equal(t, chs, []Change{ - {Op: OperationMove, Ids: []string{"008"}, AfterId: "000"}, - {Op: OperationMove, Ids: []string{"004"}, AfterId: "009"}}, + assert.Equal(t, chs, []Change[ID]{ + {Op: OperationMove, Items: []ID{"008"}, AfterId: "000"}, + {Op: OperationMove, Items: []ID{"004"}, AfterId: "009"}}, ) } func Test_ChangesApply(t *testing.T) { origin := []string{"000", "001", "002", "003", "004", "005", "006", "007", "008", "009"} - changed := []string{"000", "008", "001", "002", "003", "005", "006", "007", "009", "004", "new"} + changed := []ID{"000", "008", "001", "002", "003", "005", "006", "007", "009", "004", "new"} - chs := Diff(origin, changed) + chs := Diff(StringsToIDs(origin), changed) - res := ApplyChanges(origin, chs) + res := ApplyChanges(StringsToIDs(origin), chs) assert.Equal(t, changed, res) } func Test_SameLength(t *testing.T) { + // TODO use quickcheck here for i := 0; i < 10000; i++ { l := randNum(5, 200) origin := getRandArray(l) @@ -40,10 +42,10 @@ func Test_SameLength(t *testing.T) { rand.Shuffle(len(changed), func(i, j int) { changed[i], changed[j] = changed[j], changed[i] }) - chs := Diff(origin, changed) - res := ApplyChanges(origin, chs) + chs := Diff(StringsToIDs(origin), StringsToIDs(changed)) + res := ApplyChanges(StringsToIDs(origin), chs) - assert.Equal(t, res, changed) + assert.Equal(t, res, StringsToIDs(changed)) } } @@ -76,19 +78,19 @@ func Test_DifferentLength(t *testing.T) { changed = Insert(changed, insIdx, []string{bson.NewObjectId().Hex()}...) } - chs := Diff(origin, changed) - res := ApplyChanges(origin, chs) + chs := Diff(StringsToIDs(origin), StringsToIDs(changed)) + res := ApplyChanges(StringsToIDs(origin), chs) - assert.Equal(t, res, changed) + assert.Equal(t, res, StringsToIDs(changed)) } } -func randNum(min, max int) int{ +func randNum(min, max int) int { if max <= min { return max } rand.Seed(time.Now().UnixNano()) - return rand.Intn(max - min) + min + return rand.Intn(max-min) + min } func getRandArray(len int) []string { diff --git a/util/slice/slice.go b/util/slice/slice.go index 529f3e506..02f4fba9e 100644 --- a/util/slice/slice.go +++ b/util/slice/slice.go @@ -42,7 +42,7 @@ func DifferenceRemovedAdded(a, b []string) (removed []string, added []string) { return } -func FindPos(s []string, v string) int { +func FindPos[T comparable](s []T, v T) int { for i, sv := range s { if sv == v { return i @@ -62,7 +62,7 @@ func Difference(a, b []string) []string { return diff } -func Insert(s []string, pos int, v ...string) []string { +func Insert[T any](s []T, pos int, v ...T) []T { if len(s) <= pos { return append(s, v...) } @@ -84,8 +84,8 @@ func Remove(s []string, v string) []string { return s[:n] } -func Filter(vals []string, cond func(string) bool) []string { - var result = make([]string, 0, len(vals)) +func Filter[T any](vals []T, cond func(T) bool) []T { + var result = make([]T, 0, len(vals)) for i := range vals { if cond(vals[i]) { result = append(result, vals[i])