use anyhow::{anyhow, Result};
use chrono::{DateTime, Duration, Utc};
pub fn parse_time_expression(expr: &str) -> Result<String> {
if let Some(result) = try_parse_relative(expr)? {
return Ok(result);
}
if let Ok(dt) = DateTime::parse_from_rfc3339(expr) {
return Ok(dt.to_rfc3339());
}
if let Ok(date) = chrono::NaiveDate::parse_from_str(expr, "%Y-%m-%d") {
let datetime = date
.and_hms_opt(0, 0, 0)
.ok_or_else(|| anyhow!("Invalid date: {}", expr))?;
let utc_datetime = DateTime::<Utc>::from_naive_utc_and_offset(datetime, Utc);
return Ok(utc_datetime.to_rfc3339());
}
Err(anyhow!(
"Invalid time expression: '{}'\n\
Supported formats:\n\
- Relative: 24h, 7d, 1w, 30d\n\
- ISO8601 with timezone: 2024-01-01T10:00:00Z\n\
- ISO8601 date only: 2024-01-01 (assumes 00:00:00 UTC)",
expr
))
}
fn try_parse_relative(expr: &str) -> Result<Option<String>> {
let expr = expr.trim();
if expr.len() < 2 {
return Ok(None);
}
let unit = expr.chars().last().unwrap();
let value_str = &expr[..expr.len() - 1];
if !matches!(unit, 'h' | 'd' | 'w') {
return Ok(None);
}
let value: i64 = value_str.parse().map_err(|_| {
anyhow!(
"Invalid relative duration: '{}' - numeric part must be an integer",
expr
)
})?;
if value <= 0 {
return Err(anyhow!(
"Invalid relative duration: '{}' - duration must be positive (greater than 0)",
expr
));
}
let duration = match unit {
'h' => Duration::hours(value),
'd' => Duration::days(value),
'w' => Duration::weeks(value),
_ => unreachable!(), };
let now = Utc::now();
let target = now - duration;
Ok(Some(target.to_rfc3339()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_relative_hours() {
let result = parse_time_expression("24h");
assert!(result.is_ok());
assert!(DateTime::parse_from_rfc3339(&result.unwrap()).is_ok());
}
#[test]
fn test_parse_relative_days() {
let result = parse_time_expression("7d");
assert!(result.is_ok());
assert!(DateTime::parse_from_rfc3339(&result.unwrap()).is_ok());
}
#[test]
fn test_parse_relative_weeks() {
let result = parse_time_expression("2w");
assert!(result.is_ok());
assert!(DateTime::parse_from_rfc3339(&result.unwrap()).is_ok());
}
#[test]
fn test_parse_iso8601_with_timezone() {
let result = parse_time_expression("2024-01-01T10:00:00Z").unwrap();
assert_eq!(result, "2024-01-01T10:00:00+00:00");
}
#[test]
fn test_parse_iso8601_with_offset() {
let result = parse_time_expression("2024-01-01T10:00:00+08:00").unwrap();
assert!(result.contains("2024-01-01T10:00:00+08:00"));
}
#[test]
fn test_parse_date_only_assumes_utc_midnight() {
let result = parse_time_expression("2024-01-01").unwrap();
assert_eq!(result, "2024-01-01T00:00:00+00:00");
}
#[test]
fn test_reject_zero_duration() {
let result = parse_time_expression("0h");
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("must be positive"));
}
#[test]
fn test_reject_negative_duration() {
let result = parse_time_expression("-1d");
assert!(result.is_err());
}
#[test]
fn test_reject_invalid_format() {
let result = parse_time_expression("not-a-time");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Invalid time expression"));
}
#[test]
fn test_reject_invalid_unit() {
let result = parse_time_expression("5m");
assert!(result.is_err());
}
#[test]
fn test_reject_non_numeric_value() {
let result = parse_time_expression("abch");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("numeric part must be an integer"));
}
#[test]
fn test_empty_string() {
let result = parse_time_expression("");
assert!(result.is_err());
}
#[test]
fn test_whitespace_handling() {
let result = parse_time_expression(" 24h ");
assert!(result.is_ok());
}
#[test]
fn test_large_duration() {
let result = parse_time_expression("365d");
assert!(result.is_ok());
assert!(DateTime::parse_from_rfc3339(&result.unwrap()).is_ok());
}
#[test]
fn test_relative_duration_calculation() {
let result = parse_time_expression("1h").unwrap();
let parsed = DateTime::parse_from_rfc3339(&result).unwrap();
let now = Utc::now();
let diff = now.signed_duration_since(parsed);
assert!(diff.num_hours() >= 0);
assert!(diff.num_hours() <= 1);
assert!(diff.num_seconds() >= 3599);
assert!(diff.num_seconds() <= 3601);
}
}