use chrono::{Duration, Months, NaiveDate};
use crate::{AgdError, ErrorCode, PlanningTimestamp};
use crate::date::parse_org_date;
pub(super) fn repeated_dates(
timestamp: &PlanningTimestamp,
from: NaiveDate,
to: NaiveDate,
) -> Result<Vec<NaiveDate>, AgdError> {
let mut date = parse_org_date(×tamp.date)?;
let Some(repeater) = timestamp.repeater.as_deref() else {
return Ok((date >= from && date <= to)
.then_some(date)
.into_iter()
.collect());
};
let (_, amount, unit) = parse_repeater(repeater)?;
while date < from {
date = add_period(date, amount, unit)?;
}
let mut dates = Vec::new();
while date <= to {
dates.push(date);
date = add_period(date, amount, unit)?;
}
Ok(dates)
}
pub(super) fn planning_dates(
timestamp: &PlanningTimestamp,
from: NaiveDate,
to: NaiveDate,
today: NaiveDate,
done: bool,
) -> Result<Vec<NaiveDate>, AgdError> {
let base = parse_org_date(×tamp.date)?;
let mut dates = Vec::new();
if base >= from && base <= to {
dates.push(base);
}
if done {
return Ok(dates);
}
for date in repeated_dates(timestamp, from, to)? {
if date > today && date != base {
dates.push(date);
}
}
Ok(dates)
}
pub fn next_repeat(timestamp: &PlanningTimestamp, today: NaiveDate) -> Result<NaiveDate, AgdError> {
let repeater = timestamp
.repeater
.as_deref()
.ok_or_else(|| AgdError::new(ErrorCode::InvalidRepeater, "timestamp has no repeater"))?;
let (kind, amount, unit) = parse_repeater(repeater)?;
if kind == ".+" {
return add_period(today, amount, unit);
}
let mut next = add_period(parse_org_date(×tamp.date)?, amount, unit)?;
while kind == "++" && next <= today {
next = add_period(next, amount, unit)?;
}
Ok(next)
}
fn parse_repeater(input: &str) -> Result<(&str, u64, char), AgdError> {
let input = input.split('/').next().unwrap_or(input);
let invalid = || {
AgdError::new(
ErrorCode::InvalidRepeater,
format!("invalid repeater: {input}"),
)
};
let (kind, value) = if let Some(value) = input.strip_prefix("++") {
("++", value)
} else if let Some(value) = input.strip_prefix(".+") {
(".+", value)
} else if let Some(value) = input.strip_prefix('+') {
("+", value)
} else {
return Err(invalid());
};
let unit = value.chars().next_back().ok_or_else(invalid)?;
let amount = value[..value.len() - unit.len_utf8()]
.parse::<u64>()
.map_err(|_| invalid())?;
if amount == 0 {
return Err(AgdError::new(
ErrorCode::InvalidRepeater,
"repeater interval must be positive",
));
}
Ok((kind, amount, unit))
}
fn add_period(date: NaiveDate, amount: u64, unit: char) -> Result<NaiveDate, AgdError> {
let next = match unit {
'd' => date.checked_add_signed(Duration::days(amount as i64)),
'w' => date.checked_add_signed(Duration::weeks(amount as i64)),
'm' => date.checked_add_months(Months::new(amount as u32)),
'y' => date.checked_add_months(Months::new((amount * 12) as u32)),
_ => None,
};
next.ok_or_else(|| AgdError::new(ErrorCode::InvalidRepeater, "repeater date is out of range"))
}