Skip to main content

solverforge_solver/heuristic/move/
list_change.rs

1/* ListChangeMove - relocates an element within or between list variables.
2
3This move removes an element from one position and inserts it at another.
4Essential for vehicle routing and scheduling problems.
5
6# Zero-Erasure Design
7
8Uses typed function pointers for list operations. No `dyn Any`, no downcasting.
9*/
10
11use std::fmt::Debug;
12
13use solverforge_core::domain::PlanningSolution;
14use solverforge_scoring::Director;
15
16use super::Move;
17
18/// A move that relocates an element from one list position to another.
19///
20/// Supports both intra-list moves (within same entity) and inter-list moves
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::ListChangeMove;
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 list_remove(s: &mut Solution, entity_idx: usize, pos: usize) -> Option<i32> {
50///     s.vehicles.get_mut(entity_idx).map(|v| v.visits.remove(pos))
51/// }
52/// fn list_insert(s: &mut Solution, entity_idx: usize, pos: usize, val: i32) {
53///     if let Some(v) = s.vehicles.get_mut(entity_idx) { v.visits.insert(pos, val); }
54/// }
55///
56/// // Move element from vehicle 0 position 2 to vehicle 1 position 0
57/// let m = ListChangeMove::<Solution, i32>::new(
58///     0, 2, 1, 0,
59///     list_len, list_remove, list_insert,
60///     "visits", 0,
61/// );
62/// ```
63pub struct ListChangeMove<S, V> {
64    // Source entity index (which entity's list to remove from)
65    source_entity_index: usize,
66    // Position in source list to remove from
67    source_position: usize,
68    // Destination entity index (which entity's list to insert into)
69    dest_entity_index: usize,
70    // Position in destination list to insert at
71    dest_position: usize,
72    list_len: fn(&S, usize) -> usize,
73    // Remove element at position, returns removed value
74    list_remove: fn(&mut S, usize, usize) -> Option<V>,
75    // Insert element at position
76    list_insert: fn(&mut S, usize, usize, V),
77    variable_name: &'static str,
78    descriptor_index: usize,
79    // Store indices for entity_indices()
80    indices: [usize; 2],
81}
82
83impl<S, V> Clone for ListChangeMove<S, V> {
84    fn clone(&self) -> Self {
85        *self
86    }
87}
88
89impl<S, V> Copy for ListChangeMove<S, V> {}
90
91impl<S, V: Debug> Debug for ListChangeMove<S, V> {
92    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93        f.debug_struct("ListChangeMove")
94            .field("source_entity", &self.source_entity_index)
95            .field("source_position", &self.source_position)
96            .field("dest_entity", &self.dest_entity_index)
97            .field("dest_position", &self.dest_position)
98            .field("variable_name", &self.variable_name)
99            .finish()
100    }
101}
102
103impl<S, V> ListChangeMove<S, V> {
104    /* Creates a new list change move with typed function pointers.
105
106    # Arguments
107    * `source_entity_index` - Entity index to remove from
108    * `source_position` - Position in source list
109    * `dest_entity_index` - Entity index to insert into
110    * `dest_position` - Position in destination list
111    * `list_len` - Function to get list length
112    * `list_remove` - Function to remove element at position
113    * `list_insert` - Function to insert element at position
114    * `variable_name` - Name of the list variable
115    * `descriptor_index` - Entity descriptor index
116    */
117    #[allow(clippy::too_many_arguments)]
118    pub fn new(
119        source_entity_index: usize,
120        source_position: usize,
121        dest_entity_index: usize,
122        dest_position: usize,
123        list_len: fn(&S, usize) -> usize,
124        list_remove: fn(&mut S, usize, usize) -> Option<V>,
125        list_insert: fn(&mut S, usize, usize, V),
126        variable_name: &'static str,
127        descriptor_index: usize,
128    ) -> Self {
129        Self {
130            source_entity_index,
131            source_position,
132            dest_entity_index,
133            dest_position,
134            list_len,
135            list_remove,
136            list_insert,
137            variable_name,
138            descriptor_index,
139            indices: [source_entity_index, dest_entity_index],
140        }
141    }
142
143    pub fn source_entity_index(&self) -> usize {
144        self.source_entity_index
145    }
146
147    pub fn source_position(&self) -> usize {
148        self.source_position
149    }
150
151    pub fn dest_entity_index(&self) -> usize {
152        self.dest_entity_index
153    }
154
155    pub fn dest_position(&self) -> usize {
156        self.dest_position
157    }
158
159    pub fn is_intra_list(&self) -> bool {
160        self.source_entity_index == self.dest_entity_index
161    }
162}
163
164impl<S, V> Move<S> for ListChangeMove<S, V>
165where
166    S: PlanningSolution,
167    V: Clone + PartialEq + Send + Sync + Debug + 'static,
168{
169    fn is_doable<D: Director<S>>(&self, score_director: &D) -> bool {
170        let solution = score_director.working_solution();
171
172        // Check source position is valid
173        let source_len = (self.list_len)(solution, self.source_entity_index);
174        if self.source_position >= source_len {
175            return false;
176        }
177
178        /* Check destination position is valid
179        For intra-list, dest can be 0..=len-1 (after removal)
180        For inter-list, dest can be 0..=len
181        */
182        let dest_len = (self.list_len)(solution, self.dest_entity_index);
183        let max_dest = if self.is_intra_list() {
184            source_len - 1 // After removal, list is shorter
185        } else {
186            dest_len
187        };
188
189        if self.dest_position > max_dest {
190            return false;
191        }
192
193        /* For intra-list moves, check for no-ops
194        Moving to same position is obviously a no-op
195        Moving forward by 1 position is also a no-op due to index adjustment:
196        remove at source, adjusted_dest = dest-1 = source, insert at source → same list
197        */
198        if self.is_intra_list() {
199            if self.source_position == self.dest_position {
200                return false;
201            }
202            // Forward move by exactly 1 is a no-op
203            if self.dest_position == self.source_position + 1 {
204                return false;
205            }
206        }
207
208        true
209    }
210
211    fn do_move<D: Director<S>>(&self, score_director: &mut D) {
212        // Notify before changes
213        score_director.before_variable_changed(self.descriptor_index, self.source_entity_index);
214        if !self.is_intra_list() {
215            score_director.before_variable_changed(self.descriptor_index, self.dest_entity_index);
216        }
217
218        // Remove from source
219        let value = (self.list_remove)(
220            score_director.working_solution_mut(),
221            self.source_entity_index,
222            self.source_position,
223        )
224        .expect("source position should be valid");
225
226        // For intra-list moves, adjust dest position if it was after source
227        let adjusted_dest = if self.is_intra_list() && self.dest_position > self.source_position {
228            self.dest_position - 1
229        } else {
230            self.dest_position
231        };
232
233        // Insert at destination
234        (self.list_insert)(
235            score_director.working_solution_mut(),
236            self.dest_entity_index,
237            adjusted_dest,
238            value.clone(),
239        );
240
241        // Notify after changes
242        score_director.after_variable_changed(self.descriptor_index, self.source_entity_index);
243        if !self.is_intra_list() {
244            score_director.after_variable_changed(self.descriptor_index, self.dest_entity_index);
245        }
246
247        // Register undo - reverse the operation
248        let list_remove = self.list_remove;
249        let list_insert = self.list_insert;
250        let src_entity = self.source_entity_index;
251        let src_pos = self.source_position;
252        let dest_entity = self.dest_entity_index;
253
254        score_director.register_undo(Box::new(move |s: &mut S| {
255            // Remove from where we inserted
256            let removed = list_remove(s, dest_entity, adjusted_dest).unwrap();
257            // Insert back at original source position
258            list_insert(s, src_entity, src_pos, removed);
259        }));
260    }
261
262    fn descriptor_index(&self) -> usize {
263        self.descriptor_index
264    }
265
266    fn entity_indices(&self) -> &[usize] {
267        if self.is_intra_list() {
268            &self.indices[0..1]
269        } else {
270            &self.indices
271        }
272    }
273
274    fn variable_name(&self) -> &str {
275        self.variable_name
276    }
277}
278
279#[cfg(test)]
280#[path = "list_change_tests.rs"]
281mod tests;