1use chrono::Duration;
22use serde::{Deserialize, Serialize};
23use std::borrow::Cow;
24use std::fmt;
25use std::str::FromStr;
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
53pub enum Timeframe {
54 Ms100,
57 Ms250,
59 Ms500,
61
62 Sec1,
65 Sec2,
67 Sec5,
69 Sec10,
71 Sec30,
73
74 Min1,
77 Min5,
79 Min15,
81 Min30,
83
84 Hour1,
87 Hour4,
89
90 Day1,
93 Week1,
95 Month1,
97
98 Custom(u64),
101}
102
103impl Timeframe {
104 pub fn as_str(&self) -> Cow<'static, str> {
106 match self {
107 Timeframe::Ms100 => Cow::Borrowed("100ms"),
108 Timeframe::Ms250 => Cow::Borrowed("250ms"),
109 Timeframe::Ms500 => Cow::Borrowed("500ms"),
110 Timeframe::Sec1 => Cow::Borrowed("1s"),
111 Timeframe::Sec2 => Cow::Borrowed("2s"),
112 Timeframe::Sec5 => Cow::Borrowed("5s"),
113 Timeframe::Sec10 => Cow::Borrowed("10s"),
114 Timeframe::Sec30 => Cow::Borrowed("30s"),
115 Timeframe::Min1 => Cow::Borrowed("1min"),
116 Timeframe::Min5 => Cow::Borrowed("5min"),
117 Timeframe::Min15 => Cow::Borrowed("15min"),
118 Timeframe::Min30 => Cow::Borrowed("30min"),
119 Timeframe::Hour1 => Cow::Borrowed("1h"),
120 Timeframe::Hour4 => Cow::Borrowed("4h"),
121 Timeframe::Day1 => Cow::Borrowed("1D"),
122 Timeframe::Week1 => Cow::Borrowed("1W"),
123 Timeframe::Month1 => Cow::Borrowed("1M"),
124 Timeframe::Custom(seconds) => Cow::Owned(format_custom_seconds(*seconds)),
125 }
126 }
127
128 pub fn all() -> Vec<Timeframe> {
130 vec![
131 Timeframe::Ms100,
132 Timeframe::Ms250,
133 Timeframe::Ms500,
134 Timeframe::Sec1,
135 Timeframe::Sec2,
136 Timeframe::Sec5,
137 Timeframe::Sec10,
138 Timeframe::Sec30,
139 Timeframe::Min1,
140 Timeframe::Min5,
141 Timeframe::Min15,
142 Timeframe::Min30,
143 Timeframe::Hour1,
144 Timeframe::Hour4,
145 Timeframe::Day1,
146 Timeframe::Week1,
147 Timeframe::Month1,
148 ]
149 }
150
151 pub fn duration_ms(&self) -> i64 {
153 match self {
154 Timeframe::Ms100 => 100,
155 Timeframe::Ms250 => 250,
156 Timeframe::Ms500 => 500,
157 Timeframe::Sec1 => 1000,
158 Timeframe::Sec2 => 2000,
159 Timeframe::Sec5 => 5000,
160 Timeframe::Sec10 => 10000,
161 Timeframe::Sec30 => 30000,
162 Timeframe::Min1 => 60_000,
163 Timeframe::Min5 => 300_000,
164 Timeframe::Min15 => 900_000,
165 Timeframe::Min30 => 1_800_000,
166 Timeframe::Hour1 => 3_600_000,
167 Timeframe::Hour4 => 14_400_000,
168 Timeframe::Day1 => 86_400_000,
169 Timeframe::Week1 => 604_800_000,
170 Timeframe::Month1 => 2_592_000_000, Timeframe::Custom(seconds) => (*seconds as i64) * 1000,
172 }
173 }
174
175 pub fn duration(&self) -> Duration {
177 Duration::milliseconds(self.duration_ms())
178 }
179
180 pub fn is_custom(&self) -> bool {
182 matches!(self, Timeframe::Custom(_))
183 }
184
185 pub fn total_seconds(&self) -> u64 {
187 (self.duration_ms() / 1000).max(0) as u64
188 }
189
190 pub fn as_seconds(self) -> i64 {
192 self.total_seconds() as i64
193 }
194
195 pub fn to_seconds(self) -> i64 {
197 self.as_seconds()
198 }
199
200 pub fn from_resolution(resolution: &str) -> Option<Self> {
202 match resolution {
203 "1" => Some(Timeframe::Min1),
204 "5" => Some(Timeframe::Min5),
205 "15" => Some(Timeframe::Min15),
206 "30" => Some(Timeframe::Min30),
207 "60" | "1H" => Some(Timeframe::Hour1),
208 "240" | "4H" => Some(Timeframe::Hour4),
209 "1D" | "D" => Some(Timeframe::Day1),
210 "1W" | "W" => Some(Timeframe::Week1),
211 "1M" | "M" => Some(Timeframe::Month1),
212 _ => None,
213 }
214 }
215}
216
217fn format_custom_seconds(seconds: u64) -> String {
220 if seconds == 0 {
221 return "0s".to_string();
222 }
223 if seconds >= 86400 && seconds.is_multiple_of(86400) {
224 format!("{}D", seconds / 86400)
225 } else if seconds >= 3600 && seconds.is_multiple_of(3600) {
226 format!("{}h", seconds / 3600)
227 } else if seconds >= 60 && seconds.is_multiple_of(60) {
228 format!("{}min", seconds / 60)
229 } else {
230 format!("{seconds}s")
231 }
232}
233
234impl Default for Timeframe {
235 fn default() -> Self {
237 Timeframe::Min1
238 }
239}
240
241impl fmt::Display for Timeframe {
242 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
243 write!(f, "{}", self.as_str())
244 }
245}
246
247impl FromStr for Timeframe {
248 type Err = String;
249
250 fn from_str(s: &str) -> Result<Self, Self::Err> {
251 match s.to_lowercase().as_str() {
253 "100ms" => return Ok(Timeframe::Ms100),
254 "250ms" => return Ok(Timeframe::Ms250),
255 "500ms" => return Ok(Timeframe::Ms500),
256 "1s" | "1sec" => return Ok(Timeframe::Sec1),
257 "2s" | "2sec" => return Ok(Timeframe::Sec2),
258 "5s" | "5sec" => return Ok(Timeframe::Sec5),
259 "10s" | "10sec" => return Ok(Timeframe::Sec10),
260 "30s" | "30sec" => return Ok(Timeframe::Sec30),
261 "1min" => return Ok(Timeframe::Min1),
262 "5min" => return Ok(Timeframe::Min5),
263 "15min" => return Ok(Timeframe::Min15),
264 "30min" => return Ok(Timeframe::Min30),
265 "1h" | "1hour" => return Ok(Timeframe::Hour1),
266 "4h" | "4hour" => return Ok(Timeframe::Hour4),
267 "1d" | "1day" => return Ok(Timeframe::Day1),
268 "1w" | "1week" => return Ok(Timeframe::Week1),
269 "1m" | "1month" => return Ok(Timeframe::Month1),
270 _ => {}
271 }
272
273 let lower = s.to_lowercase();
275 if let Some(seconds) = parse_custom_timeframe(&lower) {
276 if seconds == 0 {
277 return Err("Custom timeframe must be greater than zero".to_string());
278 }
279 return Ok(Timeframe::Custom(seconds));
280 }
281
282 Err(format!(
283 "Invalid timeframe '{s}'. Valid formats: 100ms, 250ms, 500ms, 1s-30s, 1min-30min, 1h, 4h, 1D, 1W, 1M, or custom (e.g. 45s, 3min, 2h)"
284 ))
285 }
286}
287
288fn parse_custom_timeframe(s: &str) -> Option<u64> {
294 let suffixes: &[(&str, u64)] = &[
295 ("month", 2_592_000),
296 ("hour", 3600),
297 ("min", 60),
298 ("sec", 1),
299 ("day", 86400),
300 ("s", 1),
301 ("h", 3600),
302 ("d", 86400),
303 ("w", 604_800),
304 ];
305
306 for &(suffix, multiplier) in suffixes {
307 if let Some(num_str) = s.strip_suffix(suffix) {
308 let num: u64 = num_str.trim().parse().ok()?;
309 return Some(num * multiplier);
310 }
311 }
312
313 let num: u64 = s.trim().parse().ok()?;
315 Some(num * 60)
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321
322 #[test]
323 fn test_preset_as_str() {
324 assert_eq!(&*Timeframe::Min1.as_str(), "1min");
325 assert_eq!(&*Timeframe::Hour4.as_str(), "4h");
326 assert_eq!(&*Timeframe::Day1.as_str(), "1D");
327 }
328
329 #[test]
330 fn test_custom_as_str() {
331 assert_eq!(&*Timeframe::Custom(45).as_str(), "45s");
332 assert_eq!(&*Timeframe::Custom(180).as_str(), "3min");
333 assert_eq!(&*Timeframe::Custom(7200).as_str(), "2h");
334 assert_eq!(&*Timeframe::Custom(172800).as_str(), "2D");
335 }
336
337 #[test]
338 fn test_custom_duration_ms() {
339 assert_eq!(Timeframe::Custom(45).duration_ms(), 45_000);
340 assert_eq!(Timeframe::Custom(180).duration_ms(), 180_000);
341 assert_eq!(Timeframe::Custom(7200).duration_ms(), 7_200_000);
342 }
343
344 #[test]
345 fn test_is_custom() {
346 assert!(!Timeframe::Min1.is_custom());
347 assert!(!Timeframe::Day1.is_custom());
348 assert!(Timeframe::Custom(45).is_custom());
349 assert!(Timeframe::Custom(180).is_custom());
350 }
351
352 #[test]
353 fn test_from_str_presets() {
354 assert_eq!("1min".parse::<Timeframe>(), Ok(Timeframe::Min1));
355 assert_eq!("1h".parse::<Timeframe>(), Ok(Timeframe::Hour1));
356 assert_eq!("1d".parse::<Timeframe>(), Ok(Timeframe::Day1));
357 assert_eq!("1w".parse::<Timeframe>(), Ok(Timeframe::Week1));
358 assert_eq!("1m".parse::<Timeframe>(), Ok(Timeframe::Month1));
359 }
360
361 #[test]
362 fn test_from_str_custom() {
363 assert_eq!("45s".parse::<Timeframe>(), Ok(Timeframe::Custom(45)));
364 assert_eq!("45sec".parse::<Timeframe>(), Ok(Timeframe::Custom(45)));
365 assert_eq!("3min".parse::<Timeframe>(), Ok(Timeframe::Custom(180)));
366 assert_eq!("2h".parse::<Timeframe>(), Ok(Timeframe::Custom(7200)));
367 assert_eq!("2hour".parse::<Timeframe>(), Ok(Timeframe::Custom(7200)));
368 assert_eq!("2d".parse::<Timeframe>(), Ok(Timeframe::Custom(172800)));
369 assert_eq!("2day".parse::<Timeframe>(), Ok(Timeframe::Custom(172800)));
370 }
371
372 #[test]
373 fn test_from_str_custom_zero_rejected() {
374 assert!("0s".parse::<Timeframe>().is_err());
375 assert!("0min".parse::<Timeframe>().is_err());
376 }
377
378 #[test]
379 fn test_from_str_invalid() {
380 assert!("xyz".parse::<Timeframe>().is_err());
381 assert!("".parse::<Timeframe>().is_err());
382 }
383
384 #[test]
385 fn test_custom_display() {
386 assert_eq!(Timeframe::Custom(45).to_string(), "45s");
387 assert_eq!(Timeframe::Custom(180).to_string(), "3min");
388 assert_eq!(Timeframe::Custom(7200).to_string(), "2h");
389 }
390
391 #[test]
392 fn test_total_seconds() {
393 assert_eq!(Timeframe::Min1.total_seconds(), 60);
394 assert_eq!(Timeframe::Hour1.total_seconds(), 3600);
395 assert_eq!(Timeframe::Custom(45).total_seconds(), 45);
396 }
397
398 #[test]
399 fn test_custom_equality() {
400 assert_eq!(Timeframe::Custom(60), Timeframe::Custom(60));
401 assert_ne!(Timeframe::Custom(60), Timeframe::Custom(120));
402 assert_ne!(Timeframe::Custom(60), Timeframe::Min1);
404 }
405
406 #[test]
407 fn test_preset_from_str_takes_precedence() {
408 assert_eq!("1s".parse::<Timeframe>(), Ok(Timeframe::Sec1));
410 assert_eq!("5min".parse::<Timeframe>(), Ok(Timeframe::Min5));
411 assert_eq!("1h".parse::<Timeframe>(), Ok(Timeframe::Hour1));
412 }
413}