From e101de5a87fea4d2c28cbffbcaf8eca1f6b8b689 Mon Sep 17 00:00:00 2001 From: Krzysztof Klimonda Date: Fri, 2 Aug 2024 09:48:07 +0200 Subject: [PATCH] Rewrite GenerateMovements() to work without LCS algorithm --- assets/pango/movement/movement.go | 284 +++++++++++-------------- assets/pango/movement/movement_test.go | 209 ++++++++++++++---- 2 files changed, 289 insertions(+), 204 deletions(-) diff --git a/assets/pango/movement/movement.go b/assets/pango/movement/movement.go index 633c8e9b..e105328f 100644 --- a/assets/pango/movement/movement.go +++ b/assets/pango/movement/movement.go @@ -2,7 +2,6 @@ package movement import ( "errors" - "fmt" "log/slog" "slices" ) @@ -30,6 +29,7 @@ type MoveAction struct { type Position interface { Move(entries []Movable, existing []Movable) ([]MoveAction, error) + GetExpected(entries []Movable, existing []Movable) ([]Movable, error) } type PositionTop struct{} @@ -79,14 +79,8 @@ func findPivotIdx(entries []Movable, pivot Movable) int { } -type movementType int - -const ( - movementBefore movementType = iota - movementAfter -) - var ( + errNoMovements = errors.New("no movements needed") ErrSlicesNotEqualLength = errors.New("existing and expected slices length mismatch") ErrPivotInEntries = errors.New("pivot element found in the entries slice") ErrPivotNotInExisting = errors.New("pivot element not foudn in the existing slice") @@ -95,17 +89,17 @@ var ( // PositionBefore and PositionAfter are similar enough that we can generate expected sequences // for both using the same code and some conditionals based on the given movement. -func processPivotMovement(entries []Movable, existing []Movable, pivot Movable, direct bool, movement movementType) ([]Movable, []MoveAction, error) { +func getPivotMovement(entries []Movable, existing []Movable, pivot Movable, direct bool, movement ActionWhereType) ([]Movable, error) { existingIdxMap := createIdxMapFor(existing) entriesPivotIdx := findPivotIdx(entries, pivot) if entriesPivotIdx != -1 { - return nil, nil, ErrPivotInEntries + return nil, ErrPivotInEntries } existingPivotIdx := findPivotIdx(existing, pivot) if existingPivotIdx == -1 { - return nil, nil, ErrPivotNotInExisting + return nil, ErrPivotNotInExisting } if !direct { @@ -114,7 +108,6 @@ func processPivotMovement(entries []Movable, existing []Movable, pivot Movable, loop: for i := 0; i < entriesLen; i++ { existingEntryIdx := existingIdxMap[entries[i]] - slog.Debug("generate()", "i", i, "len(entries)", len(entries), "entry", entries[i], "existingEntryIdx", existingEntryIdx, "existingPivotIdx", existingPivotIdx) // For any given entry in the list of entries to move check if the entry // index is at or after pivot point index, which will require movement // set to be generated. @@ -122,7 +115,7 @@ func processPivotMovement(entries []Movable, existing []Movable, pivot Movable, // Then check if the entries to be moved have the same order in the existing // slice, and if not require a movement set to be generated. switch movement { - case movementBefore: + case ActionWhereBefore: if existingEntryIdx >= existingPivotIdx { movementRequired = true break @@ -137,7 +130,7 @@ func processPivotMovement(entries []Movable, existing []Movable, pivot Movable, break loop } - case movementAfter: + case ActionWhereAfter: if existingEntryIdx <= existingPivotIdx { movementRequired = true break @@ -157,7 +150,7 @@ func processPivotMovement(entries []Movable, existing []Movable, pivot Movable, } if !movementRequired { - return nil, nil, nil + return nil, errNoMovements } } @@ -172,37 +165,28 @@ func processPivotMovement(entries []Movable, existing []Movable, pivot Movable, filteredPivotIdx := findPivotIdx(filtered, pivot) - slog.Debug("pivot()", "existing", existing, "filtered", filtered, "filteredPivotIdx", filteredPivotIdx) switch movement { - case movementBefore: + case ActionWhereBefore: expectedIdx := 0 for ; expectedIdx < filteredPivotIdx; expectedIdx++ { expected[expectedIdx] = filtered[expectedIdx] } - slog.Debug("pivot()", "expected", expected) - for _, elt := range entries { expected[expectedIdx] = elt expectedIdx++ } - slog.Debug("pivot()", "expected", expected) - expected[expectedIdx] = pivot expectedIdx++ - slog.Debug("pivot()", "expected", expected) - filteredLen := len(filtered) for i := filteredPivotIdx + 1; i < filteredLen; i++ { expected[expectedIdx] = filtered[i] expectedIdx++ } - slog.Debug("pivot()", "expected", expected) - case movementAfter: - slog.Debug("pivot()", "filtered", filtered) + case ActionWhereAfter: expectedIdx := 0 for ; expectedIdx < filteredPivotIdx+1; expectedIdx++ { expected[expectedIdx] = filtered[expectedIdx] @@ -214,8 +198,6 @@ func processPivotMovement(entries []Movable, existing []Movable, pivot Movable, expectedIdx++ } - slog.Debug("pivot()", "expected", expected) - filteredLen := len(filtered) for i := filteredPivotIdx + 1; i < filteredLen; i++ { expected[expectedIdx] = filtered[i] @@ -227,23 +209,31 @@ func processPivotMovement(entries []Movable, existing []Movable, pivot Movable, expectedIdx++ } - slog.Debug("pivot()", "expected", expected) - for _, elt := range entries { expected[expectedIdx] = elt expectedIdx++ } - slog.Debug("pivot()", "expected", expected) } } - actions, err := GenerateMovements(existing, expected, entries, movement) - return expected, actions, err + return expected, nil +} + +func (o PositionAfter) GetExpected(entries []Movable, existing []Movable) ([]Movable, error) { + return getPivotMovement(entries, existing, o.Pivot, o.Directly, ActionWhereAfter) } func (o PositionAfter) Move(entries []Movable, existing []Movable) ([]MoveAction, error) { - expected, actions, err := processPivotMovement(entries, existing, o.Pivot, o.Directly, movementAfter) + expected, err := o.GetExpected(entries, existing) + if err != nil { + if errors.Is(err, errNoMovements) { + return nil, nil + } + return nil, err + } + + actions, err := GenerateMovements(existing, expected, entries, ActionWhereAfter, o.Pivot, o.Directly) if err != nil { return nil, err } @@ -251,8 +241,22 @@ func (o PositionAfter) Move(entries []Movable, existing []Movable) ([]MoveAction return OptimizeMovements(existing, expected, entries, actions, o), nil } +func (o PositionBefore) GetExpected(entries []Movable, existing []Movable) ([]Movable, error) { + return getPivotMovement(entries, existing, o.Pivot, o.Directly, ActionWhereBefore) +} + func (o PositionBefore) Move(entries []Movable, existing []Movable) ([]MoveAction, error) { - expected, actions, err := processPivotMovement(entries, existing, o.Pivot, o.Directly, movementBefore) + expected, err := o.GetExpected(entries, existing) + if err != nil { + if errors.Is(err, errNoMovements) { + return nil, nil + } + return nil, err + } + + slog.Debug("PositionBefore.Move()", "existing", existing, "expected", expected, "entries", entries) + + actions, err := GenerateMovements(existing, expected, entries, ActionWhereBefore, o.Pivot, o.Directly) if err != nil { return nil, err } @@ -325,44 +329,24 @@ func OptimizeMovements(existing []Movable, expected []Movable, entries []Movable } } - slog.Debug("OptimiveMovements()", "optimized", optimized) + slog.Debug("OptimizeMovements()", "optimized", optimized) + return optimized } -func GenerateMovements(existing []Movable, expected []Movable, entries []Movable, movement movementType) ([]MoveAction, error) { - slog.Debug("GenerateMovements()", "existing", existing, "expected", expected) +func GenerateMovements(existing []Movable, expected []Movable, entries []Movable, movement ActionWhereType, pivot Movable, directly bool) ([]MoveAction, error) { if len(existing) != len(expected) { return nil, ErrSlicesNotEqualLength } - commonSequences := LongestCommonSubstring(existing, expected) - entriesIdxMap := createIdxMapFor(entries) - - // LCS returns a list of longest common sequences found between existing and expected - // slices. We want to find the longest common sequence that doesn't intersect entries - // given by the user, as entries are moved in relation to the common sequence. - var common []Movable - for _, sequence := range commonSequences { - filtered := removeEntriesFromExisting(sequence, func(elt Movable) bool { - _, ok := entriesIdxMap[elt] - return ok - }) - - if len(filtered) > len(common) { - common = filtered - } - - } - commonLen := len(common) - existingIdxMap := createIdxMapFor(existing) expectedIdxMap := createIdxMapFor(expected) var movements []MoveAction - var previous Movable for _, elt := range entries { + slog.Debug("GenerateMovements()", "elt", elt, "existing", existingIdxMap[elt], "expected", expectedIdxMap[elt], "len(expected)", len(expected)) // If existing index for the element matches the expected one, skip it over if existingIdxMap[elt] == expectedIdxMap[elt] { continue @@ -375,7 +359,7 @@ func GenerateMovements(existing []Movable, expected []Movable, entries []Movable Where: ActionWhereTop, }) previous = elt - } else if expectedIdxMap[elt] == len(expectedIdxMap) { + } else if expectedIdxMap[elt] == len(expectedIdxMap)-1 { movements = append(movements, MoveAction{ Movable: elt, Destination: nil, @@ -392,18 +376,41 @@ func GenerateMovements(existing []Movable, expected []Movable, entries []Movable } else { var where ActionWhereType + var pivot Movable switch movement { - case movementAfter: - previous = common[commonLen-1] + case ActionWhereBottom: + pivot = existing[len(existing)-1] + where = ActionWhereAfter + case ActionWhereAfter: + pivot = expected[expectedIdxMap[elt]-1] where = ActionWhereAfter - case movementBefore: - previous = common[0] + case ActionWhereTop: + pivot = existing[0] + where = ActionWhereBefore + case ActionWhereBefore: + eltExpectedIdx := expectedIdxMap[elt] + pivot = expected[eltExpectedIdx+1] where = ActionWhereBefore + // if previous was nil (we are processing the first element in entries set) + // and selected pivot is part of the entries set it means the order of entries + // changes between existing and expected sets. If direct move has been requested, + // we need to find the correct pivot point for the move. + if _, ok := entriesIdxMap[pivot]; ok && directly { + // The actual pivot for the move is the element that follows all elements + // from the existing set. + pivotIdx := eltExpectedIdx + len(entries) + if pivotIdx >= len(expected) { + // This should never happen as by definition there is at least + // element (pivot point) at the end of the expected slice. + return nil, ErrInvalidMovementPlan + } + pivot = expected[pivotIdx] + } } movements = append(movements, MoveAction{ Movable: elt, - Destination: previous, + Destination: pivot, Where: where, }) previous = elt @@ -412,12 +419,12 @@ func GenerateMovements(existing []Movable, expected []Movable, entries []Movable _ = previous - slog.Debug("GenerateMovements()", "movements", movements) + slog.Debug("GeneraveMovements()", "movements", movements) return movements, nil } -func (o PositionTop) Move(entries []Movable, existing []Movable) ([]MoveAction, error) { +func (o PositionTop) GetExpected(entries []Movable, existing []Movable) ([]Movable, error) { entriesIdxMap := createIdxMapFor(entries) filtered := removeEntriesFromExisting(existing, func(entry Movable) bool { @@ -427,7 +434,15 @@ func (o PositionTop) Move(entries []Movable, existing []Movable) ([]MoveAction, expected := append(entries, filtered...) - actions, err := GenerateMovements(existing, expected, entries, movementBefore) + return expected, nil +} + +func (o PositionTop) Move(entries []Movable, existing []Movable) ([]MoveAction, error) { + expected, err := o.GetExpected(entries, existing) + if err != nil { + return nil, err + } + actions, err := GenerateMovements(existing, expected, entries, ActionWhereTop, nil, false) if err != nil { return nil, err } @@ -435,7 +450,7 @@ func (o PositionTop) Move(entries []Movable, existing []Movable) ([]MoveAction, return OptimizeMovements(existing, expected, entries, actions, o), nil } -func (o PositionBottom) Move(entries []Movable, existing []Movable) ([]MoveAction, error) { +func (o PositionBottom) GetExpected(entries []Movable, existing []Movable) ([]Movable, error) { entriesIdxMap := createIdxMapFor(entries) filtered := removeEntriesFromExisting(existing, func(entry Movable) bool { @@ -445,112 +460,55 @@ func (o PositionBottom) Move(entries []Movable, existing []Movable) ([]MoveActio expected := append(filtered, entries...) - actions, err := GenerateMovements(existing, expected, entries, movementAfter) - if err != nil { - return nil, err - } - return OptimizeMovements(existing, expected, entries, actions, o), nil -} - -func MoveGroup(position Position, entries []Movable, existing []Movable) ([]MoveAction, error) { - return position.Move(entries, existing) + return expected, nil } -// Debug helper to print generated LCS matrix -func printLCSMatrix(S []Movable, T []Movable, L [][]int) { - r := len(S) - n := len(T) - - line := " " - for _, elt := range S { - line += fmt.Sprintf("%s ", elt.EntryName()) - } - slog.Debug("LCS", "line", line) - - line = " " - for _, elt := range L[0] { - line += fmt.Sprintf("%d ", elt) +func (o PositionBottom) Move(entries []Movable, existing []Movable) ([]MoveAction, error) { + slog.Debug("PositionBottom.Move())", "entries", entries, "existing", existing) + expected, err := o.GetExpected(entries, existing) + if err != nil { + return nil, err } - slog.Debug("LCS", "line", line) - for i := 1; i < r+1; i++ { - line = fmt.Sprintf("%s ", T[i-1].EntryName()) - for j := 0; j < n+1; j++ { - line += fmt.Sprintf("%d ", L[i][j]) - } + actions, err := GenerateMovements(existing, expected, entries, ActionWhereBottom, nil, false) + if err != nil { + return nil, err } - + return OptimizeMovements(existing, expected, entries, actions, o), nil } -// LongestCommonSubstring implements dynamic programming variant of the algorithm -// -// See https://en.wikipedia.org/wiki/Longest_common_substring for the details. Our -// implementation is not optimal, as generation of the matrix can be done at the -// same time as finding LCSs, but it's easier to reason about for now. -func LongestCommonSubstring(S []Movable, T []Movable) [][]Movable { - r := len(S) - n := len(T) - - L := make([][]int, r+1) - for idx := range r + 1 { - L[idx] = make([]int, n+1) - } - - for i := 1; i < r+1; i++ { - for j := 1; j < n+1; j++ { - if S[i-1].EntryName() == T[j-1].EntryName() { - if i == 1 { - L[j][i] = 1 - } else if j == 1 { - L[j][i] = 1 - } else { - L[j][i] = L[j-1][i-1] + 1 - } - } - } - } +type Movement struct { + Entries []Movable + Position Position +} - var results [][]Movable - var lcsList [][]Movable - - var entry []Movable - var index int - for i := r; i > 0; i-- { - for j := n; j > 0; j-- { - if S[i-1].EntryName() == T[j-1].EntryName() { - if L[j][i] >= index { - if len(entry) > 0 { - var entries []string - for _, elt := range entry { - entries = append(entries, elt.EntryName()) - } - - lcsList = append(lcsList, entry) - } - index = L[j][i] - entry = []Movable{S[i-1]} - } else if L[j][i] < index { - index = L[j][i] - entry = append(entry, S[i-1]) - } else { - entry = []Movable{} - } +func MoveGroups(existing []Movable, movements []Movement) ([]MoveAction, error) { + expected := existing + for idx := range len(movements) - 1 { + position := movements[idx].Position + entries := movements[idx].Entries + slog.Debug("MoveGroups()", "position", position, "existing", existing, "entries", entries) + result, err := position.GetExpected(entries, expected) + if err != nil { + if !errors.Is(err, errNoMovements) { + return nil, err } + continue } + expected = result } - if len(entry) > 0 { - lcsList = append(lcsList, entry) - } + entries := movements[len(movements)-1].Entries + position := movements[len(movements)-1].Position + slog.Debug("MoveGroups()", "position", position, "expected", expected, "entries", entries) + return position.Move(entries, expected) +} - lcsLen := len(lcsList) - for idx := range lcsList { - elt := lcsList[lcsLen-idx-1] - if len(elt) > 1 { - slices.Reverse(elt) - results = append(results, elt) - } - } +func MoveGroup(position Position, entries []Movable, existing []Movable) ([]MoveAction, error) { + return position.Move(entries, existing) +} - return results +type Move struct { + Position Position + Existing []Movable } diff --git a/assets/pango/movement/movement_test.go b/assets/pango/movement/movement_test.go index 0703a54f..335099fd 100644 --- a/assets/pango/movement/movement_test.go +++ b/assets/pango/movement/movement_test.go @@ -29,39 +29,11 @@ func asMovable(mocks []string) []movement.Movable { return movables } -var _ = Describe("LCS", func() { - Context("with two common substrings", func() { - existing := asMovable([]string{"A", "B", "C", "D", "E"}) - expected := asMovable([]string{"C", "A", "B", "D", "E"}) - It("should return two sequences of two elements", func() { - options := movement.LongestCommonSubstring(existing, expected) - Expect(options).To(HaveLen(2)) - - Expect(options[0]).To(HaveExactElements(asMovable([]string{"A", "B"}))) - Expect(options[1]).To(HaveExactElements(asMovable([]string{"D", "E"}))) - }) - }) - // Context("with one very large common substring", func() { - // It("should return one sequence of elements in a reasonable time", Label("benchmark"), func() { - // var elts []string - // elements := 50000 - // for idx := range elements { - // elts = append(elts, fmt.Sprintf("%d", idx)) - // } - // existing := asMovable(elts) - // expected := existing - - // options := movement.LongestCommonSubstring(existing, expected) - // Expect(options).To(HaveLen(1)) - // Expect(options[0]).To(HaveLen(elements)) - // }) - // }) -}) - -var _ = Describe("Movement", func() { +var _ = Describe("MoveGroup()", func() { Context("With PositionTop used as position", func() { Context("when existing positions matches expected", func() { It("should generate no movements", func() { + // '(A B C) -> '(A B C) expected := asMovable([]string{"A", "B", "C"}) moves, err := movement.MoveGroup(movement.PositionTop{}, expected, expected) Expect(err).ToNot(HaveOccurred()) @@ -70,6 +42,7 @@ var _ = Describe("Movement", func() { }) Context("when it has to move two elements", func() { It("should generate three move actions", func() { + // '(D E A B C) -> '(A B C D E) entries := asMovable([]string{"A", "B", "C"}) existing := asMovable([]string{"D", "E", "A", "B", "C"}) @@ -92,6 +65,7 @@ var _ = Describe("Movement", func() { }) Context("when expected order is reversed", func() { It("should generate required move actions to converge lists", func() { + // '(A B C D E) -> '(E D C B A) entries := asMovable([]string{"E", "D", "C", "B", "A"}) existing := asMovable([]string{"A", "B", "C", "D", "E"}) moves, err := movement.MoveGroup(movement.PositionTop{}, entries, existing) @@ -101,9 +75,31 @@ var _ = Describe("Movement", func() { }) }) }) + Context("With PositionBottom used as position", func() { + Context("with non-consecutive entries", func() { + It("should generate two move actions", func() { + // '(A E B C D) -> '(A B D E C) + entries := asMovable([]string{"E", "C"}) + existing := asMovable([]string{"A", "E", "B", "C", "D"}) + + moves, err := movement.MoveGroup(movement.PositionBottom{}, entries, existing) + Expect(err).ToNot(HaveOccurred()) + Expect(moves).To(HaveLen(2)) + + Expect(moves[0].Movable.EntryName()).To(Equal("E")) + Expect(moves[0].Where).To(Equal(movement.ActionWhereAfter)) + Expect(moves[0].Destination.EntryName()).To(Equal("D")) + + Expect(moves[1].Movable.EntryName()).To(Equal("C")) + Expect(moves[1].Where).To(Equal(movement.ActionWhereBottom)) + Expect(moves[1].Destination).To(BeNil()) + }) + }) + }) Context("With PositionBottom used as position", func() { Context("when it needs to move one element", func() { It("should generate a single move action", func() { + // '(A E B C D) -> '(A B C D E) entries := asMovable([]string{"E"}) existing := asMovable([]string{"A", "E", "B", "C", "D"}) @@ -112,8 +108,8 @@ var _ = Describe("Movement", func() { Expect(moves).To(HaveLen(1)) Expect(moves[0].Movable.EntryName()).To(Equal("E")) - Expect(moves[0].Where).To(Equal(movement.ActionWhereAfter)) - Expect(moves[0].Destination.EntryName()).To(Equal("D")) + Expect(moves[0].Where).To(Equal(movement.ActionWhereBottom)) + Expect(moves[0].Destination).To(BeNil()) }) }) }) @@ -122,6 +118,7 @@ var _ = Describe("Movement", func() { existing := asMovable([]string{"A", "B", "C", "D", "E"}) Context("when direct position relative to the pivot is not required", func() { It("should not generate any move actions", func() { + // '(A B C D E) -> '(A B C D E) entries := asMovable([]string{"D", "E"}) moves, err := movement.MoveGroup( movement.PositionAfter{Directly: false, Pivot: Mock{"B"}}, @@ -132,8 +129,8 @@ var _ = Describe("Movement", func() { Expect(moves).To(HaveLen(0)) }) Context("and moved entries are out of order", func() { - FIt("should generate a single command to move B before D", func() { - // A B C D E -> A B C E D + It("should generate a single command to move B before D", func() { + // '(A B C D E) -> '(A B C E D) entries := asMovable([]string{"E", "D"}) moves, err := movement.MoveGroup( movement.PositionAfter{Directly: false, Pivot: Mock{"B"}}, @@ -151,7 +148,7 @@ var _ = Describe("Movement", func() { }) Context("when direct position relative to the pivot is required", func() { It("should generate required move actions", func() { - // A B C D E -> C D A B E + // '(A B C D E) -> '(C D A B E) entries := asMovable([]string{"A", "B"}) moves, err := movement.MoveGroup( movement.PositionAfter{Directly: true, Pivot: Mock{"D"}}, @@ -170,7 +167,27 @@ var _ = Describe("Movement", func() { Expect(moves[1].Destination.EntryName()).To(Equal("A")) }) }) + Context("when direct position relative to the pivot is required", func() { + It("should generate required move actions", func() { + // '(A B C D E) -> '(C D B A E) + entries := asMovable([]string{"B", "A"}) + moves, err := movement.MoveGroup( + movement.PositionAfter{Directly: true, Pivot: Mock{"D"}}, + entries, existing, + ) + + Expect(err).ToNot(HaveOccurred()) + Expect(moves).To(HaveLen(2)) + + Expect(moves[0].Movable.EntryName()).To(Equal("B")) + Expect(moves[0].Where).To(Equal(movement.ActionWhereAfter)) + Expect(moves[0].Destination.EntryName()).To(Equal("D")) + Expect(moves[1].Movable.EntryName()).To(Equal("A")) + Expect(moves[1].Where).To(Equal(movement.ActionWhereAfter)) + Expect(moves[1].Destination.EntryName()).To(Equal("B")) + }) + }) }) Context("With PositionBefore used as position", func() { existing := asMovable([]string{"A", "B", "C", "D", "E"}) @@ -178,6 +195,7 @@ var _ = Describe("Movement", func() { Context("when direct position relative to the pivot is not required", func() { Context("and moved entries are already before pivot point", func() { It("should not generate any move actions", func() { + // '(A B C D E) -> '(A B C D E) entries := asMovable([]string{"A", "B"}) moves, err := movement.MoveGroup( movement.PositionBefore{Directly: false, Pivot: Mock{"D"}}, @@ -190,7 +208,7 @@ var _ = Describe("Movement", func() { }) Context("and moved entries are out of order", func() { It("should generate a single command to move B before D", func() { - // A B C D E -> A C B D E + // '(A B C D E) -> '(A C B D E) entries := asMovable([]string{"C", "B"}) moves, err := movement.MoveGroup( movement.PositionBefore{Directly: false, Pivot: Mock{"D"}}, @@ -200,15 +218,45 @@ var _ = Describe("Movement", func() { Expect(err).ToNot(HaveOccurred()) Expect(moves).To(HaveLen(1)) - Expect(moves[0].Movable.EntryName()).To(Equal("B")) - Expect(moves[0].Where).To(Equal(movement.ActionWhereAfter)) - Expect(moves[0].Destination.EntryName()).To(Equal("C")) + Expect(moves[0].Movable.EntryName()).To(Equal("C")) + Expect(moves[0].Where).To(Equal(movement.ActionWhereBefore)) + Expect(moves[0].Destination.EntryName()).To(Equal("B")) + }) + }) + Context("and moved entries are out of order", func() { + It("should generate a single command to move B before D", func() { + // '(A B C D E) -> '(A B C D E) + entries := asMovable([]string{"A", "C"}) + moves, err := movement.MoveGroup( + movement.PositionBefore{Directly: false, Pivot: Mock{"D"}}, + entries, existing, + ) + + Expect(err).ToNot(HaveOccurred()) + Expect(moves).To(HaveLen(0)) + }) + }) + Context("and moved entries are out of order", func() { + It("should generate a single command to move B before D", func() { + // '(A B C D E) -> '(A C B D E) + entries := asMovable([]string{"A", "C", "B"}) + moves, err := movement.MoveGroup( + movement.PositionBefore{Directly: false, Pivot: Mock{"D"}}, + entries, existing, + ) + + Expect(err).ToNot(HaveOccurred()) + Expect(moves).To(HaveLen(1)) + + Expect(moves[0].Movable.EntryName()).To(Equal("C")) + Expect(moves[0].Where).To(Equal(movement.ActionWhereBefore)) + Expect(moves[0].Destination.EntryName()).To(Equal("B")) }) }) }) Context("when direct position relative to the pivot is required", func() { It("should generate required move actions", func() { - // A B C D E -> C A B D E + // '(A B C D E) -> '(C A B D E) entries := asMovable([]string{"A", "B"}) moves, err := movement.MoveGroup( movement.PositionBefore{Directly: true, Pivot: Mock{"D"}}, @@ -225,9 +273,88 @@ var _ = Describe("Movement", func() { Expect(moves[1].Movable.EntryName()).To(Equal("B")) Expect(moves[1].Where).To(Equal(movement.ActionWhereAfter)) Expect(moves[1].Destination.EntryName()).To(Equal("A")) + }) + }) + Context("when passing single Movement to MoveGroups()", func() { + existing := asMovable([]string{"A", "B", "C", "D", "E"}) + It("should return a set of move actions that describe it", func() { + // '(A B C D E) -> '(A D B C E) + entries := asMovable([]string{"B", "C"}) + moves, err := movement.MoveGroup( + movement.PositionBefore{Directly: true, Pivot: Mock{"E"}}, + entries, existing) - Expect(true).To(BeFalse()) + Expect(err).ToNot(HaveOccurred()) + Expect(moves).To(HaveLen(2)) }) }) }) }) + +var _ = Describe("MoveGroups()", Label("MoveGroups"), func() { + existing := asMovable([]string{"A", "B", "C", "D", "E"}) + Context("when passing single Movement to MoveGroups()", func() { + It("should return a set of move actions that describe it", func() { + // '(A B C D E) -> '(A D B C E) + entries := asMovable([]string{"B", "C"}) + movements := []movement.Movement{{ + Entries: entries, + Position: movement.PositionBefore{ + Directly: true, + Pivot: Mock{"E"}, + }}} + moves, err := movement.MoveGroups(existing, movements) + + Expect(err).ToNot(HaveOccurred()) + Expect(moves).To(HaveLen(2)) + }) + }) + // Context("when passing single Movement to MoveGroups()", func() { + // FIt("should return a set of move actions that describe it", func() { + // // '(A B C D E) -> '(A D B C E) -> '(D B C E A) + // movements := []movement.Movement{ + // { + // Entries: asMovable([]string{"B", "C"}), + // Position: movement.PositionBefore{ + // Directly: true, + // Pivot: Mock{"E"}}, + // }, + // { + // Entries: asMovable([]string{"A"}), + // Position: movement.PositionBottom{}, + // }, + // } + // moves, err := movement.MoveGroups(existing, movements) + + // Expect(err).ToNot(HaveOccurred()) + // Expect(moves).To(HaveLen(3)) + // }) + // }) +}) + +var _ = Describe("Movement benchmarks", func() { + BeforeEach(func() { + if !Label("benchmark").MatchesLabelFilter(GinkgoLabelFilter()) { + Skip("unless label 'benchmark' is specified.") + } + }) + Context("when moving only a few elements", func() { + It("should generate a simple sequence of actions", Label("benchmark"), func() { + var elts []string + elements := 50000 + for idx := range elements { + elts = append(elts, fmt.Sprintf("%d", idx)) + } + existing := asMovable(elts) + + entries := asMovable([]string{"90", "80", "70", "60", "50", "40"}) + moves, err := movement.MoveGroup( + movement.PositionBefore{Directly: true, Pivot: Mock{"100"}}, + entries, existing, + ) + + Expect(err).ToNot(HaveOccurred()) + Expect(moves).To(HaveLen(6)) + }) + }) +})