Skip to main content

systemprompt_cli/commands/analytics/shared/
time.rs

1use anyhow::{anyhow, Result};
2use chrono::{DateTime, Datelike, Duration, NaiveDate, Timelike, Utc};
3
4pub fn parse_duration(s: &str) -> Option<Duration> {
5    let s = s.trim().to_lowercase();
6
7    s.strip_suffix('d')
8        .and_then(|d| d.parse::<i64>().ok())
9        .map(Duration::days)
10        .or_else(|| {
11            s.strip_suffix('h')
12                .and_then(|h| h.parse::<i64>().ok())
13                .map(Duration::hours)
14        })
15        .or_else(|| {
16            s.strip_suffix('m')
17                .and_then(|m| m.parse::<i64>().ok())
18                .map(Duration::minutes)
19        })
20        .or_else(|| {
21            s.strip_suffix('w')
22                .and_then(|w| w.parse::<i64>().ok())
23                .map(Duration::weeks)
24        })
25}
26
27pub fn parse_since(since: Option<&String>) -> Result<Option<DateTime<Utc>>> {
28    let Some(s) = since else {
29        return Ok(None);
30    };
31
32    let s = s.trim().to_lowercase();
33
34    if let Some(duration) = parse_duration(&s) {
35        return Ok(Some(Utc::now() - duration));
36    }
37
38    if let Ok(date) = NaiveDate::parse_from_str(&s, "%Y-%m-%d") {
39        return date
40            .and_hms_opt(0, 0, 0)
41            .map(|naive| DateTime::from_naive_utc_and_offset(naive, Utc))
42            .map(Some)
43            .ok_or_else(|| anyhow!("Invalid date: {}", s));
44    }
45
46    chrono::NaiveDateTime::parse_from_str(&s, "%Y-%m-%dT%H:%M:%S")
47        .map(|naive| Some(DateTime::from_naive_utc_and_offset(naive, Utc)))
48        .map_err(|_| {
49            anyhow!(
50                "Invalid --since format: {}. Use '1h', '24h', '7d', '2026-01-13', or \
51                 '2026-01-13T10:00:00'",
52                s
53            )
54        })
55}
56
57pub fn parse_until(until: Option<&String>) -> Result<Option<DateTime<Utc>>> {
58    let Some(s) = until else {
59        return Ok(None);
60    };
61
62    let s = s.trim().to_lowercase();
63
64    if let Some(duration) = parse_duration(&s) {
65        return Ok(Some(Utc::now() - duration));
66    }
67
68    if let Ok(date) = NaiveDate::parse_from_str(&s, "%Y-%m-%d") {
69        return date
70            .and_hms_opt(23, 59, 59)
71            .map(|naive| DateTime::from_naive_utc_and_offset(naive, Utc))
72            .map(Some)
73            .ok_or_else(|| anyhow!("Invalid date: {}", s));
74    }
75
76    chrono::NaiveDateTime::parse_from_str(&s, "%Y-%m-%dT%H:%M:%S")
77        .map(|naive| Some(DateTime::from_naive_utc_and_offset(naive, Utc)))
78        .map_err(|_| {
79            anyhow!(
80                "Invalid --until format: {}. Use '1h', '24h', '7d', '2026-01-13', or \
81                 '2026-01-13T10:00:00'",
82                s
83            )
84        })
85}
86
87pub fn parse_time_range(
88    since: Option<&String>,
89    until: Option<&String>,
90) -> Result<(DateTime<Utc>, DateTime<Utc>)> {
91    let start = parse_since(since)?.unwrap_or_else(|| Utc::now() - Duration::hours(24));
92    let end = parse_until(until)?.unwrap_or_else(Utc::now);
93    Ok((start, end))
94}
95
96pub fn truncate_to_period(dt: DateTime<Utc>, period: &str) -> DateTime<Utc> {
97    match period {
98        "hour" => dt
99            .date_naive()
100            .and_hms_opt(dt.time().hour(), 0, 0)
101            .map_or(dt, |naive| DateTime::from_naive_utc_and_offset(naive, Utc)),
102        "day" => dt
103            .date_naive()
104            .and_hms_opt(0, 0, 0)
105            .map_or(dt, |naive| DateTime::from_naive_utc_and_offset(naive, Utc)),
106        "week" => {
107            let days_since_monday = dt.weekday().num_days_from_monday();
108            (dt.date_naive() - Duration::days(i64::from(days_since_monday)))
109                .and_hms_opt(0, 0, 0)
110                .map_or(dt, |naive| DateTime::from_naive_utc_and_offset(naive, Utc))
111        },
112        "month" => dt
113            .date_naive()
114            .with_day(1)
115            .and_then(|d: NaiveDate| d.and_hms_opt(0, 0, 0))
116            .map_or(dt, |naive| DateTime::from_naive_utc_and_offset(naive, Utc)),
117        _ => dt,
118    }
119}
120
121pub fn format_duration_ms(ms: i64) -> String {
122    match ms {
123        ms if ms < 1000 => format!("{}ms", ms),
124        ms if ms < 60_000 => format!("{:.1}s", ms as f64 / 1000.0),
125        ms if ms < 3_600_000 => format!("{:.1}m", ms as f64 / 60_000.0),
126        _ => format!("{:.1}h", ms as f64 / 3_600_000.0),
127    }
128}
129
130pub fn format_timestamp(dt: DateTime<Utc>) -> String {
131    dt.format("%Y-%m-%d %H:%M:%S").to_string()
132}
133
134pub fn format_period_label(dt: DateTime<Utc>, period: &str) -> String {
135    match period {
136        "hour" => dt.format("%Y-%m-%d %H:00").to_string(),
137        "day" => dt.format("%Y-%m-%d").to_string(),
138        "week" => format!("Week of {}", dt.format("%Y-%m-%d")),
139        "month" => dt.format("%Y-%m").to_string(),
140        _ => dt.format("%Y-%m-%d %H:%M:%S").to_string(),
141    }
142}