use chrono::{Datelike, Timelike};
use serde::Deserialize;
use super::calendar::Calendar;
use super::goal::{Budget, BudgetPeriod};
use super::time_grid;
use chrono::NaiveDateTime;
#[derive(Debug, Clone, Deserialize)]
pub struct CalendarBudget {
pub budget_id: String,
pub title: String,
pub participating_goals: Vec<String>,
pub time_budgets: Vec<TimeBudget>,
pub periods: Vec<BudgetPeriod>,
}
impl CalendarBudget {
pub fn applies_to(&self, id: &str) -> bool {
self.budget_id == id || self.participating_goals.iter().any(|g| g == id)
}
pub fn reduce_for_(
&mut self,
calendar_start: NaiveDateTime,
goal: &str,
cal_index: usize,
cal_index_end: usize,
) {
if !self.applies_to(goal) {
return;
}
for time_budget in self.time_budgets.iter_mut() {
for offset in 0..(cal_index_end - cal_index) {
let slot = cal_index + offset;
if slot < time_budget.calendar_start_index || slot >= time_budget.calendar_end_index
{
continue;
}
match time_budget.time_budget_type {
TimeBudgetType::Week => {
time_budget.scheduled += 1;
}
TimeBudgetType::Period => {
let Some(period_index) = time_budget.period_index else {
continue;
};
if slot_in_period_window(calendar_start, &self.periods[period_index], slot)
{
time_budget.scheduled += 1;
}
}
}
}
}
}
pub fn slot_counts_toward(
&self,
calendar_start: NaiveDateTime,
time_budget: &TimeBudget,
slot: usize,
) -> bool {
if slot < time_budget.calendar_start_index || slot >= time_budget.calendar_end_index {
return false;
}
match time_budget.time_budget_type {
TimeBudgetType::Week => true,
TimeBudgetType::Period => {
let Some(period_index) = time_budget.period_index else {
return false;
};
slot_in_period_window(calendar_start, &self.periods[period_index], slot)
}
}
}
}
pub fn slot_in_period_window(
calendar_start: NaiveDateTime,
period: &BudgetPeriod,
slot: usize,
) -> bool {
let dt = time_grid::datetime_at(calendar_start, slot);
if !period.on_days.contains(&dt.weekday()) {
return false;
}
let minutes = dt.hour() as usize * 60 + dt.minute() as usize;
let slot_of_day = minutes / time_grid::SLOT_MINUTES as usize;
let after = period.window.after_time;
let before = period.window.before_time;
if after < before {
slot_of_day >= after && slot_of_day < before
} else {
slot_of_day >= after || slot_of_day < before
}
}
#[derive(Deserialize, Debug, Clone, PartialEq)]
pub enum TimeBudgetType {
Period,
Week,
}
#[derive(Clone, Deserialize)]
pub struct TimeBudget {
pub time_budget_type: TimeBudgetType,
pub period_index: Option<usize>,
pub calendar_start_index: usize,
pub calendar_end_index: usize,
pub scheduled: usize,
pub min_scheduled: usize,
pub max_scheduled: usize,
}
impl std::fmt::Debug for TimeBudget {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"\n{:?} budget (period={:?}) from index {:?}-{:?}: Scheduled {:?} / {:?}-{:?}\n",
&self.time_budget_type,
&self.period_index,
&self.calendar_start_index,
&self.calendar_end_index,
&self.scheduled,
&self.min_scheduled,
&self.max_scheduled
)
}
}
pub fn get_time_budgets_from(calendar: &Calendar, budget: &Budget) -> Vec<TimeBudget> {
crate::log_debug!("Getting time budgets from budget {}", budget.id);
let mut time_budgets: Vec<TimeBudget> = vec![];
for week_start in (time_grid::BUFFER_SLOTS..calendar.hours() - time_grid::BUFFER_SLOTS)
.step_by(time_grid::SLOTS_PER_WEEK)
{
#[cfg(debug_assertions)]
{
use chrono::NaiveTime;
assert_eq!(
calendar.get_datetime_of(week_start).time(),
NaiveTime::from_hms_opt(0, 0, 0).unwrap(),
"Assumed week boundary should be at midnight mark."
);
}
let week_end = week_start + time_grid::SLOTS_PER_WEEK;
for (period_index, period) in budget.periods.iter().enumerate() {
time_budgets.push(TimeBudget {
time_budget_type: TimeBudgetType::Period,
period_index: Some(period_index),
calendar_start_index: week_start,
calendar_end_index: week_end,
scheduled: 0,
min_scheduled: period.min_for_period,
max_scheduled: period.max_for_period,
});
}
time_budgets.push(TimeBudget {
time_budget_type: TimeBudgetType::Week,
period_index: None,
calendar_start_index: week_start,
calendar_end_index: week_end,
scheduled: 0,
min_scheduled: budget.min_per_week,
max_scheduled: budget.max_per_week,
});
}
crate::log_dbg!(&time_budgets);
time_budgets
}