1use chrono::NaiveDate;
9use shift_algorithm::{get_shift_type_for_date, ShiftCycleConfig, ShiftType};
10
11pub fn count_shift_type_in_month(
28 year: i32,
29 month: u32,
30 shift_type: ShiftType,
31 config: &ShiftCycleConfig,
32 team_phase_offset: u32,
33) -> u32 {
34 let start = NaiveDate::from_ymd_opt(year, month, 1).unwrap();
35 let end = if month == 12 {
36 NaiveDate::from_ymd_opt(year + 1, 1, 1).unwrap()
37 } else {
38 NaiveDate::from_ymd_opt(year, month + 1, 1).unwrap()
39 };
40
41 let mut count = 0u32;
42 let mut current = start;
43 while current < end {
44 if get_shift_type_for_date(current, config, team_phase_offset) == shift_type {
45 count += 1;
46 }
47 current += chrono::Duration::days(1);
48 }
49 count
50}
51
52pub fn count_work_days_in_month(
64 year: i32,
65 month: u32,
66 config: &ShiftCycleConfig,
67 team_phase_offset: u32,
68) -> u32 {
69 let start = NaiveDate::from_ymd_opt(year, month, 1).unwrap();
70 let end = if month == 12 {
71 NaiveDate::from_ymd_opt(year + 1, 1, 1).unwrap()
72 } else {
73 NaiveDate::from_ymd_opt(year, month + 1, 1).unwrap()
74 };
75
76 let mut count = 0u32;
77 let mut current = start;
78 while current < end {
79 let st = get_shift_type_for_date(current, config, team_phase_offset);
80 if st.is_work() {
81 count += 1;
82 }
83 current += chrono::Duration::days(1);
84 }
85 count
86}
87
88pub fn consecutive_work_days(
105 today: NaiveDate,
106 config: &ShiftCycleConfig,
107 team_phase_offset: u32,
108) -> u32 {
109 const MAX_ITERATIONS: u32 = 10000;
110 let mut count = 0u32;
111 let mut current = today;
112 for _ in 0..MAX_ITERATIONS {
113 let st = get_shift_type_for_date(current, config, team_phase_offset);
114 if st.is_work() {
115 count += 1;
116 } else {
117 break;
118 }
119 current -= chrono::Duration::days(1);
120 }
121 count
122}
123
124pub fn days_until_next_rest(
148 today: NaiveDate,
149 config: &ShiftCycleConfig,
150 team_phase_offset: u32,
151) -> u32 {
152 const MAX_ITERATIONS: u32 = 10000;
153 let mut count = 0u32;
154 let mut current = today + chrono::Duration::days(1);
155 for _ in 0..MAX_ITERATIONS {
156 let st = get_shift_type_for_date(current, config, team_phase_offset);
157 if st.is_rest() {
158 return count;
159 }
160 count += 1;
161 current += chrono::Duration::days(1);
162 }
163 count
164}
165
166#[cfg(test)]
167mod tests {
168 use super::*;
169 use shift_algorithm::cycle::default_config;
170
171 #[test]
172 fn count_morning_in_may_2026() {
173 let config = default_config();
174 let count = count_shift_type_in_month(2026, 5, ShiftType::Morning, &config, 0);
175 assert!(count > 0);
176 assert!(count <= 31);
177 }
178
179 #[test]
180 fn count_work_days_in_31_day_month() {
181 let config = default_config();
182 let work = count_work_days_in_month(2026, 5, &config, 0);
183 let total = count_shift_type_in_month(2026, 5, ShiftType::Morning, &config, 0)
184 + count_shift_type_in_month(2026, 5, ShiftType::Afternoon, &config, 0)
185 + count_shift_type_in_month(2026, 5, ShiftType::Night, &config, 0);
186 assert_eq!(work, total);
187 }
188
189 #[test]
190 fn consecutive_work_days_non_negative() {
191 let config = default_config();
192 let date = NaiveDate::from_ymd_opt(2026, 5, 22).unwrap();
193 let c = consecutive_work_days(date, &config, 0);
194 assert!(c < 365);
195 }
196
197 #[test]
198 fn days_until_rest_positive() {
199 let config = default_config();
200 let date = NaiveDate::from_ymd_opt(2026, 5, 22).unwrap();
201 let d = days_until_next_rest(date, &config, 0);
202 assert!(d < 42);
203 }
204
205 #[test]
208 fn all_work_cycle_consecutive_is_bounded() {
209 use ShiftType::*;
210 let config = ShiftCycleConfig {
211 cycle: vec![Morning; 3],
212 cycle_length: 3,
213 reference_date: shift_algorithm::cycle::default_reference_date(),
214 total_teams: 1,
215 };
216 let date = NaiveDate::from_ymd_opt(2026, 5, 22).unwrap();
217 let c = consecutive_work_days(date, &config, 0);
218 assert_eq!(c, 10000);
219 }
220
221 #[test]
222 fn all_work_cycle_days_until_rest_bounded() {
223 use ShiftType::*;
224 let config = ShiftCycleConfig {
225 cycle: vec![Morning; 3],
226 cycle_length: 3,
227 reference_date: shift_algorithm::cycle::default_reference_date(),
228 total_teams: 1,
229 };
230 let date = NaiveDate::from_ymd_opt(2026, 5, 22).unwrap();
231 let d = days_until_next_rest(date, &config, 0);
232 assert_eq!(d, 10000);
233 }
234
235 #[test]
236 fn all_rest_cycle_consecutive_is_zero() {
237 use ShiftType::*;
238 let config = ShiftCycleConfig {
239 cycle: vec![Rest; 5],
240 cycle_length: 5,
241 reference_date: shift_algorithm::cycle::default_reference_date(),
242 total_teams: 1,
243 };
244 let date = NaiveDate::from_ymd_opt(2026, 5, 22).unwrap();
245 assert_eq!(consecutive_work_days(date, &config, 0), 0);
246 }
247
248 #[test]
249 fn all_rest_cycle_count_work_is_zero() {
250 use ShiftType::*;
251 let config = ShiftCycleConfig {
252 cycle: vec![Rest; 5],
253 cycle_length: 5,
254 reference_date: shift_algorithm::cycle::default_reference_date(),
255 total_teams: 1,
256 };
257 let work = count_work_days_in_month(2026, 5, &config, 0);
258 assert_eq!(work, 0);
259 }
260
261 #[test]
262 fn february_2026_has_28_days() {
263 let config = default_config();
264 let morning = count_shift_type_in_month(2026, 2, ShiftType::Morning, &config, 0);
265 let afternoon = count_shift_type_in_month(2026, 2, ShiftType::Afternoon, &config, 0);
266 let rest = count_shift_type_in_month(2026, 2, ShiftType::Rest, &config, 0);
267 let night = count_shift_type_in_month(2026, 2, ShiftType::Night, &config, 0);
268 let study = count_shift_type_in_month(2026, 2, ShiftType::Study, &config, 0);
269 assert_eq!(morning + afternoon + rest + night + study, 28);
270 }
271
272 #[test]
273 fn december_2026_transition() {
274 let config = default_config();
275 let total = count_shift_type_in_month(2026, 12, ShiftType::Morning, &config, 0)
276 + count_shift_type_in_month(2026, 12, ShiftType::Afternoon, &config, 0)
277 + count_shift_type_in_month(2026, 12, ShiftType::Rest, &config, 0)
278 + count_shift_type_in_month(2026, 12, ShiftType::Night, &config, 0)
279 + count_shift_type_in_month(2026, 12, ShiftType::Study, &config, 0);
280 assert_eq!(total, 31);
281 }
282}