1use std::fmt::Debug;
11use std::marker::PhantomData;
12
13use solverforge_core::domain::PlanningSolution;
14use solverforge_scoring::ScoreDirector;
15
16use super::Move;
17
18#[derive(Clone, Copy)]
64pub struct ListChangeMove<S, V> {
65 source_entity_index: usize,
67 source_position: usize,
69 dest_entity_index: usize,
71 dest_position: usize,
73 list_len: fn(&S, usize) -> usize,
75 list_remove: fn(&mut S, usize, usize) -> Option<V>,
77 list_insert: fn(&mut S, usize, usize, V),
79 variable_name: &'static str,
80 descriptor_index: usize,
81 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 #[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 pub fn source_entity_index(&self) -> usize {
140 self.source_entity_index
141 }
142
143 pub fn source_position(&self) -> usize {
145 self.source_position
146 }
147
148 pub fn dest_entity_index(&self) -> usize {
150 self.dest_entity_index
151 }
152
153 pub fn dest_position(&self) -> usize {
155 self.dest_position
156 }
157
158 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 let source_len = (self.list_len)(solution, self.source_entity_index);
174 if self.source_position >= source_len {
175 return false;
176 }
177
178 let dest_len = (self.list_len)(solution, self.dest_entity_index);
182 let max_dest = if self.is_intra_list() {
183 source_len - 1 } else {
185 dest_len
186 };
187
188 if self.dest_position > max_dest {
189 return false;
190 }
191
192 if self.is_intra_list() {
194 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 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 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 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 (self.list_insert)(
240 score_director.working_solution_mut(),
241 self.dest_entity_index,
242 adjusted_dest,
243 value.clone(),
244 );
245
246 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 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 let removed = list_remove(s, dest_entity, adjusted_dest).unwrap();
270 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 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 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 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 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 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 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 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}