Skip to main content

shape_ast/data/
mod.rs

1//! Timeframe definitions and utilities
2//!
3//! Represents time intervals for data aggregation (e.g., 5m, 1h, 1d).
4
5use serde::{Deserialize, Serialize};
6use std::fmt;
7
8/// Represents a time interval for data aggregation
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub struct Timeframe {
11    pub value: u32,
12    pub unit: TimeframeUnit,
13}
14
15impl PartialOrd for Timeframe {
16    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
17        Some(self.cmp(other))
18    }
19}
20
21impl Ord for Timeframe {
22    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
23        self.to_seconds().cmp(&other.to_seconds())
24    }
25}
26
27/// Time unit for timeframes
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
29pub enum TimeframeUnit {
30    Second,
31    Minute,
32    Hour,
33    Day,
34    Week,
35    Month,
36    Year,
37}
38
39impl Timeframe {
40    /// Create a new timeframe
41    pub fn new(value: u32, unit: TimeframeUnit) -> Self {
42        Self { value, unit }
43    }
44
45    /// Parse a timeframe string like "5m", "1h", "4h", "1d", etc.
46    pub fn parse(s: &str) -> Option<Self> {
47        if s.is_empty() {
48            return None;
49        }
50
51        // Find where the number ends and unit begins
52        let digit_end = s.find(|c: char| !c.is_numeric())?;
53        if digit_end == 0 {
54            return None;
55        }
56
57        let (num_str, unit_str) = s.split_at(digit_end);
58        let value: u32 = num_str.parse().ok()?;
59
60        let unit = match unit_str.to_lowercase().as_str() {
61            "s" | "sec" | "second" | "seconds" => TimeframeUnit::Second,
62            "m" | "min" | "minute" | "minutes" => TimeframeUnit::Minute,
63            "h" | "hr" | "hour" | "hours" => TimeframeUnit::Hour,
64            "d" | "day" | "days" => TimeframeUnit::Day,
65            "w" | "week" | "weeks" => TimeframeUnit::Week,
66            "mo" | "mon" | "month" | "months" => TimeframeUnit::Month,
67            _ => return None,
68        };
69
70        Some(Self { value, unit })
71    }
72
73    /// Convert to total seconds for comparison
74    pub fn to_seconds(&self) -> u64 {
75        let multiplier = match self.unit {
76            TimeframeUnit::Second => 1,
77            TimeframeUnit::Minute => 60,
78            TimeframeUnit::Hour => 3600,
79            TimeframeUnit::Day => 86400,
80            TimeframeUnit::Week => 604800,
81            TimeframeUnit::Month => 2592000, // Approximate: 30 days
82            TimeframeUnit::Year => 31536000, // Approximate: 365 days
83        };
84        self.value as u64 * multiplier
85    }
86
87    /// Convert to total milliseconds
88    pub fn to_millis(&self) -> u64 {
89        self.to_seconds() * 1000
90    }
91
92    /// Check if this timeframe can be aggregated from another
93    pub fn can_aggregate_from(&self, base: &Timeframe) -> bool {
94        let self_seconds = self.to_seconds();
95        let base_seconds = base.to_seconds();
96
97        // Must be larger than base and evenly divisible
98        self_seconds > base_seconds && self_seconds.is_multiple_of(base_seconds)
99    }
100
101    /// Calculate how many base rows are needed for this timeframe
102    pub fn aggregation_factor(&self, base: &Timeframe) -> Option<usize> {
103        if !self.can_aggregate_from(base) {
104            return None;
105        }
106        Some((self.to_seconds() / base.to_seconds()) as usize)
107    }
108
109    /// Get alignment timestamp for a given timestamp
110    /// For example, 9:32:45 with 5m timeframe aligns to 9:30:00
111    pub fn align_timestamp(&self, timestamp: i64) -> i64 {
112        let interval = self.to_seconds() as i64;
113        (timestamp / interval) * interval
114    }
115
116    /// Get next aligned timestamp
117    pub fn next_aligned_timestamp(&self, timestamp: i64) -> i64 {
118        self.align_timestamp(timestamp) + self.to_seconds() as i64
119    }
120
121    // Common timeframes as convenience constructors
122    pub fn m1() -> Self {
123        Self::new(1, TimeframeUnit::Minute)
124    }
125    pub fn m5() -> Self {
126        Self::new(5, TimeframeUnit::Minute)
127    }
128    pub fn m15() -> Self {
129        Self::new(15, TimeframeUnit::Minute)
130    }
131    pub fn m30() -> Self {
132        Self::new(30, TimeframeUnit::Minute)
133    }
134    pub fn h1() -> Self {
135        Self::new(1, TimeframeUnit::Hour)
136    }
137    pub fn h4() -> Self {
138        Self::new(4, TimeframeUnit::Hour)
139    }
140    pub fn d1() -> Self {
141        Self::new(1, TimeframeUnit::Day)
142    }
143    pub fn w1() -> Self {
144        Self::new(1, TimeframeUnit::Week)
145    }
146
147    /// Check if this is an intraday timeframe
148    pub fn is_intraday(&self) -> bool {
149        matches!(
150            self.unit,
151            TimeframeUnit::Second | TimeframeUnit::Minute | TimeframeUnit::Hour
152        )
153    }
154}
155
156impl fmt::Display for Timeframe {
157    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158        let unit_str = match self.unit {
159            TimeframeUnit::Second => "s",
160            TimeframeUnit::Minute => "m",
161            TimeframeUnit::Hour => "h",
162            TimeframeUnit::Day => "d",
163            TimeframeUnit::Week => "w",
164            TimeframeUnit::Month => "M",
165            TimeframeUnit::Year => "y",
166        };
167        write!(f, "{}{}", self.value, unit_str)
168    }
169}
170
171impl std::str::FromStr for Timeframe {
172    type Err = String;
173
174    fn from_str(s: &str) -> Result<Self, Self::Err> {
175        Self::parse(s).ok_or_else(|| format!("Invalid timeframe: {}", s))
176    }
177}
178
179impl Default for Timeframe {
180    fn default() -> Self {
181        Self::d1()
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn test_timeframe_parsing() {
191        assert_eq!(
192            Timeframe::parse("5m"),
193            Some(Timeframe::new(5, TimeframeUnit::Minute))
194        );
195        assert_eq!(
196            Timeframe::parse("1h"),
197            Some(Timeframe::new(1, TimeframeUnit::Hour))
198        );
199        assert_eq!(
200            Timeframe::parse("4h"),
201            Some(Timeframe::new(4, TimeframeUnit::Hour))
202        );
203        assert_eq!(
204            Timeframe::parse("1d"),
205            Some(Timeframe::new(1, TimeframeUnit::Day))
206        );
207    }
208
209    #[test]
210    fn test_aggregation() {
211        let m1 = Timeframe::m1();
212        let m5 = Timeframe::m5();
213        let h1 = Timeframe::h1();
214
215        assert!(m5.can_aggregate_from(&m1));
216        assert!(h1.can_aggregate_from(&m1));
217        assert!(h1.can_aggregate_from(&m5));
218
219        assert_eq!(m5.aggregation_factor(&m1), Some(5));
220        assert_eq!(h1.aggregation_factor(&m1), Some(60));
221        assert_eq!(h1.aggregation_factor(&m5), Some(12));
222    }
223
224    #[test]
225    fn test_timestamp_alignment() {
226        let m5 = Timeframe::m5();
227        let ts = 1704111165; // Some timestamp
228        let aligned = m5.align_timestamp(ts);
229        assert_eq!(aligned % 300, 0); // Should be divisible by 300 seconds
230    }
231}