solverforge_solver/heuristic/move/sublist_swap.rs
1//! SubListSwapMove - swaps two contiguous sublists within or between list variables.
2//!
3//! This move exchanges two ranges of elements. Essential for vehicle routing
4//! where segments need to be swapped between vehicles.
5//!
6//! # Zero-Erasure Design
7//!
8//! Uses typed function pointers for list operations. No `dyn Any`, no downcasting.
9
10use std::fmt::Debug;
11use std::marker::PhantomData;
12
13use solverforge_core::domain::PlanningSolution;
14use solverforge_scoring::Director;
15
16use super::Move;
17
18/// A move that swaps two contiguous sublists.
19///
20/// Supports both intra-list swaps (within same entity) and inter-list swaps
21/// (between different entities). Uses typed function pointers for zero-erasure.
22///
23/// # Type Parameters
24/// * `S` - The planning solution type
25/// * `V` - The list element value type
26///
27/// # Example
28///
29/// ```
30/// use solverforge_solver::heuristic::r#move::SubListSwapMove;
31/// use solverforge_core::domain::PlanningSolution;
32/// use solverforge_core::score::SoftScore;
33///
34/// #[derive(Clone, Debug)]
35/// struct Vehicle { id: usize, visits: Vec<i32> }
36///
37/// #[derive(Clone, Debug)]
38/// struct Solution { vehicles: Vec<Vehicle>, score: Option<SoftScore> }
39///
40/// impl PlanningSolution for Solution {
41/// type Score = SoftScore;
42/// fn score(&self) -> Option<Self::Score> { self.score }
43/// fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
44/// }
45///
46/// fn list_len(s: &Solution, entity_idx: usize) -> usize {
47/// s.vehicles.get(entity_idx).map_or(0, |v| v.visits.len())
48/// }
49/// fn sublist_remove(s: &mut Solution, entity_idx: usize, start: usize, end: usize) -> Vec<i32> {
50/// s.vehicles.get_mut(entity_idx)
51/// .map(|v| v.visits.drain(start..end).collect())
52/// .unwrap_or_default()
53/// }
54/// fn sublist_insert(s: &mut Solution, entity_idx: usize, pos: usize, items: Vec<i32>) {
55/// if let Some(v) = s.vehicles.get_mut(entity_idx) {
56/// for (i, item) in items.into_iter().enumerate() {
57/// v.visits.insert(pos + i, item);
58/// }
59/// }
60/// }
61///
62/// // Swap [1..3) from vehicle 0 with [0..2) from vehicle 1
63/// let m = SubListSwapMove::<Solution, i32>::new(
64/// 0, 1, 3, // first: entity 0, range [1, 3)
65/// 1, 0, 2, // second: entity 1, range [0, 2)
66/// list_len, sublist_remove, sublist_insert,
67/// "visits", 0,
68/// );
69/// ```
70pub struct SubListSwapMove<S, V> {
71 /// First entity index
72 first_entity_index: usize,
73 /// Start of first range (inclusive)
74 first_start: usize,
75 /// End of first range (exclusive)
76 first_end: usize,
77 /// Second entity index
78 second_entity_index: usize,
79 /// Start of second range (inclusive)
80 second_start: usize,
81 /// End of second range (exclusive)
82 second_end: usize,
83 /// Get list length for an entity
84 list_len: fn(&S, usize) -> usize,
85 /// Remove sublist [start, end), returns removed elements
86 sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
87 /// Insert elements at position
88 sublist_insert: fn(&mut S, usize, usize, Vec<V>),
89 variable_name: &'static str,
90 descriptor_index: usize,
91 /// Store indices for entity_indices()
92 indices: [usize; 2],
93 _phantom: PhantomData<fn() -> V>,
94}
95
96impl<S, V> Clone for SubListSwapMove<S, V> {
97 fn clone(&self) -> Self {
98 *self
99 }
100}
101
102impl<S, V> Copy for SubListSwapMove<S, V> {}
103
104impl<S, V: Debug> Debug for SubListSwapMove<S, V> {
105 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106 f.debug_struct("SubListSwapMove")
107 .field("first_entity", &self.first_entity_index)
108 .field("first_range", &(self.first_start..self.first_end))
109 .field("second_entity", &self.second_entity_index)
110 .field("second_range", &(self.second_start..self.second_end))
111 .field("variable_name", &self.variable_name)
112 .finish()
113 }
114}
115
116impl<S, V> SubListSwapMove<S, V> {
117 /// Creates a new sublist swap move with typed function pointers.
118 ///
119 /// # Arguments
120 /// * `first_entity_index` - First entity index
121 /// * `first_start` - Start of first range (inclusive)
122 /// * `first_end` - End of first range (exclusive)
123 /// * `second_entity_index` - Second entity index
124 /// * `second_start` - Start of second range (inclusive)
125 /// * `second_end` - End of second range (exclusive)
126 /// * `list_len` - Function to get list length
127 /// * `sublist_remove` - Function to remove range [start, end)
128 /// * `sublist_insert` - Function to insert elements at position
129 /// * `variable_name` - Name of the list variable
130 /// * `descriptor_index` - Entity descriptor index
131 #[allow(clippy::too_many_arguments)]
132 pub fn new(
133 first_entity_index: usize,
134 first_start: usize,
135 first_end: usize,
136 second_entity_index: usize,
137 second_start: usize,
138 second_end: usize,
139 list_len: fn(&S, usize) -> usize,
140 sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
141 sublist_insert: fn(&mut S, usize, usize, Vec<V>),
142 variable_name: &'static str,
143 descriptor_index: usize,
144 ) -> Self {
145 Self {
146 first_entity_index,
147 first_start,
148 first_end,
149 second_entity_index,
150 second_start,
151 second_end,
152 list_len,
153 sublist_remove,
154 sublist_insert,
155 variable_name,
156 descriptor_index,
157 indices: [first_entity_index, second_entity_index],
158 _phantom: PhantomData,
159 }
160 }
161
162 /// Returns the first entity index.
163 pub fn first_entity_index(&self) -> usize {
164 self.first_entity_index
165 }
166
167 /// Returns the first range start.
168 pub fn first_start(&self) -> usize {
169 self.first_start
170 }
171
172 /// Returns the first range end.
173 pub fn first_end(&self) -> usize {
174 self.first_end
175 }
176
177 /// Returns the first sublist length.
178 pub fn first_len(&self) -> usize {
179 self.first_end.saturating_sub(self.first_start)
180 }
181
182 /// Returns the second entity index.
183 pub fn second_entity_index(&self) -> usize {
184 self.second_entity_index
185 }
186
187 /// Returns the second range start.
188 pub fn second_start(&self) -> usize {
189 self.second_start
190 }
191
192 /// Returns the second range end.
193 pub fn second_end(&self) -> usize {
194 self.second_end
195 }
196
197 /// Returns the second sublist length.
198 pub fn second_len(&self) -> usize {
199 self.second_end.saturating_sub(self.second_start)
200 }
201
202 /// Returns true if this is an intra-list swap (same entity).
203 pub fn is_intra_list(&self) -> bool {
204 self.first_entity_index == self.second_entity_index
205 }
206}
207
208impl<S, V> Move<S> for SubListSwapMove<S, V>
209where
210 S: PlanningSolution,
211 V: Clone + Send + Sync + Debug + 'static,
212{
213 fn is_doable<D: Director<S>>(&self, score_director: &D) -> bool {
214 let solution = score_director.working_solution();
215
216 // Both ranges must be valid (start < end)
217 if self.first_start >= self.first_end || self.second_start >= self.second_end {
218 return false;
219 }
220
221 // Check first range is within bounds
222 let first_list_len = (self.list_len)(solution, self.first_entity_index);
223 if self.first_end > first_list_len {
224 return false;
225 }
226
227 // Check second range is within bounds
228 let second_list_len = (self.list_len)(solution, self.second_entity_index);
229 if self.second_end > second_list_len {
230 return false;
231 }
232
233 // For intra-list swaps, ranges must not overlap
234 if self.is_intra_list() {
235 // Ranges overlap if one starts before the other ends
236 let overlaps = self.first_start < self.second_end && self.second_start < self.first_end;
237 if overlaps {
238 return false;
239 }
240 }
241
242 true
243 }
244
245 fn do_move<D: Director<S>>(&self, score_director: &mut D) {
246 // Notify before changes
247 score_director.before_variable_changed(self.descriptor_index, self.first_entity_index);
248 if !self.is_intra_list() {
249 score_director.before_variable_changed(self.descriptor_index, self.second_entity_index);
250 }
251
252 if self.is_intra_list() {
253 // Intra-list swap: need to handle carefully due to index shifts
254 // Always process the later range first to avoid index shifts affecting the earlier range
255 let (early_start, early_end, late_start, late_end) =
256 if self.first_start < self.second_start {
257 (
258 self.first_start,
259 self.first_end,
260 self.second_start,
261 self.second_end,
262 )
263 } else {
264 (
265 self.second_start,
266 self.second_end,
267 self.first_start,
268 self.first_end,
269 )
270 };
271
272 // Remove later range first
273 let late_elements = (self.sublist_remove)(
274 score_director.working_solution_mut(),
275 self.first_entity_index,
276 late_start,
277 late_end,
278 );
279
280 // Remove earlier range
281 let early_elements = (self.sublist_remove)(
282 score_director.working_solution_mut(),
283 self.first_entity_index,
284 early_start,
285 early_end,
286 );
287
288 // Insert late elements at early position
289 let late_len = late_end - late_start;
290 let early_len = early_end - early_start;
291 (self.sublist_insert)(
292 score_director.working_solution_mut(),
293 self.first_entity_index,
294 early_start,
295 late_elements,
296 );
297
298 // Insert early elements at adjusted late position
299 // After removing early range, late_start shifts by early_len
300 // After inserting late elements, it shifts back by late_len
301 let new_late_pos = late_start - early_len + late_len;
302 (self.sublist_insert)(
303 score_director.working_solution_mut(),
304 self.first_entity_index,
305 new_late_pos,
306 early_elements,
307 );
308
309 // Register undo - swap back
310 let sublist_remove = self.sublist_remove;
311 let sublist_insert = self.sublist_insert;
312 let entity = self.first_entity_index;
313
314 score_director.register_undo(Box::new(move |s: &mut S| {
315 // Remove late elements (now at early position with late_len)
316 let late_at_early = sublist_remove(s, entity, early_start, early_start + late_len);
317 // Remove early elements (now at new_late_pos with early_len)
318 let early_at_late = sublist_remove(
319 s,
320 entity,
321 new_late_pos - late_len,
322 new_late_pos - late_len + early_len,
323 );
324 // Insert early back at early
325 sublist_insert(s, entity, early_start, early_at_late);
326 // Insert late back at late
327 sublist_insert(s, entity, late_start, late_at_early);
328 }));
329 } else {
330 // Inter-list swap: simpler, no index interaction between lists
331 let first_elements = (self.sublist_remove)(
332 score_director.working_solution_mut(),
333 self.first_entity_index,
334 self.first_start,
335 self.first_end,
336 );
337
338 let second_elements = (self.sublist_remove)(
339 score_director.working_solution_mut(),
340 self.second_entity_index,
341 self.second_start,
342 self.second_end,
343 );
344
345 // Insert swapped
346 (self.sublist_insert)(
347 score_director.working_solution_mut(),
348 self.first_entity_index,
349 self.first_start,
350 second_elements,
351 );
352
353 (self.sublist_insert)(
354 score_director.working_solution_mut(),
355 self.second_entity_index,
356 self.second_start,
357 first_elements,
358 );
359
360 // Register undo
361 let sublist_remove = self.sublist_remove;
362 let sublist_insert = self.sublist_insert;
363 let first_entity = self.first_entity_index;
364 let first_start = self.first_start;
365 let second_entity = self.second_entity_index;
366 let second_start = self.second_start;
367 let first_len = self.first_len();
368 let second_len = self.second_len();
369
370 score_director.register_undo(Box::new(move |s: &mut S| {
371 // Remove second elements from first list
372 let second_at_first =
373 sublist_remove(s, first_entity, first_start, first_start + second_len);
374 // Remove first elements from second list
375 let first_at_second =
376 sublist_remove(s, second_entity, second_start, second_start + first_len);
377 // Restore originals
378 sublist_insert(s, first_entity, first_start, first_at_second);
379 sublist_insert(s, second_entity, second_start, second_at_first);
380 }));
381 }
382
383 // Notify after changes
384 score_director.after_variable_changed(self.descriptor_index, self.first_entity_index);
385 if !self.is_intra_list() {
386 score_director.after_variable_changed(self.descriptor_index, self.second_entity_index);
387 }
388 }
389
390 fn descriptor_index(&self) -> usize {
391 self.descriptor_index
392 }
393
394 fn entity_indices(&self) -> &[usize] {
395 if self.is_intra_list() {
396 &self.indices[0..1]
397 } else {
398 &self.indices
399 }
400 }
401
402 fn variable_name(&self) -> &str {
403 self.variable_name
404 }
405}
406
407#[cfg(test)]
408#[path = "sublist_swap_tests.rs"]
409mod tests;