@@ -97,4 +97,56 @@ struct ColumnReorderPlannerTests {
9797 func nonPermutationProducesNoCycle( ) {
9898 #expect( PluginColumnReorderPlanner . appendCycle ( from: current, to: [ " a " , " b " ] ) . isEmpty)
9999 }
100+
101+ // MARK: - Exhaustive
102+
103+ /// Both planners are asked for every permutation of five columns. A tie in the common
104+ /// subsequence can pick a different set to leave alone without changing how many columns move,
105+ /// so the guarantee worth pinning is the result, not the choice.
106+ @Test ( " Every permutation of five columns is reached, by both mechanisms, in the minimum moves " )
107+ func everyPermutationIsReachable( ) {
108+ let start = [ " a " , " b " , " c " , " d " , " e " ]
109+ for desired in permutations ( of: start) {
110+ let moves = PluginColumnReorderPlanner . moves ( from: start, to: desired)
111+ var byMove = start
112+ for move in moves {
113+ byMove. removeAll { $0 == move. column }
114+ if let after = move. afterColumn, let index = byMove. firstIndex ( of: after) {
115+ byMove. insert ( move. column, at: byMove. index ( after: index) )
116+ } else {
117+ byMove. insert ( move. column, at: 0 )
118+ }
119+ }
120+ #expect( byMove == desired, " moves did not reach \( desired) " )
121+ #expect( moves. count == start. count - longestCommonSubsequenceLength( start, desired) )
122+
123+ var byCycle = start
124+ for column in PluginColumnReorderPlanner . appendCycle ( from: start, to: desired) {
125+ byCycle. removeAll { $0 == column }
126+ byCycle. append ( column)
127+ }
128+ #expect( byCycle == desired, " cycling did not reach \( desired) " )
129+ }
130+ }
131+
132+ private func permutations( of values: [ String ] ) -> [ [ String ] ] {
133+ guard values. count > 1 else { return [ values] }
134+ return values. indices. flatMap { index -> [ [ String ] ] in
135+ var rest = values
136+ let picked = rest. remove ( at: index)
137+ return permutations ( of: rest) . map { [ picked] + $0 }
138+ }
139+ }
140+
141+ private func longestCommonSubsequenceLength( _ lhs: [ String ] , _ rhs: [ String ] ) -> Int {
142+ var lengths = Array ( repeating: Array ( repeating: 0 , count: rhs. count + 1 ) , count: lhs. count + 1 )
143+ for i in stride ( from: lhs. count - 1 , through: 0 , by: - 1 ) {
144+ for j in stride ( from: rhs. count - 1 , through: 0 , by: - 1 ) {
145+ lengths [ i] [ j] = lhs [ i] == rhs [ j]
146+ ? lengths [ i + 1 ] [ j + 1 ] + 1
147+ : max ( lengths [ i + 1 ] [ j] , lengths [ i] [ j + 1 ] )
148+ }
149+ }
150+ return lengths [ 0 ] [ 0 ]
151+ }
100152}
0 commit comments