use chrono::{Datelike, Local, NaiveDate};
pub fn parse_fuzzy_date(input: &str) -> Option<NaiveDate> {
let today = Local::now().date_naive();
let s = input.trim().to_lowercase();
if let Ok(d) = NaiveDate::parse_from_str(&s, "%Y-%m-%d") {
return Some(d);
}
match s.as_str() {
"今日" | "today" => Some(today),
"明日" | "tomorrow" => Some(today + chrono::Duration::days(1)),
"明後日" => Some(today + chrono::Duration::days(2)),
"来週" | "next week" => Some(today + chrono::Duration::days(7)),
"来月" | "next month" => {
let (y, m) = if today.month() == 12 {
(today.year() + 1, 1)
} else {
(today.year(), today.month() + 1)
};
let d = today.day().min(days_in_month(y, m));
NaiveDate::from_ymd_opt(y, m, d)
}
"月曜日" | "月曜" | "月" | "mon" | "monday" => {
Some(next_weekday(today, chrono::Weekday::Mon))
}
"火曜日" | "火曜" | "火" | "tue" | "tuesday" => {
Some(next_weekday(today, chrono::Weekday::Tue))
}
"水曜日" | "水曜" | "水" | "wed" | "wednesday" => {
Some(next_weekday(today, chrono::Weekday::Wed))
}
"木曜日" | "木曜" | "木" | "thu" | "thursday" => {
Some(next_weekday(today, chrono::Weekday::Thu))
}
"金曜日" | "金曜" | "金" | "fri" | "friday" => {
Some(next_weekday(today, chrono::Weekday::Fri))
}
"土曜日" | "土曜" | "土" | "sat" | "saturday" => {
Some(next_weekday(today, chrono::Weekday::Sat))
}
"日曜日" | "日曜" | "日" | "sun" | "sunday" => {
Some(next_weekday(today, chrono::Weekday::Sun))
}
_ => None,
}
}
fn next_weekday(from: NaiveDate, target: chrono::Weekday) -> NaiveDate {
let current = from.weekday().num_days_from_monday();
let target_num = target.num_days_from_monday();
let days_ahead = if target_num <= current {
7 - (current - target_num)
} else {
target_num - current
};
from + chrono::Duration::days(days_ahead as i64)
}
fn days_in_month(year: i32, month: u32) -> u32 {
let next = if month == 12 {
NaiveDate::from_ymd_opt(year + 1, 1, 1)
} else {
NaiveDate::from_ymd_opt(year, month + 1, 1)
};
next.unwrap().pred_opt().unwrap().day()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_yyyy_mm_dd() {
let d = parse_fuzzy_date("2026-04-15").unwrap();
assert_eq!(d, NaiveDate::from_ymd_opt(2026, 4, 15).unwrap());
}
#[test]
fn test_parse_today() {
let today = Local::now().date_naive();
assert_eq!(parse_fuzzy_date("今日").unwrap(), today);
assert_eq!(parse_fuzzy_date("today").unwrap(), today);
}
#[test]
fn test_parse_tomorrow() {
let tomorrow = Local::now().date_naive() + chrono::Duration::days(1);
assert_eq!(parse_fuzzy_date("明日").unwrap(), tomorrow);
assert_eq!(parse_fuzzy_date("tomorrow").unwrap(), tomorrow);
}
#[test]
fn test_parse_day_after_tomorrow() {
let dat = Local::now().date_naive() + chrono::Duration::days(2);
assert_eq!(parse_fuzzy_date("明後日").unwrap(), dat);
}
#[test]
fn test_parse_next_week() {
let nw = Local::now().date_naive() + chrono::Duration::days(7);
assert_eq!(parse_fuzzy_date("来週").unwrap(), nw);
assert_eq!(parse_fuzzy_date("next week").unwrap(), nw);
}
#[test]
fn test_parse_next_month() {
let today = Local::now().date_naive();
let result = parse_fuzzy_date("来月").unwrap();
if today.month() == 12 {
assert_eq!(result.year(), today.year() + 1);
assert_eq!(result.month(), 1);
} else {
assert_eq!(result.month(), today.month() + 1);
}
}
#[test]
fn test_parse_weekday() {
let today = Local::now().date_naive();
let fri = parse_fuzzy_date("金曜").unwrap();
assert_eq!(fri.weekday(), chrono::Weekday::Fri);
assert!(fri > today);
assert!((fri - today).num_days() <= 7);
let mon = parse_fuzzy_date("mon").unwrap();
assert_eq!(mon.weekday(), chrono::Weekday::Mon);
assert!(mon > today);
}
#[test]
fn test_parse_weekday_with_nichi_suffix() {
let today = Local::now().date_naive();
let mon = parse_fuzzy_date("月曜日").unwrap();
assert_eq!(mon.weekday(), chrono::Weekday::Mon);
assert!(mon > today);
assert!((mon - today).num_days() <= 7);
let wed = parse_fuzzy_date("水曜日").unwrap();
assert_eq!(wed.weekday(), chrono::Weekday::Wed);
assert!(wed > today);
let sun = parse_fuzzy_date("日曜日").unwrap();
assert_eq!(sun.weekday(), chrono::Weekday::Sun);
assert!(sun > today);
}
#[test]
fn test_parse_invalid() {
assert!(parse_fuzzy_date("abc").is_none());
assert!(parse_fuzzy_date("").is_none());
}
}