Skip to content

Commit be3365b

Browse files
committed
feat: LCS-based movement implementation
This new algorithm implements generation of move actions by utilizing a longest common substring algorithm (LCS). First, based on the position type, we generate a list describing the expected order of the elements. LCS algorithm is then used to find the longest sequence of items that is shared between the expected list, and an actual list (e.g. a list of entries from the server). Once longest sequence is known, we figure out the least amount of moves to translate existing list into its expected form, and those movements are returned back.
1 parent 11eb059 commit be3365b

File tree

3 files changed

+545
-0
lines changed

3 files changed

+545
-0
lines changed

assets/pango/movement/movement.go

Lines changed: 380 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,380 @@
1+
package movement
2+
3+
import (
4+
"fmt"
5+
"log/slog"
6+
"slices"
7+
)
8+
9+
var _ = slog.LevelDebug
10+
11+
type Movable interface {
12+
EntryName() string
13+
}
14+
15+
type MoveAction struct {
16+
EntryName string
17+
Where string
18+
Destination string
19+
}
20+
21+
type Position interface {
22+
Move(entries []Movable, existing []Movable) ([]MoveAction, error)
23+
}
24+
25+
type PositionTop struct{}
26+
27+
type PositionBottom struct{}
28+
29+
type PositionBefore struct {
30+
Directly bool
31+
Pivot Movable
32+
}
33+
34+
type PositionAfter struct {
35+
Directly bool
36+
Pivot Movable
37+
}
38+
39+
func removeEntriesFromExisting(entries []Movable, filterFn func(entry Movable) bool) []Movable {
40+
entryNames := make(map[string]bool, len(entries))
41+
for _, elt := range entries {
42+
entryNames[elt.EntryName()] = true
43+
}
44+
45+
filtered := make([]Movable, len(entries))
46+
copy(filtered, entries)
47+
48+
filtered = slices.DeleteFunc(filtered, filterFn)
49+
50+
return filtered
51+
}
52+
53+
func findPivotIdx(entries []Movable, pivot Movable) int {
54+
return slices.IndexFunc(entries, func(entry Movable) bool {
55+
if entry.EntryName() == pivot.EntryName() {
56+
return true
57+
}
58+
59+
return false
60+
})
61+
62+
}
63+
64+
type movementType int
65+
66+
const (
67+
movementBefore movementType = iota
68+
movementAfter
69+
)
70+
71+
func processPivotMovement(entries []Movable, existing []Movable, pivot Movable, direct bool, movement movementType) ([]MoveAction, error) {
72+
existingLen := len(existing)
73+
existingIdxMap := make(map[Movable]int, existingLen)
74+
75+
for idx, elt := range existing {
76+
existingIdxMap[elt] = idx
77+
}
78+
79+
pivotIdx := findPivotIdx(existing, pivot)
80+
if pivotIdx == -1 {
81+
return nil, fmt.Errorf("pivot point not found in the list of existing items")
82+
}
83+
84+
if !direct {
85+
movementRequired := false
86+
entriesLen := len(entries)
87+
loop:
88+
for i := 0; i < entriesLen; i++ {
89+
90+
// For any given entry in the list of entries to move check if the entry
91+
// index is at or after pivot point index, which will require movement
92+
// set to be generated.
93+
existingEntryIdx := existingIdxMap[entries[i]]
94+
switch movement {
95+
case movementBefore:
96+
if existingEntryIdx >= pivotIdx {
97+
movementRequired = true
98+
break
99+
}
100+
case movementAfter:
101+
if existingEntryIdx <= pivotIdx {
102+
movementRequired = true
103+
break
104+
}
105+
}
106+
107+
if i == 0 {
108+
continue
109+
}
110+
111+
// Check if the entries to be moved have the same order in the existing
112+
// slice, and if not require a movement set to be generated.
113+
switch movement {
114+
case movementBefore:
115+
if existingIdxMap[entries[i-1]] >= existingEntryIdx {
116+
movementRequired = true
117+
break loop
118+
119+
}
120+
case movementAfter:
121+
if existingIdxMap[entries[i-1]] <= existingEntryIdx {
122+
movementRequired = true
123+
break loop
124+
125+
}
126+
127+
}
128+
}
129+
130+
if !movementRequired {
131+
return nil, nil
132+
}
133+
}
134+
135+
expected := make([]Movable, len(existing))
136+
137+
entriesIdxMap := make(map[Movable]int, len(entries))
138+
for idx, elt := range entries {
139+
entriesIdxMap[elt] = idx
140+
}
141+
142+
filtered := removeEntriesFromExisting(existing, func(entry Movable) bool {
143+
_, ok := entriesIdxMap[entry]
144+
return ok
145+
})
146+
147+
filteredPivotIdx := findPivotIdx(filtered, pivot)
148+
149+
switch movement {
150+
case movementBefore:
151+
expectedIdx := 0
152+
for ; expectedIdx < filteredPivotIdx; expectedIdx++ {
153+
expected[expectedIdx] = filtered[expectedIdx]
154+
}
155+
156+
for _, elt := range entries {
157+
expected[expectedIdx] = elt
158+
expectedIdx++
159+
}
160+
161+
expected[expectedIdx] = pivot
162+
expectedIdx++
163+
164+
filteredLen := len(filtered)
165+
for i := filteredPivotIdx + 1; i < filteredLen; i++ {
166+
expected[expectedIdx] = filtered[i]
167+
expectedIdx++
168+
}
169+
}
170+
171+
return GenerateMovements(existing, expected, entries)
172+
}
173+
174+
func (o PositionAfter) Move(entries []Movable, existing []Movable) ([]MoveAction, error) {
175+
return processPivotMovement(entries, existing, o.Pivot, o.Directly, movementAfter)
176+
}
177+
178+
func (o PositionBefore) Move(entries []Movable, existing []Movable) ([]MoveAction, error) {
179+
return processPivotMovement(entries, existing, o.Pivot, o.Directly, movementBefore)
180+
}
181+
182+
type Entry struct {
183+
Element Movable
184+
Expected int
185+
Existing int
186+
}
187+
188+
type sequencePosition struct {
189+
Start int
190+
End int
191+
}
192+
193+
func longestCommonSubsequence(S []Movable, T []Movable) [][]Movable {
194+
195+
r := len(S)
196+
n := len(T)
197+
198+
L := make([][]int, r)
199+
for idx := range len(T) {
200+
L[idx] = make([]int, n)
201+
}
202+
z := 0
203+
204+
var results [][]Movable
205+
206+
for i := 0; i < r; i++ {
207+
for j := 0; j < n; j++ {
208+
if S[i].EntryName() == T[j].EntryName() {
209+
if i == 0 || j == 0 {
210+
L[i][j] = 1
211+
} else {
212+
L[i][j] = L[i-1][j-1] + 1
213+
}
214+
215+
if L[i][j] > z {
216+
slog.Debug("L[i][j] > z", "L[i][j]", L[i][j], "z", z, "i-z", i-z, "i", i)
217+
results = nil
218+
results = append(results, S[i-z:i+1])
219+
z = L[i][j]
220+
slog.Debug("L[i][j] > z", "results", results)
221+
} else if L[i][j] == z {
222+
results = append(results, S[i-z:i+1])
223+
slog.Debug("L[i][j] == z", "i-z", i, "i", i+1)
224+
}
225+
slog.Debug("Still", "results", results)
226+
} else {
227+
L[i][j] = 0
228+
}
229+
}
230+
}
231+
232+
slog.Debug("commonSubsequence", "results", results)
233+
234+
return results
235+
}
236+
237+
func GenerateMovements(existing []Movable, expected []Movable, entries []Movable) ([]MoveAction, error) {
238+
if len(existing) != len(expected) {
239+
return nil, fmt.Errorf("existing length != expected length: %d != %d", len(existing), len(expected))
240+
}
241+
242+
common := longestCommonSubsequence(existing, expected)
243+
244+
entriesIdxMap := make(map[Movable]int, len(entries))
245+
for idx, elt := range entries {
246+
entriesIdxMap[elt] = idx
247+
}
248+
249+
var commonSequence []Movable
250+
for _, elt := range common {
251+
filtered := removeEntriesFromExisting(elt, func(elt Movable) bool {
252+
_, ok := entriesIdxMap[elt]
253+
return ok
254+
})
255+
256+
if len(filtered) > len(commonSequence) {
257+
commonSequence = filtered
258+
}
259+
260+
}
261+
262+
existingIdxMap := make(map[Movable]int, len(existing))
263+
for idx, elt := range existing {
264+
existingIdxMap[elt] = idx
265+
}
266+
267+
expectedIdxMap := make(map[Movable]int, len(expected))
268+
for idx, elt := range expected {
269+
expectedIdxMap[elt] = idx
270+
}
271+
272+
commonLen := len(commonSequence)
273+
commonIdxMap := make(map[Movable]int, len(commonSequence))
274+
for idx, elt := range commonSequence {
275+
commonIdxMap[elt] = idx
276+
}
277+
278+
var movements []MoveAction
279+
280+
var previous Movable
281+
for _, elt := range entries {
282+
slog.Debug("GenerateMovements", "elt", elt.EntryName(), "existingIdx", existingIdxMap[elt], "expectedIdx", expectedIdxMap[elt])
283+
if existingIdxMap[elt] == expectedIdxMap[elt] {
284+
continue
285+
}
286+
287+
if expectedIdxMap[elt] == 0 {
288+
movements = append(movements, MoveAction{
289+
EntryName: elt.EntryName(),
290+
Destination: "top",
291+
Where: "top",
292+
})
293+
previous = elt
294+
} else if len(commonSequence) > 0 {
295+
if expectedIdxMap[elt] < expectedIdxMap[commonSequence[0]] {
296+
if previous == nil {
297+
previous = expected[0]
298+
}
299+
movements = append(movements, MoveAction{
300+
EntryName: elt.EntryName(),
301+
Destination: previous.EntryName(),
302+
Where: "after",
303+
})
304+
previous = elt
305+
} else if expectedIdxMap[elt] > expectedIdxMap[commonSequence[commonLen-1]] {
306+
if previous == nil {
307+
previous = commonSequence[commonLen-1]
308+
}
309+
movements = append(movements, MoveAction{
310+
EntryName: elt.EntryName(),
311+
Destination: previous.EntryName(),
312+
Where: "after",
313+
})
314+
previous = elt
315+
316+
} else if expectedIdxMap[elt] > expectedIdxMap[commonSequence[0]] {
317+
if previous == nil {
318+
previous = commonSequence[0]
319+
}
320+
movements = append(movements, MoveAction{
321+
EntryName: elt.EntryName(),
322+
Destination: previous.EntryName(),
323+
Where: "after",
324+
})
325+
previous = elt
326+
}
327+
} else {
328+
movements = append(movements, MoveAction{
329+
EntryName: elt.EntryName(),
330+
Destination: previous.EntryName(),
331+
Where: "after",
332+
})
333+
previous = elt
334+
}
335+
336+
slog.Debug("GenerateMovements()", "existing", existingIdxMap[elt], "expected", expectedIdxMap[elt])
337+
}
338+
339+
_ = previous
340+
341+
slog.Debug("GenerateMovements()", "movements", movements)
342+
343+
return movements, nil
344+
}
345+
346+
func (o PositionTop) Move(entries []Movable, existing []Movable) ([]MoveAction, error) {
347+
entriesIdxMap := make(map[Movable]int, len(entries))
348+
for idx, elt := range entries {
349+
entriesIdxMap[elt] = idx
350+
}
351+
352+
filtered := removeEntriesFromExisting(existing, func(entry Movable) bool {
353+
_, ok := entriesIdxMap[entry]
354+
return ok
355+
})
356+
357+
expected := append(entries, filtered...)
358+
359+
return GenerateMovements(existing, expected, entries)
360+
}
361+
362+
func (o PositionBottom) Move(entries []Movable, existing []Movable) ([]MoveAction, error) {
363+
entriesIdxMap := make(map[Movable]int, len(entries))
364+
for idx, elt := range entries {
365+
entriesIdxMap[elt] = idx
366+
}
367+
368+
filtered := removeEntriesFromExisting(existing, func(entry Movable) bool {
369+
_, ok := entriesIdxMap[entry]
370+
return ok
371+
})
372+
373+
expected := append(filtered, entries...)
374+
375+
return GenerateMovements(existing, expected, entries)
376+
}
377+
378+
func MoveGroup(position Position, entries []Movable, existing []Movable) ([]MoveAction, error) {
379+
return position.Move(entries, existing)
380+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
package movement_test
2+
3+
import (
4+
"log/slog"
5+
"testing"
6+
7+
. "github.com/onsi/ginkgo/v2"
8+
. "github.com/onsi/gomega"
9+
)
10+
11+
func TestMovement(t *testing.T) {
12+
handler := slog.NewTextHandler(GinkgoWriter, &slog.HandlerOptions{
13+
Level: slog.LevelDebug,
14+
})
15+
slog.SetDefault(slog.New(handler))
16+
RegisterFailHandler(Fail)
17+
RunSpecs(t, "Movement Suite")
18+
}

0 commit comments

Comments
 (0)