use chrono::{DateTime, Duration, Utc};
pub fn parse_relative(s: &str) -> Option<DateTime<Utc>> {
let s = s.trim();
if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
return Some(dt.with_timezone(&Utc));
}
parse_relative_duration(s)
}
fn parse_relative_duration(s: &str) -> Option<DateTime<Utc>> {
let s = s.strip_suffix("ago")?.trim_end();
let (num_str, unit_str) = s.split_once(char::is_whitespace)?;
let n: i64 = num_str.trim().parse().ok()?;
let unit = unit_str.trim();
let duration = match unit {
"second" | "seconds" => Duration::seconds(n),
"minute" | "minutes" => Duration::minutes(n),
"hour" | "hours" => Duration::hours(n),
"day" | "days" => Duration::days(n),
"week" | "weeks" => Duration::weeks(n),
"month" | "months" => Duration::days(n * 30),
"year" | "years" => Duration::days(n * 365),
_ => return None,
};
Some(Utc::now() - duration)
}
pub fn to_iso(dt: DateTime<Utc>) -> String {
dt.format("%Y-%m-%dT%H:%M:%SZ").to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::Duration;
fn approx_eq(a: DateTime<Utc>, b: DateTime<Utc>, tolerance_secs: i64) -> bool {
let diff = (a - b).num_seconds().abs();
diff <= tolerance_secs
}
#[test]
fn parses_6_months_ago() {
let result = parse_relative("6 months ago").expect("should parse");
let expected = Utc::now() - Duration::days(180);
assert!(
approx_eq(result, expected, 2),
"result={result} expected≈{expected}"
);
}
#[test]
fn parses_1_hour_ago() {
let result = parse_relative("1 hour ago").expect("should parse");
let expected = Utc::now() - Duration::hours(1);
assert!(approx_eq(result, expected, 2));
}
#[test]
fn parses_1_year_ago() {
let result = parse_relative("1 year ago").expect("should parse");
let expected = Utc::now() - Duration::days(365);
assert!(approx_eq(result, expected, 2));
}
#[test]
fn parses_30_days_ago() {
let result = parse_relative("30 days ago").expect("should parse");
let expected = Utc::now() - Duration::days(30);
assert!(approx_eq(result, expected, 2));
}
#[test]
fn parses_2_weeks_ago() {
let result = parse_relative("2 weeks ago").expect("should parse");
let expected = Utc::now() - Duration::weeks(2);
assert!(approx_eq(result, expected, 2));
}
#[test]
fn parses_iso_absolute() {
let result = parse_relative("2026-01-01T00:00:00Z").expect("should parse");
let expected: DateTime<Utc> = "2026-01-01T00:00:00Z".parse().unwrap();
assert_eq!(result, expected);
}
#[test]
fn returns_none_for_garbage() {
assert!(parse_relative("not a date").is_none());
assert!(parse_relative("").is_none());
assert!(parse_relative("yesterday").is_none());
}
#[test]
fn to_iso_formats_correctly() {
let dt: DateTime<Utc> = "2026-01-15T09:30:00Z".parse().unwrap();
assert_eq!(to_iso(dt), "2026-01-15T09:30:00Z");
}
}