Skip to main content

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;