1use std::fmt;
14use std::str::FromStr;
15
16use serde::{Deserialize, Serialize};
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub struct Budget {
25 pub discrepancy: usize,
28 pub split_discrepancy: usize,
31}
32
33impl Budget {
34 pub fn split_budget(&self) -> usize {
36 self.split_discrepancy + 1
37 }
38}
39
40#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
42pub struct PassReport {
43 pub improved: bool,
45 pub cut_by_discrepancy: bool,
47 pub cut_by_split: bool,
49}
50
51#[derive(Clone, Copy, Debug, PartialEq, Eq)]
54pub struct ScheduleBounds {
55 pub max_discrepancy: usize,
57 pub max_splits: usize,
59 pub split_applies: bool,
63}
64
65pub trait BudgetSchedule: Send {
67 fn first(&mut self) -> Budget;
69
70 fn next(&mut self, last: &PassReport) -> Option<Budget>;
73}
74
75#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
77#[serde(rename_all = "lowercase")]
78pub enum ScheduleKind {
79 #[default]
82 Diagonal,
83 Square,
87}
88
89impl ScheduleKind {
90 pub const ALL: [Self; 2] = [Self::Diagonal, Self::Square];
92
93 pub const fn as_str(self) -> &'static str {
95 match self {
96 Self::Diagonal => "diagonal",
97 Self::Square => "square",
98 }
99 }
100
101 pub fn build(self, bounds: ScheduleBounds) -> Box<dyn BudgetSchedule> {
103 match self {
104 Self::Diagonal => Box::new(Diagonal::new(bounds)),
105 Self::Square => Box::new(Square::new(bounds)),
106 }
107 }
108}
109
110impl fmt::Display for ScheduleKind {
111 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112 f.write_str(self.as_str())
113 }
114}
115
116impl FromStr for ScheduleKind {
117 type Err = String;
118
119 fn from_str(s: &str) -> Result<Self, Self::Err> {
120 let wanted = s.trim().to_ascii_lowercase();
121 Self::ALL
122 .into_iter()
123 .find(|kind| kind.as_str() == wanted)
124 .ok_or_else(|| {
125 let names: Vec<_> = Self::ALL.iter().map(|k| k.as_str()).collect();
126 format!(
127 "unknown budget schedule `{wanted}` (expected one of: {})",
128 names.join(", ")
129 )
130 })
131 }
132}
133
134struct Diagonal {
141 max_discrepancy: usize,
142 max_split_discrepancy: Option<usize>,
144 current: Budget,
146}
147
148impl Diagonal {
149 fn new(bounds: ScheduleBounds) -> Self {
150 Self {
151 max_discrepancy: bounds.max_discrepancy,
152 max_split_discrepancy: bounds.max_splits.checked_sub(1),
153 current: Budget {
154 discrepancy: 0,
155 split_discrepancy: 0,
156 },
157 }
158 }
159}
160
161impl BudgetSchedule for Diagonal {
162 fn first(&mut self) -> Budget {
163 self.current
164 }
165
166 fn next(&mut self, _last: &PassReport) -> Option<Budget> {
167 let max_s = self.max_split_discrepancy?;
168 let max_d = self.max_discrepancy;
169 let mut sum = self.current.discrepancy + self.current.split_discrepancy;
170 let mut d = self.current.discrepancy + 1;
171 while sum <= max_d + max_s {
172 d = d.max(sum.saturating_sub(max_s));
174 if d <= sum.min(max_d) {
175 self.current = Budget {
176 discrepancy: d,
177 split_discrepancy: sum - d,
178 };
179 return Some(self.current);
180 }
181 sum += 1;
182 d = 0;
183 }
184 None
185 }
186}
187
188struct Square {
204 max_discrepancy: usize,
205 max_split_discrepancy: usize,
206 shell: usize,
207 pending: std::collections::VecDeque<Budget>,
208}
209
210impl Square {
211 fn new(bounds: ScheduleBounds) -> Self {
212 Self {
213 max_discrepancy: bounds.max_discrepancy,
214 max_split_discrepancy: if bounds.split_applies {
215 bounds.max_splits.saturating_sub(1)
216 } else {
217 0
218 },
219 shell: 0,
220 pending: Default::default(),
221 }
222 }
223
224 fn fill(&mut self, k: usize) {
226 let (max_d, max_s) = (self.max_discrepancy, self.max_split_discrepancy);
227 let pairs = (0..k)
228 .map(|s| (k, s))
229 .chain((0..k).map(|d| (d, k)))
230 .chain(std::iter::once((k, k)));
231 self.pending
232 .extend(pairs.filter(|&(d, s)| d <= max_d && s <= max_s).map(
233 |(discrepancy, split_discrepancy)| Budget {
234 discrepancy,
235 split_discrepancy,
236 },
237 ));
238 }
239}
240
241impl BudgetSchedule for Square {
242 fn first(&mut self) -> Budget {
243 Budget {
244 discrepancy: 0,
245 split_discrepancy: 0,
246 }
247 }
248
249 fn next(&mut self, _last: &PassReport) -> Option<Budget> {
250 while self.pending.is_empty() {
252 self.shell += 1;
253 if self.shell > self.max_discrepancy.max(self.max_split_discrepancy) {
254 return None;
255 }
256 self.fill(self.shell);
257 }
258 self.pending.pop_front()
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use super::*;
265
266 fn walk(kind: ScheduleKind, bounds: ScheduleBounds) -> Vec<(usize, usize)> {
267 let mut schedule = kind.build(bounds);
268 let mut out = vec![schedule.first()];
269 while let Some(b) = schedule.next(&PassReport::default()) {
270 out.push(b);
271 }
272 out.iter()
273 .map(|b| (b.discrepancy, b.split_discrepancy))
274 .collect()
275 }
276
277 #[test]
278 fn the_diagonal_walks_by_sum_and_never_repeats_a_budget() {
279 let bounds = ScheduleBounds {
280 max_discrepancy: 2,
281 max_splits: 3,
282 split_applies: true,
283 };
284 let seen = walk(ScheduleKind::Diagonal, bounds);
285 let expected = [
286 (0, 0),
287 (0, 1),
288 (1, 0),
289 (0, 2),
290 (1, 1),
291 (2, 0),
292 (1, 2),
293 (2, 1),
294 (2, 2),
295 ];
296 assert_eq!(seen, expected);
297 let mut distinct = seen.clone();
298 distinct.sort_unstable();
299 distinct.dedup();
300 assert_eq!(distinct.len(), seen.len());
301 }
302
303 #[test]
304 fn the_square_grows_shell_by_shell() {
305 let bounds = ScheduleBounds {
306 max_discrepancy: 2,
307 max_splits: 3,
308 split_applies: true,
309 };
310 assert_eq!(
311 walk(ScheduleKind::Square, bounds),
312 vec![
313 (0, 0),
314 (1, 0),
315 (0, 1),
316 (1, 1),
317 (2, 0),
318 (2, 1),
319 (0, 2),
320 (1, 2),
321 (2, 2),
322 ]
323 );
324 }
325
326 #[test]
327 fn the_square_covers_the_rectangle_once_and_ends_unrestricted() {
328 let bounds = ScheduleBounds {
329 max_discrepancy: 3,
330 max_splits: 6,
331 split_applies: true,
332 };
333 let seen = walk(ScheduleKind::Square, bounds);
334 let mut sorted = seen.clone();
335 sorted.sort_unstable();
336 sorted.dedup();
337 assert_eq!(sorted.len(), seen.len(), "a budget was repeated");
338 assert_eq!(seen.len(), 4 * 6, "every budget in the rectangle, once");
339 assert_eq!(*seen.last().unwrap(), (3, 5));
340 }
341
342 #[test]
343 fn each_square_shell_ends_on_a_budget_containing_all_before_it() {
344 let bounds = ScheduleBounds {
345 max_discrepancy: 4,
346 max_splits: 5,
347 split_applies: true,
348 };
349 let seen = walk(ScheduleKind::Square, bounds);
350 for (i, &(d, s)) in seen.iter().enumerate() {
351 if d == s {
352 assert!(seen[..i].iter().all(|&(pd, ps)| pd <= d && ps <= s));
353 }
354 }
355 }
356
357 #[test]
358 fn without_a_split_budget_the_square_walks_only_the_discrepancy() {
359 let bounds = ScheduleBounds {
360 max_discrepancy: 3,
361 max_splits: 10,
362 split_applies: false,
363 };
364 assert_eq!(
365 walk(ScheduleKind::Square, bounds),
366 vec![(0, 0), (1, 0), (2, 0), (3, 0)]
367 );
368 }
369
370 #[test]
371 fn schedules_parse_from_their_own_names() {
372 for kind in ScheduleKind::ALL {
373 assert_eq!(kind.as_str().parse::<ScheduleKind>(), Ok(kind));
374 }
375 assert!("sideways".parse::<ScheduleKind>().is_err());
376 }
377}