coding_agent_search/analytics/
bucketing.rs1use crate::storage::sqlite::FrankenStorage;
7
8use super::types::AnalyticsFilter;
9
10pub fn day_id_to_iso(day_id: i64) -> String {
12 use chrono::{TimeZone, Utc};
13 let ms = FrankenStorage::millis_from_day_id(day_id);
14 Utc.timestamp_millis_opt(ms)
15 .single()
16 .map(|dt| dt.format("%Y-%m-%d").to_string())
17 .unwrap_or_else(|| format!("day:{day_id}"))
18}
19
20pub fn hour_id_to_iso(hour_id: i64) -> String {
22 use chrono::{TimeZone, Utc};
23 let ms = FrankenStorage::millis_from_hour_id(hour_id);
24 Utc.timestamp_millis_opt(ms)
25 .single()
26 .map(|dt| dt.format("%Y-%m-%dT%H:00Z").to_string())
27 .unwrap_or_else(|| format!("hour:{hour_id}"))
28}
29
30pub fn day_id_to_iso_week(day_id: i64) -> String {
32 use chrono::{Datelike, TimeZone, Utc};
33 let ms = FrankenStorage::millis_from_day_id(day_id);
34 Utc.timestamp_millis_opt(ms)
35 .single()
36 .map(|dt| {
37 let iso = dt.iso_week();
38 format!("{}-W{:02}", iso.year(), iso.week())
39 })
40 .unwrap_or_else(|| format!("day:{day_id}"))
41}
42
43pub fn day_id_to_month(day_id: i64) -> String {
45 use chrono::{TimeZone, Utc};
46 let ms = FrankenStorage::millis_from_day_id(day_id);
47 Utc.timestamp_millis_opt(ms)
48 .single()
49 .map(|dt| dt.format("%Y-%m").to_string())
50 .unwrap_or_else(|| format!("day:{day_id}"))
51}
52
53pub fn resolve_day_range(filter: &AnalyticsFilter) -> (Option<i64>, Option<i64>) {
57 let min_day = filter.since_ms.map(FrankenStorage::day_id_from_millis);
58 let max_day = filter.until_ms.map(FrankenStorage::day_id_from_millis);
59 (min_day, max_day)
60}
61
62pub fn resolve_hour_range(filter: &AnalyticsFilter) -> (Option<i64>, Option<i64>) {
65 let min_hour = filter.since_ms.map(FrankenStorage::hour_id_from_millis);
66 let max_hour = filter.until_ms.map(FrankenStorage::hour_id_from_millis);
67 (min_hour, max_hour)
68}
69
70#[cfg(test)]
75mod tests {
76 use super::*;
77
78 #[test]
82 fn day_id_roundtrip() {
83 let ms = 1_749_945_600_000_i64;
85 let day_id = FrankenStorage::day_id_from_millis(ms);
86 let iso = day_id_to_iso(day_id);
87 assert_eq!(iso, "2025-06-15");
88 }
89
90 #[test]
91 fn hour_id_roundtrip() {
92 let ms = 1_749_996_000_000_i64;
94 let hour_id = FrankenStorage::hour_id_from_millis(ms);
95 let iso = hour_id_to_iso(hour_id);
96 assert_eq!(iso, "2025-06-15T14:00Z");
97 }
98
99 #[test]
100 fn day_id_to_week() {
101 let mon_ms = 1_736_121_600_000_i64; let sun_ms = 1_736_640_000_000_i64; let mon_id = FrankenStorage::day_id_from_millis(mon_ms);
105 let sun_id = FrankenStorage::day_id_from_millis(sun_ms);
106 assert_eq!(day_id_to_iso_week(mon_id), day_id_to_iso_week(sun_id));
107 assert!(day_id_to_iso_week(mon_id).contains("W02"));
108 }
109
110 #[test]
111 fn day_id_to_month_format() {
112 let ms = 1_750_032_000_000_i64; let day_id = FrankenStorage::day_id_from_millis(ms);
114 assert_eq!(day_id_to_month(day_id), "2025-06");
115 }
116
117 #[test]
118 fn resolve_empty_filter_gives_none() {
119 let f = AnalyticsFilter::default();
120 let (min, max) = resolve_day_range(&f);
121 assert!(min.is_none());
122 assert!(max.is_none());
123 let (hmin, hmax) = resolve_hour_range(&f);
124 assert!(hmin.is_none());
125 assert!(hmax.is_none());
126 }
127
128 #[test]
129 fn resolve_day_range_with_since() {
130 let f = AnalyticsFilter {
131 since_ms: Some(1_750_032_000_000),
132 ..Default::default()
133 };
134 let (min, max) = resolve_day_range(&f);
135 assert!(min.is_some());
136 assert!(max.is_none());
137 }
138}