Factor out parse_since_from(input, reference_ms) so callers can compute relative durations against a fixed reference timestamp instead of always using now(). The existing parse_since() now delegates to it with now_ms(). Enables testable and reproducible time-relative queries for features like timeline --as-of and who --as-of. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
137 lines
3.8 KiB
Rust
137 lines
3.8 KiB
Rust
use chrono::{DateTime, Utc};
|
|
|
|
pub fn iso_to_ms(iso_string: &str) -> Option<i64> {
|
|
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<i64> {
|
|
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<i64> {
|
|
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<i64, String> {
|
|
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<String>) -> Result<Option<i64>, 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"));
|
|
}
|
|
}
|