codex-recall 0.1.2

Local search and recall for Codex session JSONL archives
Documentation
use anyhow::{bail, Result};

pub fn resolve_date_window(
    since: Option<String>,
    from: Option<String>,
    until: Option<String>,
    day: Option<String>,
) -> Result<(Option<String>, Option<String>, Option<String>)> {
    let Some(day) = day else {
        return Ok((since, from, until));
    };

    if since.is_some() || from.is_some() || until.is_some() {
        bail!("use --day by itself; do not combine it with --since, --from, or --until");
    }

    let next_day = next_calendar_day(&day)?;
    Ok((None, Some(day), Some(next_day)))
}

fn next_calendar_day(day: &str) -> Result<String> {
    let (year, month, day_of_month) = parse_day(day)?;
    let days_this_month = days_in_month(year, month)?;
    let (year, month, day_of_month) = if day_of_month < days_this_month {
        (year, month, day_of_month + 1)
    } else if month < 12 {
        (year, month + 1, 1)
    } else {
        (year + 1, 1, 1)
    };

    Ok(format!("{year:04}-{month:02}-{day_of_month:02}"))
}

fn parse_day(day: &str) -> Result<(u32, u32, u32)> {
    let parts = day.split('-').collect::<Vec<_>>();
    if parts.len() != 3 {
        bail!("unsupported --day value `{day}`; use YYYY-MM-DD");
    }

    let year = parts[0]
        .parse::<u32>()
        .map_err(|_| anyhow::anyhow!("unsupported --day value `{day}`; use YYYY-MM-DD"))?;
    let month = parts[1]
        .parse::<u32>()
        .map_err(|_| anyhow::anyhow!("unsupported --day value `{day}`; use YYYY-MM-DD"))?;
    let day_of_month = parts[2]
        .parse::<u32>()
        .map_err(|_| anyhow::anyhow!("unsupported --day value `{day}`; use YYYY-MM-DD"))?;

    let max_day = days_in_month(year, month)?;
    if day_of_month == 0 || day_of_month > max_day {
        bail!("unsupported --day value `{day}`; use YYYY-MM-DD");
    }

    Ok((year, month, day_of_month))
}

fn days_in_month(year: u32, month: u32) -> Result<u32> {
    let days = match month {
        1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
        4 | 6 | 9 | 11 => 30,
        2 if is_leap_year(year) => 29,
        2 => 28,
        _ => bail!("unsupported month `{month}` in --day value"),
    };
    Ok(days)
}

fn is_leap_year(year: u32) -> bool {
    year.is_multiple_of(4) && !year.is_multiple_of(100) || year.is_multiple_of(400)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn next_calendar_day_handles_month_year_and_leap_boundaries() {
        assert_eq!(next_calendar_day("2026-04-13").unwrap(), "2026-04-14");
        assert_eq!(next_calendar_day("2026-04-30").unwrap(), "2026-05-01");
        assert_eq!(next_calendar_day("2026-12-31").unwrap(), "2027-01-01");
        assert_eq!(next_calendar_day("2024-02-28").unwrap(), "2024-02-29");
        assert_eq!(next_calendar_day("2024-02-29").unwrap(), "2024-03-01");
    }
}