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