| use chrono::{Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime, Weekday}; |
| use rand::prelude::*; |
| use rand::rngs::StdRng; |
| use std::collections::BTreeSet; |
|
|
| use crate::domain::Shift; |
|
|
| use super::vocabulary::{DAYS_IN_SCHEDULE, FIRST_NAMES, LAST_NAMES}; |
|
|
| |
| pub(super) fn horizon_dates(shifts: &[Shift]) -> Vec<NaiveDate> { |
| let mut dates = BTreeSet::new(); |
| for shift in shifts { |
| for &date in &shift.touched_dates { |
| dates.insert(date); |
| } |
| } |
| dates.into_iter().collect() |
| } |
|
|
| |
| pub(super) fn weekday_dates(start_date: NaiveDate) -> [Vec<NaiveDate>; 7] { |
| let mut dates = std::array::from_fn(|_| Vec::new()); |
| for day in 0..DAYS_IN_SCHEDULE { |
| let date = start_date + Duration::days(day); |
| dates[date.weekday().num_days_from_monday() as usize].push(date); |
| } |
| dates |
| } |
|
|
| |
| pub(super) fn choose_weekday_with_four_available_dates( |
| unavailable_dates: &BTreeSet<NaiveDate>, |
| primary_off: usize, |
| candidates: impl IntoIterator<Item = usize>, |
| ) -> usize { |
| candidates |
| .into_iter() |
| .filter(|&weekday| weekday != primary_off) |
| .find(|&weekday| { |
| weekday_dates(find_next_monday( |
| NaiveDate::from_ymd_opt(2024, 1, 1).unwrap(), |
| ))[weekday] |
| .iter() |
| .filter(|date| !unavailable_dates.contains(date)) |
| .count() |
| == 4 |
| }) |
| .expect("weekday with four available dates should exist") |
| } |
|
|
| |
| pub(super) fn dates_touched_by_span(start: NaiveDateTime, end: NaiveDateTime) -> Vec<NaiveDate> { |
| let mut touched_dates = Vec::new(); |
| let mut date = start.date(); |
|
|
| while date <= end.date() { |
| if overlap_minutes_for_day(start, end, date) > 0 { |
| touched_dates.push(date); |
| } |
|
|
| let Some(next_date) = date.succ_opt() else { |
| break; |
| }; |
| date = next_date; |
| } |
|
|
| touched_dates |
| } |
|
|
| |
| fn overlap_minutes_for_day(start: NaiveDateTime, end: NaiveDateTime, date: NaiveDate) -> i64 { |
| let day_start = date.and_hms_opt(0, 0, 0).unwrap(); |
| let day_end = date |
| .succ_opt() |
| .unwrap_or(date) |
| .and_hms_opt(0, 0, 0) |
| .unwrap(); |
|
|
| let overlap_start = start.max(day_start); |
| let overlap_end = end.min(day_end); |
|
|
| if overlap_start < overlap_end { |
| (overlap_end - overlap_start).num_minutes() |
| } else { |
| 0 |
| } |
| } |
|
|
| |
| pub(super) fn generate_name_permutations(rng: &mut StdRng) -> Vec<String> { |
| let mut names = Vec::with_capacity(FIRST_NAMES.len() * LAST_NAMES.len()); |
| for first in FIRST_NAMES { |
| for last in LAST_NAMES { |
| names.push(format!("{first} {last}")); |
| } |
| } |
| names.shuffle(rng); |
| names |
| } |
|
|
| |
| pub(super) fn time(hour: u32, minute: u32) -> NaiveTime { |
| NaiveTime::from_hms_opt(hour, minute, 0).unwrap() |
| } |
|
|
| |
| pub(super) fn find_next_monday(date: NaiveDate) -> NaiveDate { |
| let days_until_monday = match date.weekday() { |
| Weekday::Mon => 0, |
| Weekday::Tue => 6, |
| Weekday::Wed => 5, |
| Weekday::Thu => 4, |
| Weekday::Fri => 3, |
| Weekday::Sat => 2, |
| Weekday::Sun => 1, |
| }; |
| date + Duration::days(days_until_monday) |
| } |
|
|