solverforge_solver/heuristic/move/
pillar_change.rs

1//! PillarChangeMove - assigns a value to all entities in a pillar.
2//!
3//! A pillar is a group of entities that share the same variable value.
4//! This move changes all of them to a new value atomically.
5//!
6//! # Zero-Erasure Design
7//!
8//! PillarChangeMove uses typed function pointers instead of `dyn Any` for complete
9//! compile-time type safety. No runtime type checks or downcasting.
10
11use std::fmt::Debug;
12
13use solverforge_core::domain::PlanningSolution;
14use solverforge_scoring::ScoreDirector;
15
16use super::Move;
17
18/// A move that assigns a value to all entities in a pillar.
19///
20/// Stores entity indices and typed function pointers for zero-erasure access.
21/// Undo is handled by `RecordingScoreDirector`, not by this move.
22///
23/// # Type Parameters
24/// * `S` - The planning solution type
25/// * `V` - The variable value type
26pub struct PillarChangeMove<S, V> {
27    entity_indices: Vec<usize>,
28    descriptor_index: usize,
29    variable_name: &'static str,
30    to_value: Option<V>,
31    /// Typed getter function pointer - zero erasure.
32    getter: fn(&S, usize) -> Option<V>,
33    /// Typed setter function pointer - zero erasure.
34    setter: fn(&mut S, usize, Option<V>),
35}
36
37impl<S, V: Clone> Clone for PillarChangeMove<S, V> {
38    fn clone(&self) -> Self {
39        Self {
40            entity_indices: self.entity_indices.clone(),
41            descriptor_index: self.descriptor_index,
42            variable_name: self.variable_name,
43            to_value: self.to_value.clone(),
44            getter: self.getter,
45            setter: self.setter,
46        }
47    }
48}
49
50impl<S, V: Debug> Debug for PillarChangeMove<S, V> {
51    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52        f.debug_struct("PillarChangeMove")
53            .field("entity_indices", &self.entity_indices)
54            .field("descriptor_index", &self.descriptor_index)
55            .field("variable_name", &self.variable_name)
56            .field("to_value", &self.to_value)
57            .finish()
58    }
59}
60
61impl<S, V> PillarChangeMove<S, V> {
62    /// Creates a new pillar change move with typed function pointers.
63    ///
64    /// # Arguments
65    /// * `entity_indices` - Indices of entities in the pillar
66    /// * `to_value` - The new value to assign to all entities
67    /// * `getter` - Typed getter function pointer
68    /// * `setter` - Typed setter function pointer
69    /// * `variable_name` - Name of the variable being changed
70    /// * `descriptor_index` - Index in the entity descriptor
71    pub fn new(
72        entity_indices: Vec<usize>,
73        to_value: Option<V>,
74        getter: fn(&S, usize) -> Option<V>,
75        setter: fn(&mut S, usize, Option<V>),
76        variable_name: &'static str,
77        descriptor_index: usize,
78    ) -> Self {
79        Self {
80            entity_indices,
81            descriptor_index,
82            variable_name,
83            to_value,
84            getter,
85            setter,
86        }
87    }
88
89    /// Returns the pillar size.
90    pub fn pillar_size(&self) -> usize {
91        self.entity_indices.len()
92    }
93
94    /// Returns the target value.
95    pub fn to_value(&self) -> Option<&V> {
96        self.to_value.as_ref()
97    }
98}
99
100impl<S, V> Move<S> for PillarChangeMove<S, V>
101where
102    S: PlanningSolution,
103    V: Clone + PartialEq + Send + Sync + Debug + 'static,
104{
105    fn is_doable<D: ScoreDirector<S>>(&self, score_director: &D) -> bool {
106        if self.entity_indices.is_empty() {
107            return false;
108        }
109
110        // Check first entity exists
111        let count = score_director.entity_count(self.descriptor_index);
112        if let Some(&first_idx) = self.entity_indices.first() {
113            if count.is_none_or(|c| first_idx >= c) {
114                return false;
115            }
116
117            // Get current value using typed getter - zero erasure
118            let current = (self.getter)(score_director.working_solution(), first_idx);
119
120            match (&current, &self.to_value) {
121                (None, None) => false,
122                (Some(cur), Some(target)) => cur != target,
123                _ => true,
124            }
125        } else {
126            false
127        }
128    }
129
130    fn do_move<D: ScoreDirector<S>>(&self, score_director: &mut D) {
131        // Capture old values using typed getter - zero erasure
132        let old_values: Vec<(usize, Option<V>)> = self
133            .entity_indices
134            .iter()
135            .map(|&idx| (idx, (self.getter)(score_director.working_solution(), idx)))
136            .collect();
137
138        // Notify before changes for all entities
139        for &idx in &self.entity_indices {
140            score_director.before_variable_changed(self.descriptor_index, idx, self.variable_name);
141        }
142
143        // Apply new value to all entities using typed setter - zero erasure
144        for &idx in &self.entity_indices {
145            (self.setter)(
146                score_director.working_solution_mut(),
147                idx,
148                self.to_value.clone(),
149            );
150        }
151
152        // Notify after changes
153        for &idx in &self.entity_indices {
154            score_director.after_variable_changed(self.descriptor_index, idx, self.variable_name);
155        }
156
157        // Register typed undo closure
158        let setter = self.setter;
159        score_director.register_undo(Box::new(move |s: &mut S| {
160            for (idx, old_value) in old_values {
161                setter(s, idx, old_value);
162            }
163        }));
164    }
165
166    fn descriptor_index(&self) -> usize {
167        self.descriptor_index
168    }
169
170    fn entity_indices(&self) -> &[usize] {
171        &self.entity_indices
172    }
173
174    fn variable_name(&self) -> &str {
175        self.variable_name
176    }
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182    use solverforge_core::domain::{EntityDescriptor, SolutionDescriptor, TypedEntityExtractor};
183    use solverforge_core::score::SimpleScore;
184    use solverforge_scoring::{RecordingScoreDirector, SimpleScoreDirector};
185    use std::any::TypeId;
186
187    #[derive(Clone, Debug)]
188    struct Employee {
189        id: usize,
190        shift: Option<i32>,
191    }
192
193    #[derive(Clone, Debug)]
194    struct ScheduleSolution {
195        employees: Vec<Employee>,
196        score: Option<SimpleScore>,
197    }
198
199    impl PlanningSolution for ScheduleSolution {
200        type Score = SimpleScore;
201        fn score(&self) -> Option<Self::Score> {
202            self.score
203        }
204        fn set_score(&mut self, score: Option<Self::Score>) {
205            self.score = score;
206        }
207    }
208
209    // Typed getter - zero erasure
210    fn get_shift(s: &ScheduleSolution, idx: usize) -> Option<i32> {
211        s.employees.get(idx).and_then(|e| e.shift)
212    }
213
214    // Typed setter - zero erasure
215    fn set_shift(s: &mut ScheduleSolution, idx: usize, v: Option<i32>) {
216        if let Some(e) = s.employees.get_mut(idx) {
217            e.shift = v;
218        }
219    }
220
221    fn create_director(
222        employees: Vec<Employee>,
223    ) -> SimpleScoreDirector<ScheduleSolution, impl Fn(&ScheduleSolution) -> SimpleScore> {
224        let solution = ScheduleSolution {
225            employees,
226            score: None,
227        };
228
229        let extractor = Box::new(TypedEntityExtractor::new(
230            "Employee",
231            "employees",
232            |s: &ScheduleSolution| &s.employees,
233            |s: &mut ScheduleSolution| &mut s.employees,
234        ));
235        let entity_desc = EntityDescriptor::new("Employee", TypeId::of::<Employee>(), "employees")
236            .with_extractor(extractor);
237
238        let descriptor =
239            SolutionDescriptor::new("ScheduleSolution", TypeId::of::<ScheduleSolution>())
240                .with_entity(entity_desc);
241
242        SimpleScoreDirector::with_calculator(solution, descriptor, |_| SimpleScore::of(0))
243    }
244
245    #[test]
246    fn test_pillar_change_all_entities() {
247        let mut director = create_director(vec![
248            Employee {
249                id: 0,
250                shift: Some(1),
251            },
252            Employee {
253                id: 1,
254                shift: Some(1),
255            },
256            Employee {
257                id: 2,
258                shift: Some(2),
259            },
260        ]);
261
262        // Change pillar [0, 1] from shift 1 to shift 5
263        let m = PillarChangeMove::<ScheduleSolution, i32>::new(
264            vec![0, 1],
265            Some(5),
266            get_shift,
267            set_shift,
268            "shift",
269            0,
270        );
271
272        assert!(m.is_doable(&director));
273        assert_eq!(m.pillar_size(), 2);
274
275        {
276            let mut recording = RecordingScoreDirector::new(&mut director);
277            m.do_move(&mut recording);
278
279            // Verify ALL entities changed using typed getter
280            assert_eq!(get_shift(recording.working_solution(), 0), Some(5));
281            assert_eq!(get_shift(recording.working_solution(), 1), Some(5));
282            assert_eq!(get_shift(recording.working_solution(), 2), Some(2)); // Unchanged
283
284            // Undo
285            recording.undo_changes();
286        }
287
288        assert_eq!(get_shift(director.working_solution(), 0), Some(1));
289        assert_eq!(get_shift(director.working_solution(), 1), Some(1));
290        assert_eq!(get_shift(director.working_solution(), 2), Some(2));
291
292        // Verify entity identity preserved
293        let solution = director.working_solution();
294        assert_eq!(solution.employees[0].id, 0);
295        assert_eq!(solution.employees[1].id, 1);
296        assert_eq!(solution.employees[2].id, 2);
297    }
298
299    #[test]
300    fn test_pillar_change_same_value_not_doable() {
301        let director = create_director(vec![
302            Employee {
303                id: 0,
304                shift: Some(5),
305            },
306            Employee {
307                id: 1,
308                shift: Some(5),
309            },
310        ]);
311
312        let m = PillarChangeMove::<ScheduleSolution, i32>::new(
313            vec![0, 1],
314            Some(5),
315            get_shift,
316            set_shift,
317            "shift",
318            0,
319        );
320
321        assert!(!m.is_doable(&director));
322    }
323
324    #[test]
325    fn test_pillar_change_empty_pillar_not_doable() {
326        let director = create_director(vec![Employee {
327            id: 0,
328            shift: Some(1),
329        }]);
330
331        let m = PillarChangeMove::<ScheduleSolution, i32>::new(
332            vec![],
333            Some(5),
334            get_shift,
335            set_shift,
336            "shift",
337            0,
338        );
339
340        assert!(!m.is_doable(&director));
341    }
342
343    #[test]
344    fn test_pillar_change_entity_indices() {
345        let m = PillarChangeMove::<ScheduleSolution, i32>::new(
346            vec![1, 3, 5],
347            Some(5),
348            get_shift,
349            set_shift,
350            "shift",
351            0,
352        );
353        assert_eq!(m.entity_indices(), &[1, 3, 5]);
354    }
355}