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