Skip to main content

shape_runtime/
timeframe_utils.rs

1//! Utilities for timeframe operations and timestamp generation
2//!
3//! This module provides utilities for working with timeframes, generating
4//! aligned timestamps, and converting between timeframes.
5
6use chrono::{DateTime, Datelike, Duration, Timelike, Utc};
7use shape_ast::ast::{Timeframe, TimeframeUnit};
8use shape_ast::error::{Result, ShapeError};
9
10/// Parse a timeframe string (e.g., "1m", "1h", "1d")
11pub 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    // Find where the number ends and unit begins
21    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, // Default to minute if no unit
37        _ => {
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
48/// Convert a timeframe to a Duration
49pub fn timeframe_to_duration(tf: &Timeframe) -> Duration {
50    // Timeframe has a to_seconds() method - use that
51    Duration::seconds(tf.to_seconds() as i64)
52}
53
54/// Get the numeric value of a timeframe in minutes
55pub fn timeframe_to_minutes(tf: &Timeframe) -> i64 {
56    tf.to_seconds() as i64 / 60
57}
58
59/// Align a timestamp to the start of a timeframe bucket
60pub fn align_timestamp(ts: DateTime<Utc>, tf: &Timeframe) -> DateTime<Utc> {
61    let seconds = tf.to_seconds() as i64;
62
63    // Special handling for common cases
64    if seconds == 60 {
65        // 1 minute
66        ts.with_second(0).unwrap().with_nanosecond(0).unwrap()
67    } else if seconds == 300 {
68        // 5 minutes
69        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        // 15 minutes
79        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        // 30 minutes
89        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        // 1 hour
99        ts.with_minute(0)
100            .unwrap()
101            .with_second(0)
102            .unwrap()
103            .with_nanosecond(0)
104            .unwrap()
105    } else if seconds == 14400 {
106        // 4 hours
107        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        // 1 day
119        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        // 1 week - align to Monday
129        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        // Generic alignment - round down to nearest timeframe boundary
142        let ts_seconds = ts.timestamp();
143        let aligned_seconds = (ts_seconds / seconds) * seconds;
144        DateTime::from_timestamp(aligned_seconds, 0).unwrap()
145    }
146}
147
148/// Generate a series of aligned timestamps for a given timeframe
149pub 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    // Align the start time to the timeframe
158    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
168/// Generate timestamps as i64 microseconds for SIMD operations
169pub 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
180/// Count the number of rows between two timestamps for a given timeframe
181pub 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    // Add 1 because we include both start and end
186    ((duration.num_milliseconds() / tf_duration.num_milliseconds()) + 1) as usize
187}
188
189/// Find the common timeframe that both can align to (the finer one)
190pub fn find_common_timeframe(tf1: &Timeframe, tf2: &Timeframe) -> Timeframe {
191    // Return the smaller (finer) timeframe
192    let minutes1 = timeframe_to_minutes(tf1);
193    let minutes2 = timeframe_to_minutes(tf2);
194
195    if minutes1 <= minutes2 { *tf1 } else { *tf2 }
196}
197
198/// Check if one timeframe is compatible with another (can be evenly divided)
199pub 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    // Compatible if target is divisible by base or vice versa
204    target_minutes % base_minutes == 0 || base_minutes % target_minutes == 0
205}
206
207/// Find the index in source timestamps that covers the target timestamp
208/// Uses binary search for efficiency
209pub fn find_covering_index(source_timestamps: &[i64], target_timestamp: i64) -> Option<usize> {
210    if source_timestamps.is_empty() {
211        return None;
212    }
213
214    // Binary search to find the appropriate source index
215    match source_timestamps.binary_search(&target_timestamp) {
216        Ok(idx) => Some(idx),
217        Err(idx) => {
218            if idx == 0 {
219                // Target is before all source timestamps
220                None
221            } else {
222                // Return the previous index (forward-fill semantics)
223                Some(idx - 1)
224            }
225        }
226    }
227}
228
229/// Calculate the alignment ratio between two timeframes
230pub 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
236/// Find the closest index in a sorted array of timestamps
237/// Returns the index of the timestamp closest to the target
238pub 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                // Check which is closer: idx-1 or idx
252                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}