mps-rs 1.6.2

MPS — plain-text personal productivity CLI (Rust)
Documentation
use crate::error::MpsError;
use chrono::{Datelike, Duration, Local, NaiveDate, Weekday};

/// Parse a natural-language or YYYYMMDD date string into a NaiveDate.
/// Supported: today, yesterday, YYYYMMDD, last <weekday>, <N> days ago,
///            last week, monday/tuesday/.../sunday (means last occurrence).
pub fn parse_date(input: &str) -> Result<NaiveDate, MpsError> {
    let s = input.trim().to_lowercase();
    let today = Local::now().date_naive();

    match s.as_str() {
        "today" => return Ok(today),
        "yesterday" => return Ok(today - Duration::days(1)),
        "last week" => return Ok(today - Duration::days(7)),
        _ => {}
    }

    // "N days ago"
    if let Some(rest) = s.strip_suffix(" days ago") {
        if let Ok(n) = rest.trim().parse::<i64>() {
            return Ok(today - Duration::days(n));
        }
    }
    if let Some(rest) = s.strip_suffix(" day ago") {
        if rest.trim() == "1" || rest.trim() == "a" {
            return Ok(today - Duration::days(1));
        }
    }

    // "last <weekday>"
    if let Some(rest) = s.strip_prefix("last ") {
        if let Some(wd) = parse_weekday(rest.trim()) {
            return Ok(last_weekday(today, wd));
        }
    }

    // bare weekday name → last occurrence (not including today)
    if let Some(wd) = parse_weekday(&s) {
        return Ok(last_weekday(today, wd));
    }

    // YYYYMMDD
    if s.len() == 8 && s.chars().all(|c| c.is_ascii_digit()) {
        return NaiveDate::parse_from_str(&s, "%Y%m%d")
            .map_err(|_| MpsError::DateParseError(input.to_string()));
    }

    // YYYY-MM-DD
    if s.len() == 10 && s.chars().nth(4) == Some('-') {
        return NaiveDate::parse_from_str(&s, "%Y-%m-%d")
            .map_err(|_| MpsError::DateParseError(input.to_string()));
    }

    Err(MpsError::DateParseError(input.to_string()))
}

fn parse_weekday(s: &str) -> Option<Weekday> {
    match s {
        "monday" | "mon" => Some(Weekday::Mon),
        "tuesday" | "tue" => Some(Weekday::Tue),
        "wednesday" | "wed" => Some(Weekday::Wed),
        "thursday" | "thu" => Some(Weekday::Thu),
        "friday" | "fri" => Some(Weekday::Fri),
        "saturday" | "sat" => Some(Weekday::Sat),
        "sunday" | "sun" => Some(Weekday::Sun),
        _ => None,
    }
}

/// Most recent past occurrence of weekday (never today).
fn last_weekday(today: NaiveDate, wd: Weekday) -> NaiveDate {
    let mut d = today - Duration::days(1);
    while d.weekday() != wd {
        d -= Duration::days(1);
    }
    d
}

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

    #[test]
    fn test_today() {
        assert_eq!(parse_date("today").unwrap(), Local::now().date_naive());
    }

    #[test]
    fn test_yesterday() {
        let expected = Local::now().date_naive() - Duration::days(1);
        assert_eq!(parse_date("yesterday").unwrap(), expected);
    }

    #[test]
    fn test_yyyymmdd() {
        assert_eq!(
            parse_date("20260428").unwrap(),
            NaiveDate::from_ymd_opt(2026, 4, 28).unwrap()
        );
    }

    #[test]
    fn test_yyyy_mm_dd() {
        assert_eq!(
            parse_date("2026-04-28").unwrap(),
            NaiveDate::from_ymd_opt(2026, 4, 28).unwrap()
        );
    }

    #[test]
    fn test_n_days_ago() {
        let expected = Local::now().date_naive() - Duration::days(3);
        assert_eq!(parse_date("3 days ago").unwrap(), expected);
    }

    #[test]
    fn test_last_week() {
        let expected = Local::now().date_naive() - Duration::days(7);
        assert_eq!(parse_date("last week").unwrap(), expected);
    }

    #[test]
    fn test_invalid() {
        assert!(parse_date("gibberish date string").is_err());
    }
}