1use std::fmt::Debug;
11
12use solverforge_core::domain::PlanningSolution;
13use solverforge_scoring::ScoreDirector;
14
15use super::Move;
16
17pub struct ListChangeMove<S, V> {
63 source_entity_index: usize,
65 source_position: usize,
67 dest_entity_index: usize,
69 dest_position: usize,
71 list_len: fn(&S, usize) -> usize,
73 list_remove: fn(&mut S, usize, usize) -> Option<V>,
75 list_insert: fn(&mut S, usize, usize, V),
77 variable_name: &'static str,
78 descriptor_index: usize,
79 indices: [usize; 2],
81}
82
83impl<S, V> Clone for ListChangeMove<S, V> {
84 fn clone(&self) -> Self {
85 *self
86 }
87}
88
89impl<S, V> Copy for ListChangeMove<S, V> {}
90
91impl<S, V: Debug> Debug for ListChangeMove<S, V> {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 f.debug_struct("ListChangeMove")
94 .field("source_entity", &self.source_entity_index)
95 .field("source_position", &self.source_position)
96 .field("dest_entity", &self.dest_entity_index)
97 .field("dest_position", &self.dest_position)
98 .field("variable_name", &self.variable_name)
99 .finish()
100 }
101}
102
103impl<S, V> ListChangeMove<S, V> {
104 #[allow(clippy::too_many_arguments)]
117 pub fn new(
118 source_entity_index: usize,
119 source_position: usize,
120 dest_entity_index: usize,
121 dest_position: usize,
122 list_len: fn(&S, usize) -> usize,
123 list_remove: fn(&mut S, usize, usize) -> Option<V>,
124 list_insert: fn(&mut S, usize, usize, V),
125 variable_name: &'static str,
126 descriptor_index: usize,
127 ) -> Self {
128 Self {
129 source_entity_index,
130 source_position,
131 dest_entity_index,
132 dest_position,
133 list_len,
134 list_remove,
135 list_insert,
136 variable_name,
137 descriptor_index,
138 indices: [source_entity_index, dest_entity_index],
139 }
140 }
141
142 pub fn source_entity_index(&self) -> usize {
144 self.source_entity_index
145 }
146
147 pub fn source_position(&self) -> usize {
149 self.source_position
150 }
151
152 pub fn dest_entity_index(&self) -> usize {
154 self.dest_entity_index
155 }
156
157 pub fn dest_position(&self) -> usize {
159 self.dest_position
160 }
161
162 pub fn is_intra_list(&self) -> bool {
164 self.source_entity_index == self.dest_entity_index
165 }
166}
167
168impl<S, V> Move<S> for ListChangeMove<S, V>
169where
170 S: PlanningSolution,
171 V: Clone + PartialEq + Send + Sync + Debug + 'static,
172{
173 fn is_doable<D: ScoreDirector<S>>(&self, score_director: &D) -> bool {
174 let solution = score_director.working_solution();
175
176 let source_len = (self.list_len)(solution, self.source_entity_index);
178 if self.source_position >= source_len {
179 return false;
180 }
181
182 let dest_len = (self.list_len)(solution, self.dest_entity_index);
186 let max_dest = if self.is_intra_list() {
187 source_len - 1 } else {
189 dest_len
190 };
191
192 if self.dest_position > max_dest {
193 return false;
194 }
195
196 if self.is_intra_list() {
201 if self.source_position == self.dest_position {
202 return false;
203 }
204 if self.dest_position == self.source_position + 1 {
206 return false;
207 }
208 }
209
210 true
211 }
212
213 fn do_move<D: ScoreDirector<S>>(&self, score_director: &mut D) {
214 score_director.before_variable_changed(
216 self.descriptor_index,
217 self.source_entity_index,
218 self.variable_name,
219 );
220 if !self.is_intra_list() {
221 score_director.before_variable_changed(
222 self.descriptor_index,
223 self.dest_entity_index,
224 self.variable_name,
225 );
226 }
227
228 let value = (self.list_remove)(
230 score_director.working_solution_mut(),
231 self.source_entity_index,
232 self.source_position,
233 )
234 .expect("source position should be valid");
235
236 let adjusted_dest = if self.is_intra_list() && self.dest_position > self.source_position {
238 self.dest_position - 1
239 } else {
240 self.dest_position
241 };
242
243 (self.list_insert)(
245 score_director.working_solution_mut(),
246 self.dest_entity_index,
247 adjusted_dest,
248 value.clone(),
249 );
250
251 score_director.after_variable_changed(
253 self.descriptor_index,
254 self.source_entity_index,
255 self.variable_name,
256 );
257 if !self.is_intra_list() {
258 score_director.after_variable_changed(
259 self.descriptor_index,
260 self.dest_entity_index,
261 self.variable_name,
262 );
263 }
264
265 let list_remove = self.list_remove;
267 let list_insert = self.list_insert;
268 let src_entity = self.source_entity_index;
269 let src_pos = self.source_position;
270 let dest_entity = self.dest_entity_index;
271
272 score_director.register_undo(Box::new(move |s: &mut S| {
273 let removed = list_remove(s, dest_entity, adjusted_dest).unwrap();
275 list_insert(s, src_entity, src_pos, removed);
277 }));
278 }
279
280 fn descriptor_index(&self) -> usize {
281 self.descriptor_index
282 }
283
284 fn entity_indices(&self) -> &[usize] {
285 if self.is_intra_list() {
286 &self.indices[0..1]
287 } else {
288 &self.indices
289 }
290 }
291
292 fn variable_name(&self) -> &str {
293 self.variable_name
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300 use solverforge_core::domain::{EntityDescriptor, SolutionDescriptor, TypedEntityExtractor};
301 use solverforge_core::score::SimpleScore;
302 use solverforge_scoring::{RecordingScoreDirector, SimpleScoreDirector};
303 use std::any::TypeId;
304
305 #[derive(Clone, Debug)]
306 struct Vehicle {
307 visits: Vec<i32>,
308 }
309
310 #[derive(Clone, Debug)]
311 struct RoutingSolution {
312 vehicles: Vec<Vehicle>,
313 score: Option<SimpleScore>,
314 }
315
316 impl PlanningSolution for RoutingSolution {
317 type Score = SimpleScore;
318 fn score(&self) -> Option<Self::Score> {
319 self.score
320 }
321 fn set_score(&mut self, score: Option<Self::Score>) {
322 self.score = score;
323 }
324 }
325
326 fn get_vehicles(s: &RoutingSolution) -> &Vec<Vehicle> {
327 &s.vehicles
328 }
329 fn get_vehicles_mut(s: &mut RoutingSolution) -> &mut Vec<Vehicle> {
330 &mut s.vehicles
331 }
332
333 fn list_len(s: &RoutingSolution, entity_idx: usize) -> usize {
334 s.vehicles.get(entity_idx).map_or(0, |v| v.visits.len())
335 }
336 fn list_remove(s: &mut RoutingSolution, entity_idx: usize, pos: usize) -> Option<i32> {
337 s.vehicles.get_mut(entity_idx).map(|v| v.visits.remove(pos))
338 }
339 fn list_insert(s: &mut RoutingSolution, entity_idx: usize, pos: usize, val: i32) {
340 if let Some(v) = s.vehicles.get_mut(entity_idx) {
341 v.visits.insert(pos, val);
342 }
343 }
344
345 fn create_director(
346 vehicles: Vec<Vehicle>,
347 ) -> SimpleScoreDirector<RoutingSolution, impl Fn(&RoutingSolution) -> SimpleScore> {
348 let solution = RoutingSolution {
349 vehicles,
350 score: None,
351 };
352 let extractor = Box::new(TypedEntityExtractor::new(
353 "Vehicle",
354 "vehicles",
355 get_vehicles,
356 get_vehicles_mut,
357 ));
358 let entity_desc = EntityDescriptor::new("Vehicle", TypeId::of::<Vehicle>(), "vehicles")
359 .with_extractor(extractor);
360 let descriptor =
361 SolutionDescriptor::new("RoutingSolution", TypeId::of::<RoutingSolution>())
362 .with_entity(entity_desc);
363 SimpleScoreDirector::with_calculator(solution, descriptor, |_| SimpleScore::of(0))
364 }
365
366 #[test]
367 fn intra_list_move_forward() {
368 let vehicles = vec![Vehicle {
369 visits: vec![1, 2, 3, 4, 5],
370 }];
371 let mut director = create_director(vehicles);
372
373 let m = ListChangeMove::<RoutingSolution, i32>::new(
375 0,
376 1,
377 0,
378 3,
379 list_len,
380 list_remove,
381 list_insert,
382 "visits",
383 0,
384 );
385
386 assert!(m.is_doable(&director));
387
388 {
389 let mut recording = RecordingScoreDirector::new(&mut director);
390 m.do_move(&mut recording);
391
392 let visits = &recording.working_solution().vehicles[0].visits;
394 assert_eq!(visits, &[1, 3, 2, 4, 5]);
395
396 recording.undo_changes();
397 }
398
399 let visits = &director.working_solution().vehicles[0].visits;
401 assert_eq!(visits, &[1, 2, 3, 4, 5]);
402 }
403
404 #[test]
405 fn intra_list_move_backward() {
406 let vehicles = vec![Vehicle {
407 visits: vec![1, 2, 3, 4, 5],
408 }];
409 let mut director = create_director(vehicles);
410
411 let m = ListChangeMove::<RoutingSolution, i32>::new(
413 0,
414 3,
415 0,
416 1,
417 list_len,
418 list_remove,
419 list_insert,
420 "visits",
421 0,
422 );
423
424 assert!(m.is_doable(&director));
425
426 {
427 let mut recording = RecordingScoreDirector::new(&mut director);
428 m.do_move(&mut recording);
429
430 let visits = &recording.working_solution().vehicles[0].visits;
432 assert_eq!(visits, &[1, 4, 2, 3, 5]);
433
434 recording.undo_changes();
435 }
436
437 let visits = &director.working_solution().vehicles[0].visits;
438 assert_eq!(visits, &[1, 2, 3, 4, 5]);
439 }
440
441 #[test]
442 fn inter_list_move() {
443 let vehicles = vec![
444 Vehicle {
445 visits: vec![1, 2, 3],
446 },
447 Vehicle {
448 visits: vec![10, 20],
449 },
450 ];
451 let mut director = create_director(vehicles);
452
453 let m = ListChangeMove::<RoutingSolution, i32>::new(
455 0,
456 1,
457 1,
458 1,
459 list_len,
460 list_remove,
461 list_insert,
462 "visits",
463 0,
464 );
465
466 assert!(m.is_doable(&director));
467
468 {
469 let mut recording = RecordingScoreDirector::new(&mut director);
470 m.do_move(&mut recording);
471
472 let sol = recording.working_solution();
473 assert_eq!(sol.vehicles[0].visits, vec![1, 3]);
474 assert_eq!(sol.vehicles[1].visits, vec![10, 2, 20]);
475
476 recording.undo_changes();
477 }
478
479 let sol = director.working_solution();
480 assert_eq!(sol.vehicles[0].visits, vec![1, 2, 3]);
481 assert_eq!(sol.vehicles[1].visits, vec![10, 20]);
482 }
483
484 #[test]
485 fn same_position_not_doable() {
486 let vehicles = vec![Vehicle {
487 visits: vec![1, 2, 3],
488 }];
489 let director = create_director(vehicles);
490
491 let m = ListChangeMove::<RoutingSolution, i32>::new(
493 0,
494 1,
495 0,
496 1,
497 list_len,
498 list_remove,
499 list_insert,
500 "visits",
501 0,
502 );
503
504 assert!(!m.is_doable(&director));
505 }
506
507 #[test]
508 fn invalid_source_position_not_doable() {
509 let vehicles = vec![Vehicle {
510 visits: vec![1, 2, 3],
511 }];
512 let director = create_director(vehicles);
513
514 let m = ListChangeMove::<RoutingSolution, i32>::new(
515 0,
516 10,
517 0,
518 0,
519 list_len,
520 list_remove,
521 list_insert,
522 "visits",
523 0,
524 );
525
526 assert!(!m.is_doable(&director));
527 }
528}