1use crate::clock::{time_of_day, TimeOfDay};
2use crate::questions::{Pack, Question};
3
4#[derive(Default)]
6pub struct SelectionState {
7 pub served_count: std::collections::HashMap<String, u32>,
9 pub last_served: std::collections::HashMap<String, u64>,
11 pub held_run: Option<(String, u32)>,
13}
14
15pub fn select<'a>(pack: &'a Pack, state: &SelectionState, hour: u32) -> Option<&'a Question> {
16 if let Some((id, done)) = &state.held_run {
18 if let Some(q) = pack.questions.iter().find(|q| &q.id == id) {
19 if let Some(len) = Pack::held_len(&q.cadence) {
20 if *done < len {
21 return Some(q);
22 }
23 }
24 }
25 }
26
27 let slot = match time_of_day(hour) {
28 TimeOfDay::Morning => Some("morning"),
29 TimeOfDay::Evening => Some("evening"),
30 _ => None,
31 };
32
33 let candidates: Vec<&Question> = match slot {
34 Some(s) if pack.questions.iter().any(|q| q.slot.as_deref() == Some(s)) => {
35 pack.questions.iter().filter(|q| q.slot.as_deref() == Some(s)).collect()
36 }
37 _ => pack.questions.iter().collect(),
38 };
39
40 candidates.into_iter().enumerate().min_by(|(ia, a), (ib, b)| {
42 let la = state.last_served.get(&a.id).copied().unwrap_or(0);
43 let lb = state.last_served.get(&b.id).copied().unwrap_or(0);
44 let ca = state.served_count.get(&a.id).copied().unwrap_or(0);
45 let cb = state.served_count.get(&b.id).copied().unwrap_or(0);
46 la.cmp(&lb).then(ca.cmp(&cb)).then(ia.cmp(ib))
47 }).map(|(_, q)| q)
48}
49
50#[cfg(test)]
51mod tests {
52 use super::*;
53
54 fn pack() -> Pack {
55 Pack::from_toml(r#"
56 name = "t"
57 [[questions]]
58 id = "a"
59 text = "A?"
60 slot = "morning"
61 [[questions]]
62 id = "b"
63 text = "B?"
64 slot = "evening"
65 [[questions]]
66 id = "h"
67 text = "Held?"
68 cadence = "held:7"
69 "#).unwrap()
70 }
71
72 #[test]
73 fn held_run_keeps_serving_until_complete() {
74 let p = pack();
75 let st = SelectionState { held_run: Some(("h".into(), 3)), ..Default::default() };
76 assert_eq!(select(&p, &st, 10).unwrap().id, "h");
77 }
78
79 #[test]
80 fn held_run_releases_when_complete() {
81 let p = pack();
82 let st = SelectionState { held_run: Some(("h".into(), 7)), ..Default::default() };
83 assert_ne!(select(&p, &st, 7).unwrap().id, "h");
84 }
85
86 #[test]
87 fn morning_prefers_morning_slot() {
88 let p = pack();
89 let st = SelectionState::default();
90 assert_eq!(select(&p, &st, 7).unwrap().id, "a");
91 }
92
93 #[test]
94 fn rotation_avoids_the_most_recent() {
95 let p = pack();
96 let mut st = SelectionState::default();
97 st.last_served.insert("a".into(), 5);
98 assert_ne!(select(&p, &st, 12).unwrap().id, "a");
100 }
101}