use crate::task; use crate::util; use anyhow::Context as _; #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum BatchingInterval { Monthly, Weekly, OnTheDay, } pub async fn remind_due_tasks(ctx: &crate::Context, tasks: &[task::Task]) -> anyhow::Result<()> { for task in tasks { let Some(due_date) = task.state.due_date_if_open() else { continue; }; let batching_interval = find_batching_interval(ctx, due_date, task); log::debug!("Reminding {task} with interval {batching_interval:?}."); if !is_time_to_remind(ctx, due_date, batching_interval) { log::debug!("Not yet time, skipping."); continue; } if is_overdue(ctx, due_date) { log::info!("Task {task} is already overdue!"); } log::warn!("TODO: Remind {task}"); } Ok(()) } fn find_batching_interval( ctx: &crate::Context, due_date: &jiff::Zoned, task: &task::Task, ) -> BatchingInterval { let time_until_due = due_date - &ctx.timestamp; if let Some(recurring) = &task.recurring { match &recurring.interval { task::RecurringInterval::Months(_) => BatchingInterval::Monthly, task::RecurringInterval::Weeks(_) => BatchingInterval::Weekly, task::RecurringInterval::Days(_) => BatchingInterval::OnTheDay, } } else { // For tasks that are not recurring, the batching interval is determined based on how // far in the future the task is due. let weeks_until_due = time_until_due .total((jiff::Unit::Week, jiff::SpanRelativeTo::days_are_24_hours())) .unwrap(); if weeks_until_due >= 3. { BatchingInterval::Monthly } else if weeks_until_due >= 1. { BatchingInterval::Weekly } else { BatchingInterval::OnTheDay } } } fn is_time_to_remind( ctx: &crate::Context, due_date: &jiff::Zoned, batching_interval: BatchingInterval, ) -> bool { let batch_time = match batching_interval { BatchingInterval::Monthly => due_date.first_of_month().unwrap(), BatchingInterval::Weekly => start_of_week(due_date).unwrap(), BatchingInterval::OnTheDay => due_date.clone(), }; let batch_time = batch_time .round( jiff::ZonedRound::new() .smallest(jiff::Unit::Day) .mode(jiff::RoundMode::Floor), ) .unwrap(); ctx.timestamp >= batch_time } fn is_overdue(ctx: &crate::Context, due_date: &jiff::Zoned) -> bool { &ctx.timestamp >= due_date } fn start_of_week(t: &jiff::Zoned) -> anyhow::Result { Ok(t.tomorrow()? .nth_weekday(-1, jiff::civil::Weekday::Monday)?) }