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::ScoreDirector;
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::SimpleScore;
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<SimpleScore> }
39///
40/// impl PlanningSolution for Solution {
41/// type Score = SimpleScore;
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: ScoreDirector<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: ScoreDirector<S>>(&self, score_director: &mut D) {
246 // Notify before changes
247 score_director.before_variable_changed(
248 self.descriptor_index,
249 self.first_entity_index,
250 self.variable_name,
251 );
252 if !self.is_intra_list() {
253 score_director.before_variable_changed(
254 self.descriptor_index,
255 self.second_entity_index,
256 self.variable_name,
257 );
258 }
259
260 if self.is_intra_list() {
261 // Intra-list swap: need to handle carefully due to index shifts
262 // Always process the later range first to avoid index shifts affecting the earlier range
263 let (early_start, early_end, late_start, late_end) =
264 if self.first_start < self.second_start {
265 (
266 self.first_start,
267 self.first_end,
268 self.second_start,
269 self.second_end,
270 )
271 } else {
272 (
273 self.second_start,
274 self.second_end,
275 self.first_start,
276 self.first_end,
277 )
278 };
279
280 // Remove later range first
281 let late_elements = (self.sublist_remove)(
282 score_director.working_solution_mut(),
283 self.first_entity_index,
284 late_start,
285 late_end,
286 );
287
288 // Remove earlier range
289 let early_elements = (self.sublist_remove)(
290 score_director.working_solution_mut(),
291 self.first_entity_index,
292 early_start,
293 early_end,
294 );
295
296 // Insert late elements at early position
297 let late_len = late_end - late_start;
298 let early_len = early_end - early_start;
299 (self.sublist_insert)(
300 score_director.working_solution_mut(),
301 self.first_entity_index,
302 early_start,
303 late_elements,
304 );
305
306 // Insert early elements at adjusted late position
307 // After removing early range, late_start shifts by early_len
308 // After inserting late elements, it shifts back by late_len
309 let new_late_pos = late_start - early_len + late_len;
310 (self.sublist_insert)(
311 score_director.working_solution_mut(),
312 self.first_entity_index,
313 new_late_pos,
314 early_elements,
315 );
316
317 // Register undo - swap back
318 let sublist_remove = self.sublist_remove;
319 let sublist_insert = self.sublist_insert;
320 let entity = self.first_entity_index;
321
322 score_director.register_undo(Box::new(move |s: &mut S| {
323 // Remove late elements (now at early position with late_len)
324 let late_at_early = sublist_remove(s, entity, early_start, early_start + late_len);
325 // Remove early elements (now at new_late_pos with early_len)
326 let early_at_late = sublist_remove(
327 s,
328 entity,
329 new_late_pos - late_len,
330 new_late_pos - late_len + early_len,
331 );
332 // Insert early back at early
333 sublist_insert(s, entity, early_start, early_at_late);
334 // Insert late back at late
335 sublist_insert(s, entity, late_start, late_at_early);
336 }));
337 } else {
338 // Inter-list swap: simpler, no index interaction between lists
339 let first_elements = (self.sublist_remove)(
340 score_director.working_solution_mut(),
341 self.first_entity_index,
342 self.first_start,
343 self.first_end,
344 );
345
346 let second_elements = (self.sublist_remove)(
347 score_director.working_solution_mut(),
348 self.second_entity_index,
349 self.second_start,
350 self.second_end,
351 );
352
353 // Insert swapped
354 (self.sublist_insert)(
355 score_director.working_solution_mut(),
356 self.first_entity_index,
357 self.first_start,
358 second_elements,
359 );
360
361 (self.sublist_insert)(
362 score_director.working_solution_mut(),
363 self.second_entity_index,
364 self.second_start,
365 first_elements,
366 );
367
368 // Register undo
369 let sublist_remove = self.sublist_remove;
370 let sublist_insert = self.sublist_insert;
371 let first_entity = self.first_entity_index;
372 let first_start = self.first_start;
373 let second_entity = self.second_entity_index;
374 let second_start = self.second_start;
375 let first_len = self.first_len();
376 let second_len = self.second_len();
377
378 score_director.register_undo(Box::new(move |s: &mut S| {
379 // Remove second elements from first list
380 let second_at_first =
381 sublist_remove(s, first_entity, first_start, first_start + second_len);
382 // Remove first elements from second list
383 let first_at_second =
384 sublist_remove(s, second_entity, second_start, second_start + first_len);
385 // Restore originals
386 sublist_insert(s, first_entity, first_start, first_at_second);
387 sublist_insert(s, second_entity, second_start, second_at_first);
388 }));
389 }
390
391 // Notify after changes
392 score_director.after_variable_changed(
393 self.descriptor_index,
394 self.first_entity_index,
395 self.variable_name,
396 );
397 if !self.is_intra_list() {
398 score_director.after_variable_changed(
399 self.descriptor_index,
400 self.second_entity_index,
401 self.variable_name,
402 );
403 }
404 }
405
406 fn descriptor_index(&self) -> usize {
407 self.descriptor_index
408 }
409
410 fn entity_indices(&self) -> &[usize] {
411 if self.is_intra_list() {
412 &self.indices[0..1]
413 } else {
414 &self.indices
415 }
416 }
417
418 fn variable_name(&self) -> &str {
419 self.variable_name
420 }
421}
422
423#[cfg(test)]
424#[path = "sublist_swap_tests.rs"]
425mod tests;