mirror of
https://github.com/anyproto/anytype-heart.git
synced 2025-06-08 05:47:07 +09:00
328 lines
6.6 KiB
Go
328 lines
6.6 KiB
Go
package slice
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/mb0/diff"
|
|
"github.com/samber/lo"
|
|
"golang.org/x/exp/slices"
|
|
)
|
|
|
|
// StringIdentity is identity function: it returns its argument as string
|
|
func StringIdentity[T ~string](x T) string {
|
|
return string(x)
|
|
}
|
|
|
|
func Equal[T comparable](a, b T) bool {
|
|
return a == b
|
|
}
|
|
|
|
type Change[T any] struct {
|
|
changeAdd *ChangeAdd[T]
|
|
changeRemove *ChangeRemove
|
|
changeMove *ChangeMove
|
|
changeReplace *ChangeReplace[T]
|
|
}
|
|
|
|
func (c Change[T]) String() string {
|
|
switch {
|
|
case c.changeAdd != nil:
|
|
return c.changeAdd.String()
|
|
case c.changeRemove != nil:
|
|
return c.changeRemove.String()
|
|
case c.changeMove != nil:
|
|
return c.changeMove.String()
|
|
case c.changeReplace != nil:
|
|
return c.changeReplace.String()
|
|
}
|
|
return ""
|
|
}
|
|
|
|
func MakeChangeAdd[T any](items []T, afterID string) Change[T] {
|
|
return Change[T]{
|
|
changeAdd: &ChangeAdd[T]{items, afterID},
|
|
}
|
|
}
|
|
|
|
func MakeChangeRemove[T any](ids []string) Change[T] {
|
|
return Change[T]{
|
|
changeRemove: &ChangeRemove{ids},
|
|
}
|
|
}
|
|
|
|
func MakeChangeMove[T any](ids []string, afterID string) Change[T] {
|
|
return Change[T]{
|
|
changeMove: &ChangeMove{ids, afterID},
|
|
}
|
|
}
|
|
|
|
func MakeChangeReplace[T any](item T, id string) Change[T] {
|
|
return Change[T]{
|
|
changeReplace: &ChangeReplace[T]{item, id},
|
|
}
|
|
}
|
|
|
|
func (c Change[T]) Len() int {
|
|
if c.changeAdd != nil {
|
|
return len(c.changeAdd.Items)
|
|
}
|
|
if c.changeRemove != nil {
|
|
return len(c.changeRemove.IDs)
|
|
}
|
|
if c.changeMove != nil {
|
|
return len(c.changeMove.IDs)
|
|
}
|
|
if c.changeReplace != nil {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
func (c *Change[T]) Add() *ChangeAdd[T] {
|
|
return c.changeAdd
|
|
}
|
|
|
|
func (c *Change[T]) Remove() *ChangeRemove {
|
|
return c.changeRemove
|
|
}
|
|
|
|
func (c *Change[T]) Move() *ChangeMove {
|
|
return c.changeMove
|
|
}
|
|
|
|
func (c *Change[T]) Replace() *ChangeReplace[T] {
|
|
return c.changeReplace
|
|
}
|
|
|
|
type ChangeAdd[T any] struct {
|
|
Items []T
|
|
AfterID string
|
|
}
|
|
|
|
func (c ChangeAdd[T]) String() string {
|
|
return fmt.Sprintf("add %v after %s", c.Items, c.AfterID)
|
|
}
|
|
|
|
type ChangeMove struct {
|
|
IDs []string
|
|
AfterID string
|
|
}
|
|
|
|
func (c ChangeMove) String() string {
|
|
return fmt.Sprintf("move %v after %s", c.IDs, c.AfterID)
|
|
}
|
|
|
|
type ChangeRemove struct {
|
|
IDs []string
|
|
}
|
|
|
|
func (c ChangeRemove) String() string {
|
|
return fmt.Sprintf("remove %v", c.IDs)
|
|
}
|
|
|
|
type ChangeReplace[T any] struct {
|
|
Item T
|
|
ID string
|
|
}
|
|
|
|
func (c ChangeReplace[T]) String() string {
|
|
return fmt.Sprintf("replace %v after %s", c.Item, c.ID)
|
|
}
|
|
|
|
type MixedInput[T any] struct {
|
|
A []T
|
|
B []T
|
|
getID func(T) string
|
|
}
|
|
|
|
func (m *MixedInput[T]) Equal(a, b int) bool {
|
|
return m.getID(m.A[a]) == m.getID(m.B[b])
|
|
}
|
|
|
|
// Diff returns a list of changes that need to be applied to origin slice to become changed
|
|
// returned slice is reproducible and deterministic
|
|
func Diff[T any](origin, changed []T, getID func(T) string, equal func(T, T) bool) []Change[T] {
|
|
origin = lo.UniqBy(origin, getID)
|
|
changed = lo.UniqBy(changed, getID)
|
|
|
|
m := &MixedInput[T]{
|
|
A: origin,
|
|
B: changed,
|
|
getID: getID,
|
|
}
|
|
|
|
var result []Change[T]
|
|
|
|
changes := diff.Diff(len(m.A), len(m.B), m)
|
|
delMap := make(map[string]struct{})
|
|
|
|
changedMap := make(map[string]T)
|
|
for _, c := range changed {
|
|
changedMap[getID(c)] = c
|
|
}
|
|
for _, c := range origin {
|
|
v, ok := changedMap[getID(c)]
|
|
if !ok {
|
|
continue
|
|
}
|
|
if !equal(c, v) {
|
|
result = append(result, MakeChangeReplace[T](v, getID(c)))
|
|
}
|
|
}
|
|
|
|
for _, c := range changes {
|
|
if c.Del <= 0 {
|
|
continue
|
|
}
|
|
for _, it := range m.A[c.A : c.A+c.Del] {
|
|
delMap[getID(it)] = struct{}{}
|
|
}
|
|
}
|
|
|
|
for _, c := range changes {
|
|
if c.Ins <= 0 {
|
|
continue
|
|
}
|
|
|
|
inserts := m.B[c.B : c.B+c.Ins]
|
|
var afterID string
|
|
if c.A > 0 {
|
|
afterID = getID(m.A[c.A-1])
|
|
}
|
|
var oneCh Change[T]
|
|
for _, it := range inserts {
|
|
id := getID(it)
|
|
|
|
// If inserted item is found in deleted map then it is a Move operation
|
|
// nolint:nestif
|
|
if _, ok := delMap[id]; ok {
|
|
mv := oneCh.Move()
|
|
if mv == nil {
|
|
if oneCh.Len() > 0 {
|
|
result = append(result, oneCh)
|
|
}
|
|
oneCh = MakeChangeMove[T](nil, afterID)
|
|
mv = oneCh.Move()
|
|
}
|
|
mv.IDs = append(mv.IDs, getID(it))
|
|
delete(delMap, id)
|
|
} else { // insert new
|
|
add := oneCh.Add()
|
|
if add == nil {
|
|
if oneCh.Len() > 0 {
|
|
result = append(result, oneCh)
|
|
}
|
|
oneCh = MakeChangeAdd[T](nil, afterID)
|
|
add = oneCh.Add()
|
|
}
|
|
add.Items = append(add.Items, it)
|
|
}
|
|
afterID = id
|
|
}
|
|
|
|
if oneCh.Len() > 0 {
|
|
result = append(result, oneCh)
|
|
}
|
|
|
|
}
|
|
|
|
if len(delMap) > 0 { // remove
|
|
delIDs := make([]string, 0, len(delMap))
|
|
for id := range delMap {
|
|
delIDs = append(delIDs, id)
|
|
}
|
|
// because before we have iterated on map, make sure we always have the same output for the same input
|
|
slices.Sort(delIDs)
|
|
result = append(result, MakeChangeRemove[T](delIDs))
|
|
}
|
|
return result
|
|
}
|
|
|
|
func findPos[T any](s []T, getID func(T) string, id string) int {
|
|
for i, sv := range s {
|
|
if getID(sv) == id {
|
|
return i
|
|
}
|
|
}
|
|
return -1
|
|
}
|
|
|
|
func ApplyChanges[T any](origin []T, changes []Change[T], getID func(T) string) []T {
|
|
res := lo.UniqBy(origin, getID)
|
|
|
|
itemsMap := make(map[string]T, len(origin))
|
|
for _, it := range origin {
|
|
itemsMap[getID(it)] = it
|
|
}
|
|
|
|
for _, ch := range changes {
|
|
if add := ch.Add(); add != nil {
|
|
pos := -1
|
|
if add.AfterID != "" {
|
|
pos = findPos(res, getID, add.AfterID)
|
|
}
|
|
res = Insert(res, pos+1, add.Items...)
|
|
}
|
|
|
|
if move := ch.Move(); move != nil {
|
|
res = FilterMut(res, func(id T) bool {
|
|
return FindPos(move.IDs, getID(id)) < 0
|
|
})
|
|
|
|
pos := -1
|
|
if move.AfterID != "" {
|
|
pos = findPos(res, getID, move.AfterID)
|
|
}
|
|
items := make([]T, 0, len(move.IDs))
|
|
for _, id := range move.IDs {
|
|
v, ok := itemsMap[id]
|
|
if !ok {
|
|
continue
|
|
}
|
|
items = append(items, v)
|
|
}
|
|
res = Insert(res, pos+1, items...)
|
|
}
|
|
|
|
if rm := ch.Remove(); rm != nil {
|
|
res = FilterMut(res, func(id T) bool {
|
|
return FindPos(rm.IDs, getID(id)) < 0
|
|
})
|
|
}
|
|
|
|
if replace := ch.Replace(); replace != nil {
|
|
itemsMap[replace.ID] = replace.Item
|
|
pos := findPos(res, getID, replace.ID)
|
|
if pos >= 0 && pos < len(res) {
|
|
res[pos] = replace.Item
|
|
}
|
|
}
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
func UnwrapChanges[T, R any](
|
|
changes []Change[T],
|
|
add func(afterID string, items []T) R,
|
|
remove func(ids []string) R,
|
|
move func(afterID string, ids []string) R,
|
|
update func(id string, item T) R,
|
|
) []R {
|
|
res := make([]R, 0, len(changes))
|
|
for _, c := range changes {
|
|
if v := c.Add(); v != nil {
|
|
res = append(res, add(v.AfterID, v.Items))
|
|
}
|
|
if v := c.Remove(); v != nil {
|
|
res = append(res, remove(v.IDs))
|
|
}
|
|
if v := c.Move(); v != nil {
|
|
res = append(res, move(v.AfterID, v.IDs))
|
|
}
|
|
if v := c.Replace(); v != nil {
|
|
res = append(res, update(v.ID, v.Item))
|
|
}
|
|
}
|
|
return res
|
|
}
|