use chrono::{DateTime, Utc}; pub fn iso_to_ms(iso_string: &str) -> Option { DateTime::parse_from_rfc3339(iso_string) .ok() .map(|dt| dt.timestamp_millis()) } pub fn ms_to_iso(ms: i64) -> String { DateTime::from_timestamp_millis(ms) .map(|dt| dt.to_rfc3339()) .unwrap_or_else(|| "Invalid timestamp".to_string()) } pub fn now_ms() -> i64 { Utc::now().timestamp_millis() } pub fn parse_since(input: &str) -> Option { parse_since_from(input, now_ms()) } /// Like `parse_since` but durations are relative to `reference_ms` instead of now. /// Absolute dates/timestamps are returned as-is regardless of `reference_ms`. pub fn parse_since_from(input: &str, reference_ms: i64) -> Option { let input = input.trim(); if let Some(num_str) = input.strip_suffix('d') { let days: i64 = num_str.parse().ok()?; return Some(reference_ms - (days * 24 * 60 * 60 * 1000)); } if let Some(num_str) = input.strip_suffix('w') { let weeks: i64 = num_str.parse().ok()?; return Some(reference_ms - (weeks * 7 * 24 * 60 * 60 * 1000)); } if let Some(num_str) = input.strip_suffix('m') { let months: i64 = num_str.parse().ok()?; return Some(reference_ms - (months * 30 * 24 * 60 * 60 * 1000)); } if input.len() == 10 && input.chars().filter(|&c| c == '-').count() == 2 { let iso_full = format!("{input}T00:00:00Z"); return iso_to_ms(&iso_full); } iso_to_ms(input) } pub fn iso_to_ms_strict(iso_string: &str) -> Result { DateTime::parse_from_rfc3339(iso_string) .map(|dt| dt.timestamp_millis()) .map_err(|_| format!("Invalid timestamp: {}", iso_string)) } pub fn iso_to_ms_opt_strict(iso_string: &Option) -> Result, String> { match iso_string { Some(s) => iso_to_ms_strict(s).map(Some), None => Ok(None), } } pub fn format_full_datetime(ms: i64) -> String { DateTime::from_timestamp_millis(ms) .map(|dt| dt.format("%Y-%m-%d %H:%M UTC").to_string()) .unwrap_or_else(|| "Unknown".to_string()) } #[cfg(test)] mod tests { use super::*; #[test] fn test_iso_to_ms() { let ms = iso_to_ms("2024-01-15T10:30:00Z").unwrap(); assert!(ms > 0); } #[test] fn test_ms_to_iso() { let iso = ms_to_iso(1705315800000); assert!(iso.contains("2024-01-15")); } #[test] fn test_now_ms() { let now = now_ms(); assert!(now > 1700000000000); } #[test] fn test_parse_since_days() { let now = now_ms(); let seven_days = parse_since("7d").unwrap(); let expected = now - (7 * 24 * 60 * 60 * 1000); assert!((seven_days - expected).abs() < 1000); } #[test] fn test_parse_since_weeks() { let now = now_ms(); let two_weeks = parse_since("2w").unwrap(); let expected = now - (14 * 24 * 60 * 60 * 1000); assert!((two_weeks - expected).abs() < 1000); } #[test] fn test_parse_since_months() { let now = now_ms(); let one_month = parse_since("1m").unwrap(); let expected = now - (30 * 24 * 60 * 60 * 1000); assert!((one_month - expected).abs() < 1000); } #[test] fn test_parse_since_iso_date() { let ms = parse_since("2024-01-15").unwrap(); assert!(ms > 0); let expected = iso_to_ms("2024-01-15T00:00:00Z").unwrap(); assert_eq!(ms, expected); } #[test] fn test_parse_since_invalid() { assert!(parse_since("invalid").is_none()); assert!(parse_since("").is_none()); } #[test] fn test_format_full_datetime() { let dt = format_full_datetime(1705315800000); assert!(dt.contains("2024-01-15")); assert!(dt.contains("UTC")); } }