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