1use std::time::SystemTime;
2
3use chrono::{Datelike, Duration, NaiveDate, Timelike, Utc};
4use serde::{Deserialize, Serialize};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
8pub enum PartitionGranularity {
9 #[serde(rename = "5min")]
10 FiveMin,
11 #[serde(rename = "15min")]
12 FifteenMin,
13 #[serde(rename = "30min")]
14 ThirtyMin,
15 #[serde(rename = "hour")]
16 Hour,
17 #[serde(rename = "day")]
18 Day,
19 #[serde(rename = "month")]
20 Month,
21 #[serde(rename = "year")]
22 Year,
23}
24
25impl PartitionGranularity {
26 pub fn approx_seconds(self) -> u64 {
27 match self {
28 Self::FiveMin => 5 * 60,
29 Self::FifteenMin => 15 * 60,
30 Self::ThirtyMin => 30 * 60,
31 Self::Hour => 3600,
32 Self::Day => 86400,
33 Self::Month => 31 * 86400,
34 Self::Year => 366 * 86400,
35 }
36 }
37
38 fn approx_minutes(self) -> u32 {
39 (self.approx_seconds() / 60) as u32
40 }
41}
42
43pub fn partition_name(ts: SystemTime, granularity: PartitionGranularity) -> String {
53 let dt = chrono::DateTime::<Utc>::from(ts);
54 let y = dt.year();
55 let m = dt.month();
56 let d = dt.day();
57 let h = dt.hour();
58 let min = dt.minute();
59
60 match granularity {
61 PartitionGranularity::Year => format!("{y}"),
62 PartitionGranularity::Month => format!("{y}_{m:02}"),
63 PartitionGranularity::Day => format!("{y}_{m:02}_{d:02}"),
64 PartitionGranularity::Hour => format!("{y}_{m:02}_{d:02}_{h:02}"),
65 PartitionGranularity::FiveMin
66 | PartitionGranularity::FifteenMin
67 | PartitionGranularity::ThirtyMin => {
68 let approx_min = granularity.approx_minutes();
69 let start_min = (min / approx_min) * approx_min;
70 format!("{y}_{m:02}_{d:02}_{h:02}_{start_min:02}")
71 }
72 }
73}
74
75pub fn partitions_in_range(
77 start: SystemTime,
78 end: SystemTime,
79 granularity: PartitionGranularity,
80) -> Vec<String> {
81 let mut names = Vec::new();
82 let mut current = start;
83 loop {
84 let name = partition_name(current, granularity);
85 if names.last().map(|n: &String| n.as_str()) != Some(&name) {
86 names.push(name);
87 }
88 if current >= end {
89 break;
90 }
91 current = next_partition_start(current, granularity);
92 if current > end {
93 let name = partition_name(end, granularity);
95 if names.last().map(|n: &String| n.as_str()) != Some(&name) {
96 names.push(name);
97 }
98 break;
99 }
100 }
101 names
102}
103
104pub fn recent_partitions(
106 ts: SystemTime,
107 granularity: PartitionGranularity,
108 count: usize,
109) -> Vec<String> {
110 let mut names = Vec::new();
111 let mut current = ts;
112 for _ in 0..count {
113 names.push(partition_name(current, granularity));
114 current = prev_partition_end(current, granularity);
115 }
116 names.reverse();
117 names
118}
119
120pub fn next_partition_start(ts: SystemTime, granularity: PartitionGranularity) -> SystemTime {
122 let dt = chrono::DateTime::<Utc>::from(ts);
123
124 let next = match granularity {
125 PartitionGranularity::Year => NaiveDate::from_ymd_opt(dt.year() + 1, 1, 1)
126 .expect("Jan 1 is always valid")
127 .and_hms_opt(0, 0, 0)
128 .expect("midnight is always valid")
129 .and_utc(),
130 PartitionGranularity::Month => {
131 let (y, m) = if dt.month() == 12 {
132 (dt.year() + 1, 1)
133 } else {
134 (dt.year(), dt.month() + 1)
135 };
136 NaiveDate::from_ymd_opt(y, m, 1)
137 .expect("1st of month is always valid")
138 .and_hms_opt(0, 0, 0)
139 .expect("midnight is always valid")
140 .and_utc()
141 }
142 PartitionGranularity::Day => (dt.date_naive() + Duration::days(1))
143 .and_hms_opt(0, 0, 0)
144 .expect("midnight is always valid")
145 .and_utc(),
146 PartitionGranularity::Hour => {
147 let current_hour = dt
148 .date_naive()
149 .and_hms_opt(dt.hour(), 0, 0)
150 .expect("hour from valid DateTime")
151 .and_utc();
152 current_hour + Duration::hours(1)
153 }
154 PartitionGranularity::FiveMin
155 | PartitionGranularity::FifteenMin
156 | PartitionGranularity::ThirtyMin => {
157 let approx_min = granularity.approx_minutes();
158 let start_min = (dt.minute() / approx_min) * approx_min;
159 let current_start = dt
160 .date_naive()
161 .and_hms_opt(dt.hour(), start_min, 0)
162 .expect("aligned minute from valid DateTime")
163 .and_utc();
164 current_start + Duration::minutes(approx_min as i64)
165 }
166 };
167
168 next.into()
169}
170
171fn prev_partition_end(ts: SystemTime, granularity: PartitionGranularity) -> SystemTime {
173 let dt = chrono::DateTime::<Utc>::from(ts);
174
175 let prev_end = match granularity {
176 PartitionGranularity::Year => NaiveDate::from_ymd_opt(dt.year() - 1, 12, 31)
177 .expect("Dec 31 is always valid")
178 .and_hms_opt(23, 59, 59)
179 .expect("23:59:59 is always valid")
180 .and_utc(),
181 PartitionGranularity::Month => {
182 let first_of_month = NaiveDate::from_ymd_opt(dt.year(), dt.month(), 1)
183 .expect("1st of month is always valid");
184 let prev = first_of_month - Duration::days(1);
185 prev.and_hms_opt(23, 59, 59)
186 .expect("23:59:59 is always valid")
187 .and_utc()
188 }
189 PartitionGranularity::Day => {
190 let prev = dt.date_naive() - Duration::days(1);
191 prev.and_hms_opt(23, 59, 59)
192 .expect("23:59:59 is always valid")
193 .and_utc()
194 }
195 PartitionGranularity::Hour => {
196 dt.date_naive()
197 .and_hms_opt(dt.hour(), 0, 0)
198 .expect("hour from valid DateTime")
199 .and_utc()
200 - Duration::seconds(1)
201 }
202 PartitionGranularity::FiveMin
203 | PartitionGranularity::FifteenMin
204 | PartitionGranularity::ThirtyMin => {
205 let approx_min = granularity.approx_minutes();
206 let start_min = (dt.minute() / approx_min) * approx_min;
207 dt.date_naive()
208 .and_hms_opt(dt.hour(), start_min, 0)
209 .expect("aligned minute from valid DateTime")
210 .and_utc()
211 - Duration::seconds(1)
212 }
213 };
214
215 prev_end.into()
216}
217
218#[cfg(test)]
219mod tests {
220 use super::*;
221 use chrono::TimeZone;
222
223 #[test]
224 fn test_partition_name_year() {
225 let ts: SystemTime = Utc.with_ymd_and_hms(2024, 6, 15, 10, 30, 0).unwrap().into();
226 assert_eq!(partition_name(ts, PartitionGranularity::Year), "2024");
227 }
228
229 #[test]
230 fn test_partition_name_month() {
231 let ts: SystemTime = Utc.with_ymd_and_hms(2024, 3, 15, 10, 30, 0).unwrap().into();
232 assert_eq!(partition_name(ts, PartitionGranularity::Month), "2024_03");
233 }
234
235 #[test]
236 fn test_partition_name_day() {
237 let ts: SystemTime = Utc.with_ymd_and_hms(2024, 3, 5, 10, 30, 0).unwrap().into();
238 assert_eq!(partition_name(ts, PartitionGranularity::Day), "2024_03_05");
239 }
240
241 #[test]
242 fn test_partition_name_hour() {
243 let ts: SystemTime = Utc.with_ymd_and_hms(2024, 3, 5, 9, 30, 0).unwrap().into();
244 assert_eq!(
245 partition_name(ts, PartitionGranularity::Hour),
246 "2024_03_05_09"
247 );
248 }
249
250 #[test]
251 fn test_partition_name_15min() {
252 let ts: SystemTime = Utc.with_ymd_and_hms(2024, 3, 5, 9, 47, 0).unwrap().into();
253 assert_eq!(
254 partition_name(ts, PartitionGranularity::FifteenMin),
255 "2024_03_05_09_45"
256 );
257 }
258
259 #[test]
260 fn test_partitions_in_range() {
261 let start: SystemTime = Utc.with_ymd_and_hms(2024, 3, 5, 10, 0, 0).unwrap().into();
262 let end: SystemTime = Utc.with_ymd_and_hms(2024, 3, 5, 12, 30, 0).unwrap().into();
263 let names = partitions_in_range(start, end, PartitionGranularity::Hour);
264 assert_eq!(
265 names,
266 vec!["2024_03_05_10", "2024_03_05_11", "2024_03_05_12"]
267 );
268 }
269}