systemprompt_cli/commands/analytics/shared/
time.rs1use 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 Ok(date) = NaiveDate::parse_from_str(&s, "%Y-%m-%d") {
65 return date
66 .and_hms_opt(23, 59, 59)
67 .map(|naive| DateTime::from_naive_utc_and_offset(naive, Utc))
68 .map(Some)
69 .ok_or_else(|| anyhow!("Invalid date: {}", s));
70 }
71
72 chrono::NaiveDateTime::parse_from_str(&s, "%Y-%m-%dT%H:%M:%S")
73 .map(|naive| Some(DateTime::from_naive_utc_and_offset(naive, Utc)))
74 .map_err(|_| {
75 anyhow!(
76 "Invalid --until format: {}. Use '2026-01-13' or '2026-01-13T10:00:00'",
77 s
78 )
79 })
80}
81
82pub fn parse_time_range(
83 since: Option<&String>,
84 until: Option<&String>,
85) -> Result<(DateTime<Utc>, DateTime<Utc>)> {
86 let start = parse_since(since)?.unwrap_or_else(|| Utc::now() - Duration::hours(24));
87 let end = parse_until(until)?.unwrap_or_else(Utc::now);
88 Ok((start, end))
89}
90
91pub fn truncate_to_period(dt: DateTime<Utc>, period: &str) -> DateTime<Utc> {
92 match period {
93 "hour" => dt
94 .date_naive()
95 .and_hms_opt(dt.time().hour(), 0, 0)
96 .map_or(dt, |naive| DateTime::from_naive_utc_and_offset(naive, Utc)),
97 "day" => dt
98 .date_naive()
99 .and_hms_opt(0, 0, 0)
100 .map_or(dt, |naive| DateTime::from_naive_utc_and_offset(naive, Utc)),
101 "week" => {
102 let days_since_monday = dt.weekday().num_days_from_monday();
103 (dt.date_naive() - Duration::days(i64::from(days_since_monday)))
104 .and_hms_opt(0, 0, 0)
105 .map_or(dt, |naive| DateTime::from_naive_utc_and_offset(naive, Utc))
106 },
107 "month" => dt
108 .date_naive()
109 .with_day(1)
110 .and_then(|d: NaiveDate| d.and_hms_opt(0, 0, 0))
111 .map_or(dt, |naive| DateTime::from_naive_utc_and_offset(naive, Utc)),
112 _ => dt,
113 }
114}
115
116pub fn format_duration_ms(ms: i64) -> String {
117 match ms {
118 ms if ms < 1000 => format!("{}ms", ms),
119 ms if ms < 60_000 => format!("{:.1}s", ms as f64 / 1000.0),
120 ms if ms < 3_600_000 => format!("{:.1}m", ms as f64 / 60_000.0),
121 _ => format!("{:.1}h", ms as f64 / 3_600_000.0),
122 }
123}
124
125pub fn format_timestamp(dt: DateTime<Utc>) -> String {
126 dt.format("%Y-%m-%d %H:%M:%S").to_string()
127}
128
129pub fn format_period_label(dt: DateTime<Utc>, period: &str) -> String {
130 match period {
131 "hour" => dt.format("%Y-%m-%d %H:00").to_string(),
132 "day" => dt.format("%Y-%m-%d").to_string(),
133 "week" => format!("Week of {}", dt.format("%Y-%m-%d")),
134 "month" => dt.format("%Y-%m").to_string(),
135 _ => dt.format("%Y-%m-%d %H:%M:%S").to_string(),
136 }
137}