solverforge_solver/heuristic/move/
pillar_change.rs1use std::fmt::Debug;
12use std::marker::PhantomData;
13
14use solverforge_core::domain::PlanningSolution;
15use solverforge_scoring::ScoreDirector;
16
17use super::Move;
18
19#[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 getter: fn(&S, usize) -> Option<V>,
35 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 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 pub fn pillar_size(&self) -> usize {
82 self.entity_indices.len()
83 }
84
85 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 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 let current = (self.getter)(score_director.working_solution(), first_idx);
110
111 match (¤t, &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 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 for &idx in &self.entity_indices {
131 score_director.before_variable_changed(self.descriptor_index, idx, self.variable_name);
132 }
133
134 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 for &idx in &self.entity_indices {
145 score_director.after_variable_changed(self.descriptor_index, idx, self.variable_name);
146 }
147
148 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 fn get_shift(s: &ScheduleSolution, idx: usize) -> Option<i32> {
202 s.employees.get(idx).and_then(|e| e.shift)
203 }
204
205 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 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 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)); 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 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}