1#[derive(Clone, Copy, Debug, PartialEq, Eq)]
4pub enum TimeframeUnit {
5 Ticks,
6 Seconds,
7 Minutes,
8 Daily,
9 Weekly,
10 Monthly,
11}
12
13impl TimeframeUnit {
14 pub fn suffix(self) -> &'static str {
16 match self {
17 TimeframeUnit::Ticks => "T",
18 TimeframeUnit::Seconds => "S",
19 TimeframeUnit::Minutes => "",
20 TimeframeUnit::Daily => "D",
21 TimeframeUnit::Weekly => "W",
22 TimeframeUnit::Monthly => "M",
23 }
24 }
25
26 pub fn from_suffix(suffix: &str) -> Option<Self> {
28 Some(match suffix {
29 "T" => TimeframeUnit::Ticks,
30 "S" => TimeframeUnit::Seconds,
31 "" => TimeframeUnit::Minutes,
32 "D" => TimeframeUnit::Daily,
33 "W" => TimeframeUnit::Weekly,
34 "M" => TimeframeUnit::Monthly,
35 _ => return None,
36 })
37 }
38
39 pub fn millis(self) -> Option<i64> {
41 Some(match self {
42 TimeframeUnit::Seconds => 1_000,
43 TimeframeUnit::Minutes => 60_000,
44 TimeframeUnit::Daily => 86_400_000,
45 TimeframeUnit::Weekly => 604_800_000,
46 TimeframeUnit::Ticks | TimeframeUnit::Monthly => return None,
47 })
48 }
49}
50
51#[derive(Clone, Debug)]
55pub struct Timeframe {
56 pub multiplier: u32,
57 pub unit: TimeframeUnit,
58}
59
60impl Default for Timeframe {
61 fn default() -> Self {
62 Self {
63 multiplier: 1,
64 unit: TimeframeUnit::Daily,
65 }
66 }
67}
68
69#[derive(Debug, thiserror::Error, PartialEq, Eq)]
71#[error("unrecognised timeframe {0:?}")]
72pub struct TimeframeError(pub String);
73
74impl std::str::FromStr for Timeframe {
75 type Err = TimeframeError;
76
77 fn from_str(period: &str) -> Result<Self, Self::Err> {
79 let bad = || TimeframeError(period.to_string());
80 let split = period
81 .find(|c: char| !c.is_ascii_digit())
82 .unwrap_or(period.len());
83 let (count, suffix) = period.split_at(split);
84 Ok(Self {
85 multiplier: count.parse().ok().filter(|&m| m > 0).ok_or_else(bad)?,
86 unit: TimeframeUnit::from_suffix(suffix).ok_or_else(bad)?,
87 })
88 }
89}
90
91const REGULAR_UNITS: [TimeframeUnit; 4] = [
95 TimeframeUnit::Weekly,
96 TimeframeUnit::Daily,
97 TimeframeUnit::Minutes,
98 TimeframeUnit::Seconds,
99];
100
101impl Timeframe {
102 pub fn as_minutes(&self) -> Option<u32> {
105 let per_unit = match self.unit {
106 TimeframeUnit::Minutes => 1,
107 TimeframeUnit::Daily => 60 * 24,
108 TimeframeUnit::Weekly => 60 * 24 * 7,
109 TimeframeUnit::Ticks | TimeframeUnit::Seconds | TimeframeUnit::Monthly => return None,
110 };
111 self.multiplier.checked_mul(per_unit)
112 }
113
114 pub fn from_millis(millis: i64) -> Option<Self> {
120 if millis <= 0 {
121 return None;
122 }
123
124 REGULAR_UNITS
125 .into_iter()
126 .filter_map(|unit| Some((unit, unit.millis()?)))
127 .find(|(_, size)| millis % size == 0)
128 .map(|(unit, size)| Self {
129 multiplier: (millis / size) as u32,
130 unit,
131 })
132 }
133
134 pub fn period(&self) -> String {
136 format!("{}{}", self.multiplier, self.unit.suffix())
137 }
138
139 pub fn to_millis(&self) -> Option<i64> {
140 Some(self.unit.millis()? * i64::from(self.multiplier))
141 }
142
143 pub fn is_seconds(&self) -> bool {
144 self.unit == TimeframeUnit::Seconds
145 }
146
147 pub fn is_minutes(&self) -> bool {
148 self.unit == TimeframeUnit::Minutes
149 }
150
151 pub fn is_daily(&self) -> bool {
152 self.unit == TimeframeUnit::Daily
153 }
154
155 pub fn is_weekly(&self) -> bool {
156 self.unit == TimeframeUnit::Weekly
157 }
158
159 pub fn is_monthly(&self) -> bool {
160 self.unit == TimeframeUnit::Monthly
161 }
162
163 pub fn is_ticks(&self) -> bool {
164 self.unit == TimeframeUnit::Ticks
165 }
166
167 pub fn is_intraday(&self) -> bool {
169 self.is_seconds() || self.is_minutes()
170 }
171
172 pub fn is_dwm(&self) -> bool {
174 self.is_daily() || self.is_weekly() || self.is_monthly()
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::Timeframe;
181 use std::str::FromStr;
182
183 #[test]
184 fn parses_pine_period_notation() {
185 for period in ["30S", "5", "60", "240", "1D", "1W", "1M"] {
187 assert_eq!(Timeframe::from_str(period).unwrap().period(), period);
188 }
189 assert!(Timeframe::from_str("5").unwrap().is_minutes());
191 }
192
193 #[test]
194 fn parses_millisecond_lengths() {
195 assert_eq!(
196 Timeframe::from_str("30S").unwrap().to_millis(),
197 Some(30_000)
198 );
199 assert_eq!(Timeframe::from_str("5").unwrap().to_millis(), Some(300_000));
200 assert_eq!(
201 Timeframe::from_str("1D").unwrap().to_millis(),
202 Some(86_400_000)
203 );
204 assert_eq!(Timeframe::from_str("1M").unwrap().to_millis(), None);
206 }
207
208 #[test]
209 fn an_unreadable_interval_is_an_error() {
210 assert!(Timeframe::from_str("").is_err());
211 assert!(Timeframe::from_str("hourly").is_err());
212 assert!(Timeframe::from_str("1y").is_err()); assert!(Timeframe::from_str("5m").is_err()); assert!(Timeframe::from_str("d1").is_err());
215 }
216}