iicp_client/
availability.rs1use chrono::{Local, Timelike};
15
16#[derive(Debug, Clone, PartialEq)]
18pub struct Window {
19 pub start: String, pub end: String, pub share: f64,
22}
23
24#[derive(Debug, Clone, Default)]
26pub struct AvailabilityEvaluator {
27 windows: Vec<Window>,
28}
29
30impl AvailabilityEvaluator {
31 pub fn new(windows: Vec<Window>) -> Self {
32 Self { windows }
33 }
34
35 fn now_hhmm() -> String {
36 let now = Local::now();
37 format!("{:02}:{:02}", now.hour(), now.minute())
38 }
39
40 pub fn current_share(&self) -> f64 {
42 self.share_at(&Self::now_hhmm())
43 }
44
45 pub fn share_at(&self, current: &str) -> f64 {
47 if self.windows.is_empty() {
48 return 1.0;
49 }
50 for w in &self.windows {
51 if w.start <= w.end {
52 if w.start.as_str() <= current && current <= w.end.as_str() {
53 return w.share;
54 }
55 } else if current >= w.start.as_str() || current <= w.end.as_str() {
56 return w.share;
58 }
59 }
60 0.5 }
62
63 pub fn effective_max_concurrent(&self, base_max: usize) -> usize {
66 self.effective_at(base_max, &Self::now_hhmm())
67 }
68
69 pub fn effective_at(&self, base_max: usize, current: &str) -> usize {
71 if base_max == 0 {
72 return 0;
73 }
74 let share = self.share_at(current);
75 if share <= 0.0 {
76 return 0;
77 }
78 std::cmp::max(1, (base_max as f64 * share) as usize)
79 }
80
81 pub fn is_within_window(&self) -> bool {
82 self.windows.is_empty() || self.current_share() > 0.0
83 }
84}
85
86#[cfg(test)]
87mod tests {
88 use super::*;
89
90 fn win(start: &str, end: &str, share: f64) -> Window {
91 Window {
92 start: start.into(),
93 end: end.into(),
94 share,
95 }
96 }
97
98 #[test]
99 fn no_windows_full() {
100 let ev = AvailabilityEvaluator::default();
101 assert_eq!(ev.share_at("03:00"), 1.0);
102 assert_eq!(ev.effective_at(4, "14:00"), 4);
103 }
104
105 #[test]
106 fn inside_normal_window() {
107 let ev = AvailabilityEvaluator::new(vec![win("08:00", "22:00", 0.5)]);
108 assert_eq!(ev.share_at("12:00"), 0.5);
109 assert_eq!(ev.effective_at(4, "12:00"), 2);
110 }
111
112 #[test]
113 fn outside_window_half() {
114 let ev = AvailabilityEvaluator::new(vec![win("08:00", "22:00", 1.0)]);
115 assert_eq!(ev.share_at("02:00"), 0.5);
116 }
117
118 #[test]
119 fn floors_at_one_when_share_positive() {
120 let ev = AvailabilityEvaluator::new(vec![win("08:00", "22:00", 0.1)]);
121 assert_eq!(ev.effective_at(4, "10:00"), 1);
122 }
123
124 #[test]
125 fn base_zero_stays_zero() {
126 let ev = AvailabilityEvaluator::default();
127 assert_eq!(ev.effective_at(0, "10:00"), 0);
128 }
129
130 #[test]
131 fn midnight_spanning_window() {
132 let ev = AvailabilityEvaluator::new(vec![win("22:00", "06:00", 1.0)]);
133 assert_eq!(ev.share_at("23:30"), 1.0);
134 assert_eq!(ev.share_at("02:00"), 1.0);
135 assert_eq!(ev.share_at("12:00"), 0.5);
136 }
137
138 #[test]
139 fn closed_window_zero_capacity() {
140 let ev = AvailabilityEvaluator::new(vec![win("00:00", "23:59", 0.0)]);
141 assert_eq!(ev.effective_at(4, "10:00"), 0);
142 }
143}