Files
gitlore/src/core/time.rs
Taylor Eernisse 9786ef27f5 refactor(core/time): extract parse_since_from for deterministic time parsing
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>
2026-02-13 10:54:20 -05:00

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"));
}
}