solverforge_solver/heuristic/move/sublist_change.rs
1//! SubListChangeMove - relocates a contiguous sublist within or between list variables.
2//!
3//! This move removes a range of elements from one position and inserts them at another.
4//! Essential for vehicle routing where multiple consecutive stops need relocation.
5//!
6//! # Zero-Erasure Design
7//!
8//! Uses typed function pointers for list operations. No `dyn Any`, no downcasting.
9
10use std::fmt::Debug;
11use std::marker::PhantomData;
12
13use solverforge_core::domain::PlanningSolution;
14use solverforge_scoring::ScoreDirector;
15
16use super::Move;
17
18/// A move that relocates a contiguous sublist from one position to another.
19///
20/// Supports both intra-list moves (within same entity) and inter-list moves
21/// (between different entities). Uses typed function pointers for zero-erasure.
22///
23/// # Type Parameters
24/// * `S` - The planning solution type
25/// * `V` - The list element value type
26///
27/// # Example
28///
29/// ```
30/// use solverforge_solver::heuristic::r#move::SubListChangeMove;
31/// use solverforge_core::domain::PlanningSolution;
32/// use solverforge_core::score::SimpleScore;
33///
34/// #[derive(Clone, Debug)]
35/// struct Vehicle { id: usize, visits: Vec<i32> }
36///
37/// #[derive(Clone, Debug)]
38/// struct Solution { vehicles: Vec<Vehicle>, score: Option<SimpleScore> }
39///
40/// impl PlanningSolution for Solution {
41/// type Score = SimpleScore;
42/// fn score(&self) -> Option<Self::Score> { self.score }
43/// fn set_score(&mut self, score: Option<Self::Score>) { self.score = score; }
44/// }
45///
46/// fn list_len(s: &Solution, entity_idx: usize) -> usize {
47/// s.vehicles.get(entity_idx).map_or(0, |v| v.visits.len())
48/// }
49/// fn sublist_remove(s: &mut Solution, entity_idx: usize, start: usize, end: usize) -> Vec<i32> {
50/// s.vehicles.get_mut(entity_idx)
51/// .map(|v| v.visits.drain(start..end).collect())
52/// .unwrap_or_default()
53/// }
54/// fn sublist_insert(s: &mut Solution, entity_idx: usize, pos: usize, items: Vec<i32>) {
55/// if let Some(v) = s.vehicles.get_mut(entity_idx) {
56/// for (i, item) in items.into_iter().enumerate() {
57/// v.visits.insert(pos + i, item);
58/// }
59/// }
60/// }
61///
62/// // Move elements [1..3) from vehicle 0 to vehicle 1 at position 0
63/// let m = SubListChangeMove::<Solution, i32>::new(
64/// 0, 1, 3, // source: entity 0, range [1, 3)
65/// 1, 0, // dest: entity 1, position 0
66/// list_len, sublist_remove, sublist_insert,
67/// "visits", 0,
68/// );
69/// ```
70pub struct SubListChangeMove<S, V> {
71 /// Source entity index
72 source_entity_index: usize,
73 /// Start of range in source list (inclusive)
74 source_start: usize,
75 /// End of range in source list (exclusive)
76 source_end: usize,
77 /// Destination entity index
78 dest_entity_index: usize,
79 /// Position in destination list to insert at
80 dest_position: usize,
81 /// Get list length for an entity
82 list_len: fn(&S, usize) -> usize,
83 /// Remove sublist [start, end), returns removed elements
84 sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
85 /// Insert elements at position
86 sublist_insert: fn(&mut S, usize, usize, Vec<V>),
87 variable_name: &'static str,
88 descriptor_index: usize,
89 /// Store indices for entity_indices()
90 indices: [usize; 2],
91 _phantom: PhantomData<fn() -> V>,
92}
93
94impl<S, V> Clone for SubListChangeMove<S, V> {
95 fn clone(&self) -> Self {
96 *self
97 }
98}
99
100impl<S, V> Copy for SubListChangeMove<S, V> {}
101
102impl<S, V: Debug> Debug for SubListChangeMove<S, V> {
103 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
104 f.debug_struct("SubListChangeMove")
105 .field("source_entity", &self.source_entity_index)
106 .field("source_range", &(self.source_start..self.source_end))
107 .field("dest_entity", &self.dest_entity_index)
108 .field("dest_position", &self.dest_position)
109 .field("variable_name", &self.variable_name)
110 .finish()
111 }
112}
113
114impl<S, V> SubListChangeMove<S, V> {
115 /// Creates a new sublist change move with typed function pointers.
116 ///
117 /// # Arguments
118 /// * `source_entity_index` - Entity index to remove from
119 /// * `source_start` - Start of range (inclusive)
120 /// * `source_end` - End of range (exclusive)
121 /// * `dest_entity_index` - Entity index to insert into
122 /// * `dest_position` - Position in destination list
123 /// * `list_len` - Function to get list length
124 /// * `sublist_remove` - Function to remove range [start, end)
125 /// * `sublist_insert` - Function to insert elements at position
126 /// * `variable_name` - Name of the list variable
127 /// * `descriptor_index` - Entity descriptor index
128 #[allow(clippy::too_many_arguments)]
129 pub fn new(
130 source_entity_index: usize,
131 source_start: usize,
132 source_end: usize,
133 dest_entity_index: usize,
134 dest_position: usize,
135 list_len: fn(&S, usize) -> usize,
136 sublist_remove: fn(&mut S, usize, usize, usize) -> Vec<V>,
137 sublist_insert: fn(&mut S, usize, usize, Vec<V>),
138 variable_name: &'static str,
139 descriptor_index: usize,
140 ) -> Self {
141 Self {
142 source_entity_index,
143 source_start,
144 source_end,
145 dest_entity_index,
146 dest_position,
147 list_len,
148 sublist_remove,
149 sublist_insert,
150 variable_name,
151 descriptor_index,
152 indices: [source_entity_index, dest_entity_index],
153 _phantom: PhantomData,
154 }
155 }
156
157 /// Returns the source entity index.
158 pub fn source_entity_index(&self) -> usize {
159 self.source_entity_index
160 }
161
162 /// Returns the source range start (inclusive).
163 pub fn source_start(&self) -> usize {
164 self.source_start
165 }
166
167 /// Returns the source range end (exclusive).
168 pub fn source_end(&self) -> usize {
169 self.source_end
170 }
171
172 /// Returns the sublist length.
173 pub fn sublist_len(&self) -> usize {
174 self.source_end.saturating_sub(self.source_start)
175 }
176
177 /// Returns the destination entity index.
178 pub fn dest_entity_index(&self) -> usize {
179 self.dest_entity_index
180 }
181
182 /// Returns the destination position.
183 pub fn dest_position(&self) -> usize {
184 self.dest_position
185 }
186
187 /// Returns true if this is an intra-list move (same entity).
188 pub fn is_intra_list(&self) -> bool {
189 self.source_entity_index == self.dest_entity_index
190 }
191}
192
193impl<S, V> Move<S> for SubListChangeMove<S, V>
194where
195 S: PlanningSolution,
196 V: Clone + Send + Sync + Debug + 'static,
197{
198 fn is_doable<D: ScoreDirector<S>>(&self, score_director: &D) -> bool {
199 let solution = score_director.working_solution();
200
201 // Check range is valid (start < end)
202 if self.source_start >= self.source_end {
203 return false;
204 }
205
206 // Check source range is within bounds
207 let source_len = (self.list_len)(solution, self.source_entity_index);
208 if self.source_end > source_len {
209 return false;
210 }
211
212 // Check destination position is valid
213 let dest_len = (self.list_len)(solution, self.dest_entity_index);
214 let sublist_len = self.sublist_len();
215
216 let max_dest = if self.is_intra_list() {
217 // After removing sublist, list is shorter
218 source_len - sublist_len
219 } else {
220 dest_len
221 };
222
223 if self.dest_position > max_dest {
224 return false;
225 }
226
227 // For intra-list, check if move would actually change anything
228 if self.is_intra_list() {
229 // If dest is within the source range, it's a no-op
230 if self.dest_position >= self.source_start && self.dest_position <= self.source_end {
231 return false;
232 }
233 }
234
235 true
236 }
237
238 fn do_move<D: ScoreDirector<S>>(&self, score_director: &mut D) {
239 // Notify before changes
240 score_director.before_variable_changed(
241 self.descriptor_index,
242 self.source_entity_index,
243 self.variable_name,
244 );
245 if !self.is_intra_list() {
246 score_director.before_variable_changed(
247 self.descriptor_index,
248 self.dest_entity_index,
249 self.variable_name,
250 );
251 }
252
253 // Remove sublist from source
254 let elements = (self.sublist_remove)(
255 score_director.working_solution_mut(),
256 self.source_entity_index,
257 self.source_start,
258 self.source_end,
259 );
260
261 // dest_position is relative to post-removal list, no adjustment needed
262 let dest_pos = self.dest_position;
263
264 // Insert at destination
265 (self.sublist_insert)(
266 score_director.working_solution_mut(),
267 self.dest_entity_index,
268 dest_pos,
269 elements,
270 );
271
272 // Notify after changes
273 score_director.after_variable_changed(
274 self.descriptor_index,
275 self.source_entity_index,
276 self.variable_name,
277 );
278 if !self.is_intra_list() {
279 score_director.after_variable_changed(
280 self.descriptor_index,
281 self.dest_entity_index,
282 self.variable_name,
283 );
284 }
285
286 // Register undo
287 let sublist_remove = self.sublist_remove;
288 let sublist_insert = self.sublist_insert;
289 let src_entity = self.source_entity_index;
290 let src_start = self.source_start;
291 let dest_entity = self.dest_entity_index;
292 let sublist_len = self.sublist_len();
293
294 score_director.register_undo(Box::new(move |s: &mut S| {
295 // Remove from where we inserted
296 let removed = sublist_remove(s, dest_entity, dest_pos, dest_pos + sublist_len);
297 // Insert back at original position
298 sublist_insert(s, src_entity, src_start, removed);
299 }));
300 }
301
302 fn descriptor_index(&self) -> usize {
303 self.descriptor_index
304 }
305
306 fn entity_indices(&self) -> &[usize] {
307 if self.is_intra_list() {
308 &self.indices[0..1]
309 } else {
310 &self.indices
311 }
312 }
313
314 fn variable_name(&self) -> &str {
315 self.variable_name
316 }
317}
318
319#[cfg(test)]
320#[path = "sublist_change_tests.rs"]
321mod tests;