shape_runtime/
timeframe_utils.rs1use chrono::{DateTime, Datelike, Duration, Timelike, Utc};
7use shape_ast::ast::{Timeframe, TimeframeUnit};
8use shape_ast::error::{Result, ShapeError};
9
10pub fn parse_timeframe_string(s: &str) -> Result<Timeframe> {
12 let s = s.trim();
13 if s.is_empty() {
14 return Err(ShapeError::RuntimeError {
15 message: "Empty timeframe string".to_string(),
16 location: None,
17 });
18 }
19
20 let split_idx = s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len());
22 let (num_str, unit_str) = s.split_at(split_idx);
23
24 let value: u32 = num_str.parse().map_err(|_| ShapeError::RuntimeError {
25 message: format!("Invalid timeframe number: {}", num_str),
26 location: None,
27 })?;
28
29 let unit = match unit_str.to_lowercase().as_str() {
30 "m" | "min" => TimeframeUnit::Minute,
31 "h" | "hour" => TimeframeUnit::Hour,
32 "d" | "day" => TimeframeUnit::Day,
33 "w" | "week" => TimeframeUnit::Week,
34 "mo" | "month" => TimeframeUnit::Month,
35 "y" | "year" => TimeframeUnit::Year,
36 "" => TimeframeUnit::Minute, _ => {
38 return Err(ShapeError::RuntimeError {
39 message: format!("Unknown timeframe unit: {}", unit_str),
40 location: None,
41 });
42 }
43 };
44
45 Ok(Timeframe::new(value, unit))
46}
47
48pub fn timeframe_to_duration(tf: &Timeframe) -> Duration {
50 Duration::seconds(tf.to_seconds() as i64)
52}
53
54pub fn timeframe_to_minutes(tf: &Timeframe) -> i64 {
56 tf.to_seconds() as i64 / 60
57}
58
59pub fn align_timestamp(ts: DateTime<Utc>, tf: &Timeframe) -> DateTime<Utc> {
61 let seconds = tf.to_seconds() as i64;
62
63 if seconds == 60 {
65 ts.with_second(0).unwrap().with_nanosecond(0).unwrap()
67 } else if seconds == 300 {
68 let minute = ts.minute();
70 let aligned_minute = (minute / 5) * 5;
71 ts.with_minute(aligned_minute)
72 .unwrap()
73 .with_second(0)
74 .unwrap()
75 .with_nanosecond(0)
76 .unwrap()
77 } else if seconds == 900 {
78 let minute = ts.minute();
80 let aligned_minute = (minute / 15) * 15;
81 ts.with_minute(aligned_minute)
82 .unwrap()
83 .with_second(0)
84 .unwrap()
85 .with_nanosecond(0)
86 .unwrap()
87 } else if seconds == 1800 {
88 let minute = ts.minute();
90 let aligned_minute = (minute / 30) * 30;
91 ts.with_minute(aligned_minute)
92 .unwrap()
93 .with_second(0)
94 .unwrap()
95 .with_nanosecond(0)
96 .unwrap()
97 } else if seconds == 3600 {
98 ts.with_minute(0)
100 .unwrap()
101 .with_second(0)
102 .unwrap()
103 .with_nanosecond(0)
104 .unwrap()
105 } else if seconds == 14400 {
106 let hour = ts.hour();
108 let aligned_hour = (hour / 4) * 4;
109 ts.with_hour(aligned_hour)
110 .unwrap()
111 .with_minute(0)
112 .unwrap()
113 .with_second(0)
114 .unwrap()
115 .with_nanosecond(0)
116 .unwrap()
117 } else if seconds == 86400 {
118 ts.with_hour(0)
120 .unwrap()
121 .with_minute(0)
122 .unwrap()
123 .with_second(0)
124 .unwrap()
125 .with_nanosecond(0)
126 .unwrap()
127 } else if seconds == 604800 {
128 let days_from_monday = ts.weekday().num_days_from_monday();
130 let aligned = ts - Duration::days(days_from_monday as i64);
131 aligned
132 .with_hour(0)
133 .unwrap()
134 .with_minute(0)
135 .unwrap()
136 .with_second(0)
137 .unwrap()
138 .with_nanosecond(0)
139 .unwrap()
140 } else {
141 let ts_seconds = ts.timestamp();
143 let aligned_seconds = (ts_seconds / seconds) * seconds;
144 DateTime::from_timestamp(aligned_seconds, 0).unwrap()
145 }
146}
147
148pub fn generate_timestamps(
150 start: DateTime<Utc>,
151 end: DateTime<Utc>,
152 tf: &Timeframe,
153) -> Vec<DateTime<Utc>> {
154 let mut timestamps = Vec::new();
155 let duration = timeframe_to_duration(tf);
156
157 let mut current = align_timestamp(start, tf);
159
160 while current <= end {
161 timestamps.push(current);
162 current += duration;
163 }
164
165 timestamps
166}
167
168pub fn generate_timestamps_micros(
170 start: DateTime<Utc>,
171 end: DateTime<Utc>,
172 tf: &Timeframe,
173) -> Vec<i64> {
174 generate_timestamps(start, end, tf)
175 .into_iter()
176 .map(|ts| ts.timestamp_micros())
177 .collect()
178}
179
180pub fn count_rows(start: DateTime<Utc>, end: DateTime<Utc>, tf: &Timeframe) -> usize {
182 let duration = end - start;
183 let tf_duration = timeframe_to_duration(tf);
184
185 ((duration.num_milliseconds() / tf_duration.num_milliseconds()) + 1) as usize
187}
188
189pub fn find_common_timeframe(tf1: &Timeframe, tf2: &Timeframe) -> Timeframe {
191 let minutes1 = timeframe_to_minutes(tf1);
193 let minutes2 = timeframe_to_minutes(tf2);
194
195 if minutes1 <= minutes2 { *tf1 } else { *tf2 }
196}
197
198pub fn is_timeframe_compatible(base: &Timeframe, target: &Timeframe) -> bool {
200 let base_minutes = timeframe_to_minutes(base);
201 let target_minutes = timeframe_to_minutes(target);
202
203 target_minutes % base_minutes == 0 || base_minutes % target_minutes == 0
205}
206
207pub fn find_covering_index(source_timestamps: &[i64], target_timestamp: i64) -> Option<usize> {
210 if source_timestamps.is_empty() {
211 return None;
212 }
213
214 match source_timestamps.binary_search(&target_timestamp) {
216 Ok(idx) => Some(idx),
217 Err(idx) => {
218 if idx == 0 {
219 None
221 } else {
222 Some(idx - 1)
224 }
225 }
226 }
227}
228
229pub fn alignment_ratio(from_tf: &Timeframe, to_tf: &Timeframe) -> f64 {
231 let from_minutes = timeframe_to_minutes(from_tf) as f64;
232 let to_minutes = timeframe_to_minutes(to_tf) as f64;
233 from_minutes / to_minutes
234}
235
236pub fn find_closest_index(timestamps: &[i64], target: i64) -> Option<usize> {
239 if timestamps.is_empty() {
240 return None;
241 }
242
243 match timestamps.binary_search(&target) {
244 Ok(idx) => Some(idx),
245 Err(idx) => {
246 if idx == 0 {
247 Some(0)
248 } else if idx >= timestamps.len() {
249 Some(timestamps.len() - 1)
250 } else {
251 let diff_prev = (target - timestamps[idx - 1]).abs();
253 let diff_next = (timestamps[idx] - target).abs();
254 if diff_prev <= diff_next {
255 Some(idx - 1)
256 } else {
257 Some(idx)
258 }
259 }
260 }
261 }
262}