solverforge_solver/heuristic/move/
list_change.rs

1//! ListChangeMove - relocates an element within or between list variables.
2//!
3//! This move removes an element from one position and inserts it at another.
4//! Essential for vehicle routing and scheduling problems.
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 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::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 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/// ```
63#[derive(Clone, Copy)]
64pub struct ListChangeMove<S, V> {
65    /// Source entity index (which entity's list to remove from)
66    source_entity_index: usize,
67    /// Position in source list to remove from
68    source_position: usize,
69    /// Destination entity index (which entity's list to insert into)
70    dest_entity_index: usize,
71    /// Position in destination list to insert at
72    dest_position: usize,
73    /// Get list length for an entity
74    list_len: fn(&S, usize) -> usize,
75    /// Remove element at position, returns removed value
76    list_remove: fn(&mut S, usize, usize) -> Option<V>,
77    /// Insert element at position
78    list_insert: fn(&mut S, usize, usize, V),
79    variable_name: &'static str,
80    descriptor_index: usize,
81    /// Store indices for entity_indices()
82    indices: [usize; 2],
83    _phantom: PhantomData<V>,
84}
85
86impl<S, V: Debug> Debug for ListChangeMove<S, V> {
87    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88        f.debug_struct("ListChangeMove")
89            .field("source_entity", &self.source_entity_index)
90            .field("source_position", &self.source_position)
91            .field("dest_entity", &self.dest_entity_index)
92            .field("dest_position", &self.dest_position)
93            .field("variable_name", &self.variable_name)
94            .finish()
95    }
96}
97
98impl<S, V> ListChangeMove<S, V> {
99    /// Creates a new list change move with typed function pointers.
100    ///
101    /// # Arguments
102    /// * `source_entity_index` - Entity index to remove from
103    /// * `source_position` - Position in source list
104    /// * `dest_entity_index` - Entity index to insert into
105    /// * `dest_position` - Position in destination list
106    /// * `list_len` - Function to get list length
107    /// * `list_remove` - Function to remove element at position
108    /// * `list_insert` - Function to insert element at position
109    /// * `variable_name` - Name of the list variable
110    /// * `descriptor_index` - Entity descriptor index
111    #[allow(clippy::too_many_arguments)]
112    pub fn new(
113        source_entity_index: usize,
114        source_position: usize,
115        dest_entity_index: usize,
116        dest_position: usize,
117        list_len: fn(&S, usize) -> usize,
118        list_remove: fn(&mut S, usize, usize) -> Option<V>,
119        list_insert: fn(&mut S, usize, usize, V),
120        variable_name: &'static str,
121        descriptor_index: usize,
122    ) -> Self {
123        Self {
124            source_entity_index,
125            source_position,
126            dest_entity_index,
127            dest_position,
128            list_len,
129            list_remove,
130            list_insert,
131            variable_name,
132            descriptor_index,
133            indices: [source_entity_index, dest_entity_index],
134            _phantom: PhantomData,
135        }
136    }
137
138    /// Returns the source entity index.
139    pub fn source_entity_index(&self) -> usize {
140        self.source_entity_index
141    }
142
143    /// Returns the source position.
144    pub fn source_position(&self) -> usize {
145        self.source_position
146    }
147
148    /// Returns the destination entity index.
149    pub fn dest_entity_index(&self) -> usize {
150        self.dest_entity_index
151    }
152
153    /// Returns the destination position.
154    pub fn dest_position(&self) -> usize {
155        self.dest_position
156    }
157
158    /// Returns true if this is an intra-list move (same entity).
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(&self, score_director: &dyn ScoreDirector<S>) -> 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        let dest_len = (self.list_len)(solution, self.dest_entity_index);
182        let max_dest = if self.is_intra_list() {
183            source_len - 1 // After removal, list is shorter
184        } else {
185            dest_len
186        };
187
188        if self.dest_position > max_dest {
189            return false;
190        }
191
192        // For intra-list, moving to same position is a no-op
193        if self.is_intra_list() {
194            // Adjust for removal: if dest > source, effective position after removal
195            let effective_dest = if self.dest_position > self.source_position {
196                self.dest_position - 1
197            } else {
198                self.dest_position
199            };
200            if self.source_position == effective_dest {
201                return false;
202            }
203        }
204
205        true
206    }
207
208    fn do_move(&self, score_director: &mut dyn ScoreDirector<S>) {
209        // Notify before changes
210        score_director.before_variable_changed(
211            self.descriptor_index,
212            self.source_entity_index,
213            self.variable_name,
214        );
215        if !self.is_intra_list() {
216            score_director.before_variable_changed(
217                self.descriptor_index,
218                self.dest_entity_index,
219                self.variable_name,
220            );
221        }
222
223        // Remove from source
224        let value = (self.list_remove)(
225            score_director.working_solution_mut(),
226            self.source_entity_index,
227            self.source_position,
228        )
229        .expect("source position should be valid");
230
231        // For intra-list moves, adjust dest position if it was after source
232        let adjusted_dest = if self.is_intra_list() && self.dest_position > self.source_position {
233            self.dest_position - 1
234        } else {
235            self.dest_position
236        };
237
238        // Insert at destination
239        (self.list_insert)(
240            score_director.working_solution_mut(),
241            self.dest_entity_index,
242            adjusted_dest,
243            value.clone(),
244        );
245
246        // Notify after changes
247        score_director.after_variable_changed(
248            self.descriptor_index,
249            self.source_entity_index,
250            self.variable_name,
251        );
252        if !self.is_intra_list() {
253            score_director.after_variable_changed(
254                self.descriptor_index,
255                self.dest_entity_index,
256                self.variable_name,
257            );
258        }
259
260        // Register undo - reverse the operation
261        let list_remove = self.list_remove;
262        let list_insert = self.list_insert;
263        let src_entity = self.source_entity_index;
264        let src_pos = self.source_position;
265        let dest_entity = self.dest_entity_index;
266
267        score_director.register_undo(Box::new(move |s: &mut S| {
268            // Remove from where we inserted
269            let removed = list_remove(s, dest_entity, adjusted_dest).unwrap();
270            // Insert back at original source position
271            list_insert(s, src_entity, src_pos, removed);
272        }));
273    }
274
275    fn descriptor_index(&self) -> usize {
276        self.descriptor_index
277    }
278
279    fn entity_indices(&self) -> &[usize] {
280        if self.is_intra_list() {
281            &self.indices[0..1]
282        } else {
283            &self.indices
284        }
285    }
286
287    fn variable_name(&self) -> &str {
288        self.variable_name
289    }
290}
291
292#[cfg(test)]
293mod tests {
294    use super::*;
295    use solverforge_core::domain::{EntityDescriptor, SolutionDescriptor, TypedEntityExtractor};
296    use solverforge_core::score::SimpleScore;
297    use solverforge_scoring::{RecordingScoreDirector, SimpleScoreDirector};
298    use std::any::TypeId;
299
300    #[derive(Clone, Debug)]
301    struct Vehicle {
302        visits: Vec<i32>,
303    }
304
305    #[derive(Clone, Debug)]
306    struct RoutingSolution {
307        vehicles: Vec<Vehicle>,
308        score: Option<SimpleScore>,
309    }
310
311    impl PlanningSolution for RoutingSolution {
312        type Score = SimpleScore;
313        fn score(&self) -> Option<Self::Score> {
314            self.score
315        }
316        fn set_score(&mut self, score: Option<Self::Score>) {
317            self.score = score;
318        }
319    }
320
321    fn get_vehicles(s: &RoutingSolution) -> &Vec<Vehicle> {
322        &s.vehicles
323    }
324    fn get_vehicles_mut(s: &mut RoutingSolution) -> &mut Vec<Vehicle> {
325        &mut s.vehicles
326    }
327
328    fn list_len(s: &RoutingSolution, entity_idx: usize) -> usize {
329        s.vehicles.get(entity_idx).map_or(0, |v| v.visits.len())
330    }
331    fn list_remove(s: &mut RoutingSolution, entity_idx: usize, pos: usize) -> Option<i32> {
332        s.vehicles.get_mut(entity_idx).map(|v| v.visits.remove(pos))
333    }
334    fn list_insert(s: &mut RoutingSolution, entity_idx: usize, pos: usize, val: i32) {
335        if let Some(v) = s.vehicles.get_mut(entity_idx) {
336            v.visits.insert(pos, val);
337        }
338    }
339
340    fn create_director(
341        vehicles: Vec<Vehicle>,
342    ) -> SimpleScoreDirector<RoutingSolution, impl Fn(&RoutingSolution) -> SimpleScore> {
343        let solution = RoutingSolution {
344            vehicles,
345            score: None,
346        };
347        let extractor = Box::new(TypedEntityExtractor::new(
348            "Vehicle",
349            "vehicles",
350            get_vehicles,
351            get_vehicles_mut,
352        ));
353        let entity_desc = EntityDescriptor::new("Vehicle", TypeId::of::<Vehicle>(), "vehicles")
354            .with_extractor(extractor);
355        let descriptor =
356            SolutionDescriptor::new("RoutingSolution", TypeId::of::<RoutingSolution>())
357                .with_entity(entity_desc);
358        SimpleScoreDirector::with_calculator(solution, descriptor, |_| SimpleScore::of(0))
359    }
360
361    #[test]
362    fn intra_list_move_forward() {
363        let vehicles = vec![Vehicle {
364            visits: vec![1, 2, 3, 4, 5],
365        }];
366        let mut director = create_director(vehicles);
367
368        // Move element at position 1 (value=2) to position 3
369        let m = ListChangeMove::<RoutingSolution, i32>::new(
370            0,
371            1,
372            0,
373            3,
374            list_len,
375            list_remove,
376            list_insert,
377            "visits",
378            0,
379        );
380
381        assert!(m.is_doable(&director));
382
383        {
384            let mut recording = RecordingScoreDirector::new(&mut director);
385            m.do_move(&mut recording);
386
387            // After: [1, 3, 2, 4, 5] (2 moved to after 3)
388            let visits = &recording.working_solution().vehicles[0].visits;
389            assert_eq!(visits, &[1, 3, 2, 4, 5]);
390
391            recording.undo_changes();
392        }
393
394        // Verify restored
395        let visits = &director.working_solution().vehicles[0].visits;
396        assert_eq!(visits, &[1, 2, 3, 4, 5]);
397    }
398
399    #[test]
400    fn intra_list_move_backward() {
401        let vehicles = vec![Vehicle {
402            visits: vec![1, 2, 3, 4, 5],
403        }];
404        let mut director = create_director(vehicles);
405
406        // Move element at position 3 (value=4) to position 1
407        let m = ListChangeMove::<RoutingSolution, i32>::new(
408            0,
409            3,
410            0,
411            1,
412            list_len,
413            list_remove,
414            list_insert,
415            "visits",
416            0,
417        );
418
419        assert!(m.is_doable(&director));
420
421        {
422            let mut recording = RecordingScoreDirector::new(&mut director);
423            m.do_move(&mut recording);
424
425            // After: [1, 4, 2, 3, 5]
426            let visits = &recording.working_solution().vehicles[0].visits;
427            assert_eq!(visits, &[1, 4, 2, 3, 5]);
428
429            recording.undo_changes();
430        }
431
432        let visits = &director.working_solution().vehicles[0].visits;
433        assert_eq!(visits, &[1, 2, 3, 4, 5]);
434    }
435
436    #[test]
437    fn inter_list_move() {
438        let vehicles = vec![
439            Vehicle {
440                visits: vec![1, 2, 3],
441            },
442            Vehicle {
443                visits: vec![10, 20],
444            },
445        ];
446        let mut director = create_director(vehicles);
447
448        // Move element from vehicle 0 position 1 (value=2) to vehicle 1 position 1
449        let m = ListChangeMove::<RoutingSolution, i32>::new(
450            0,
451            1,
452            1,
453            1,
454            list_len,
455            list_remove,
456            list_insert,
457            "visits",
458            0,
459        );
460
461        assert!(m.is_doable(&director));
462
463        {
464            let mut recording = RecordingScoreDirector::new(&mut director);
465            m.do_move(&mut recording);
466
467            let sol = recording.working_solution();
468            assert_eq!(sol.vehicles[0].visits, vec![1, 3]);
469            assert_eq!(sol.vehicles[1].visits, vec![10, 2, 20]);
470
471            recording.undo_changes();
472        }
473
474        let sol = director.working_solution();
475        assert_eq!(sol.vehicles[0].visits, vec![1, 2, 3]);
476        assert_eq!(sol.vehicles[1].visits, vec![10, 20]);
477    }
478
479    #[test]
480    fn same_position_not_doable() {
481        let vehicles = vec![Vehicle {
482            visits: vec![1, 2, 3],
483        }];
484        let director = create_director(vehicles);
485
486        // Move to same effective position
487        let m = ListChangeMove::<RoutingSolution, i32>::new(
488            0,
489            1,
490            0,
491            1,
492            list_len,
493            list_remove,
494            list_insert,
495            "visits",
496            0,
497        );
498
499        assert!(!m.is_doable(&director));
500    }
501
502    #[test]
503    fn invalid_source_position_not_doable() {
504        let vehicles = vec![Vehicle {
505            visits: vec![1, 2, 3],
506        }];
507        let director = create_director(vehicles);
508
509        let m = ListChangeMove::<RoutingSolution, i32>::new(
510            0,
511            10,
512            0,
513            0,
514            list_len,
515            list_remove,
516            list_insert,
517            "visits",
518            0,
519        );
520
521        assert!(!m.is_doable(&director));
522    }
523}