1use serde::{Deserialize, Serialize};
6use std::fmt;
7
8#[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#[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 pub fn new(value: u32, unit: TimeframeUnit) -> Self {
42 Self { value, unit }
43 }
44
45 pub fn parse(s: &str) -> Option<Self> {
47 if s.is_empty() {
48 return None;
49 }
50
51 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 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, TimeframeUnit::Year => 31536000, };
84 self.value as u64 * multiplier
85 }
86
87 pub fn to_millis(&self) -> u64 {
89 self.to_seconds() * 1000
90 }
91
92 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 self_seconds > base_seconds && self_seconds.is_multiple_of(base_seconds)
99 }
100
101 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 pub fn align_timestamp(&self, timestamp: i64) -> i64 {
112 let interval = self.to_seconds() as i64;
113 (timestamp / interval) * interval
114 }
115
116 pub fn next_aligned_timestamp(&self, timestamp: i64) -> i64 {
118 self.align_timestamp(timestamp) + self.to_seconds() as i64
119 }
120
121 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 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; let aligned = m5.align_timestamp(ts);
229 assert_eq!(aligned % 300, 0); }
231}