1use std::{fmt, num::NonZeroU32, str::FromStr};
2
3use serde::{Deserialize, Deserializer, Serialize};
4use time::{Date, Duration, PrimitiveDateTime, Time, Weekday};
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
8pub enum TimeUnit {
9 Second = 0,
11 Minute = 1,
13 Day = 2,
15 Week = 3,
17 Month = 4,
19 Tick = 5,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
58pub struct TimeFrame {
59 pub quantity: NonZeroU32,
61 pub unit: TimeUnit,
63}
64
65impl TimeFrame {
66 #[inline]
68 pub fn new(quantity: u32, unit: TimeUnit) -> Self {
69 TimeFrame {
70 quantity: NonZeroU32::new(quantity).expect("quantity must be non-zero"),
71 unit,
72 }
73 }
74
75 #[inline]
77 pub fn seconds(quantity: u32) -> Self {
78 TimeFrame::new(quantity, TimeUnit::Second)
79 }
80
81 #[inline]
83 pub fn minutes(quantity: u32) -> Self {
84 TimeFrame::new(quantity, TimeUnit::Minute)
85 }
86
87 #[inline]
89 pub fn days(quantity: u32) -> Self {
90 TimeFrame::new(quantity, TimeUnit::Day)
91 }
92
93 #[inline]
95 pub fn weeks(quantity: u32) -> Self {
96 TimeFrame::new(quantity, TimeUnit::Week)
97 }
98
99 #[inline]
101 pub fn months(quantity: u32) -> Self {
102 TimeFrame::new(quantity, TimeUnit::Month)
103 }
104
105 #[inline]
107 pub fn ticks(quantity: u32) -> Self {
108 TimeFrame::new(quantity, TimeUnit::Tick)
109 }
110
111 #[inline]
115 pub fn multiplier(&self) -> u32 {
116 self.quantity.get()
117 }
118
119 pub fn in_seconds(&self) -> Option<u64> {
121 Some(match self.unit {
122 TimeUnit::Second => self.quantity.get() as u64,
123 TimeUnit::Minute => self.quantity.get() as u64 * 60,
124 TimeUnit::Day => self.quantity.get() as u64 * 60 * 60 * 24,
125 TimeUnit::Week => self.quantity.get() as u64 * 60 * 60 * 24 * 7,
126 TimeUnit::Month => self.quantity.get() as u64 * 2628003, TimeUnit::Tick => return None,
128 })
129 }
130
131 pub fn from_seconds(seconds: i64) -> Self {
133 const DAYS_IN_SECONDS: i64 = 86400;
134
135 if seconds <= 1 {
136 TimeFrame::new(1, TimeUnit::Second)
137 } else if seconds <= 5 {
138 TimeFrame::new(5, TimeUnit::Second)
139 } else if seconds <= 10 {
140 TimeFrame::new(10, TimeUnit::Second)
141 } else if seconds <= 15 {
142 TimeFrame::new(15, TimeUnit::Second)
143 } else if seconds <= 30 {
144 TimeFrame::new(30, TimeUnit::Second)
145 } else if seconds <= 45 {
146 TimeFrame::new(45, TimeUnit::Second)
147 } else if seconds <= DAYS_IN_SECONDS {
148 TimeFrame::new(((seconds + 59) / 60) as u32, TimeUnit::Minute)
149 } else if seconds <= 366 * DAYS_IN_SECONDS {
150 if seconds % (7 * DAYS_IN_SECONDS) == 0 {
151 let weeks = seconds / (7 * DAYS_IN_SECONDS);
152 TimeFrame::new(weeks as u32, TimeUnit::Week)
153 } else {
154 let days = (seconds + DAYS_IN_SECONDS - 1) / DAYS_IN_SECONDS;
155 TimeFrame::new(days as u32, TimeUnit::Day)
156 }
157 } else {
158 TimeFrame::new(12, TimeUnit::Month)
159 }
160 }
161
162 pub(crate) fn round(
163 &self,
164 session_start: &Time,
165 time: &PrimitiveDateTime,
166 ) -> Option<PrimitiveDateTime> {
167 match self.unit {
168 TimeUnit::Second => {
169 let seconds = self.quantity.get() as i64;
170 let start_seconds = session_start.hour() as i64 * 3600
171 + session_start.minute() as i64 * 60
172 + session_start.second() as i64;
173 let current_seconds =
174 time.hour() as i64 * 3600 + time.minute() as i64 * 60 + time.second() as i64;
175 let offset = ((current_seconds - start_seconds) / seconds) * seconds;
176 Some(time.replace_time(*session_start + Duration::seconds(offset)))
177 }
178 TimeUnit::Minute => {
179 let minutes = self.quantity.get() as i64;
180 let start_minutes =
181 session_start.hour() as i64 * 60 + session_start.minute() as i64;
182 let current_minutes = time.hour() as i64 * 60 + time.minute() as i64;
183 let offset = ((current_minutes - start_minutes) / minutes) * minutes;
184 Some(time.replace_time(*session_start + Duration::minutes(offset)))
185 }
186 TimeUnit::Day => Some(time.replace_time(Time::MIDNIGHT)),
187 TimeUnit::Week => {
188 Date::from_iso_week_date(time.year(), time.iso_week(), Weekday::Monday)
189 .map(|date| date.with_time(Time::MIDNIGHT))
190 .ok()
191 }
192 TimeUnit::Month => Some(PrimitiveDateTime::new(
193 Date::from_calendar_date(time.year(), time.month(), 1).ok()?,
194 Time::MIDNIGHT,
195 )),
196 TimeUnit::Tick => Some(*time),
197 }
198 }
199
200 pub(crate) fn close(
201 &self,
202 session_start: &Time,
203 time: &PrimitiveDateTime,
204 ) -> Option<PrimitiveDateTime> {
205 let time = self.round(session_start, time)?;
206 match self.unit {
207 TimeUnit::Second => Some(time + Duration::seconds(self.quantity.get() as i64)),
208 TimeUnit::Minute => Some(time + Duration::minutes(self.quantity.get() as i64)),
209 TimeUnit::Day => Some(time + Duration::days(self.quantity.get() as i64)),
210 TimeUnit::Week => Some(time + Duration::weeks(self.quantity.get() as i64)),
211 TimeUnit::Month => {
212 let mut month = time.month() as u32 + self.quantity.get();
213 let mut year = time.year();
214 while month > 12 {
215 month -= 12;
216 year += 1;
217 }
218 Some(PrimitiveDateTime::new(
219 Date::from_calendar_date(year, (month as u8).try_into().unwrap(), 1).ok()?,
220 Time::MIDNIGHT,
221 ))
222 }
223 TimeUnit::Tick => None,
224 }
225 }
226}
227
228#[derive(Debug, thiserror::Error)]
230#[error("invalid time frame")]
231#[non_exhaustive]
232pub struct ParseTimeFrameError;
233
234impl FromStr for TimeFrame {
235 type Err = ParseTimeFrameError;
236
237 fn from_str(s: &str) -> Result<Self, Self::Err> {
238 let s = s.trim();
239 let unit = match s.chars().next_back() {
240 Some('S') => TimeUnit::Second,
241 Some('D') => TimeUnit::Day,
242 Some('W') => TimeUnit::Week,
243 Some('M') => TimeUnit::Month,
244 Some('T') => TimeUnit::Tick,
245 _ => {
246 return Ok(TimeFrame {
247 quantity: s.parse().map_err(|_| ParseTimeFrameError)?,
248 unit: TimeUnit::Minute,
249 });
250 }
251 };
252 let n_str = &s[..s.len() - 1];
253
254 if n_str.is_empty() {
255 return Ok(TimeFrame {
256 quantity: NonZeroU32::new(1).unwrap(),
257 unit,
258 });
259 }
260
261 n_str
262 .parse()
263 .map_err(|_| ParseTimeFrameError)
264 .map(|quantity| TimeFrame { quantity, unit })
265 }
266}
267
268impl fmt::Display for TimeFrame {
269 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
270 fn write_with_suffix(
271 f: &mut fmt::Formatter<'_>,
272 quantity: NonZeroU32,
273 suffix: &str,
274 ) -> fmt::Result {
275 if quantity.get() > 1 {
276 write!(f, "{}{}", quantity, suffix)
277 } else {
278 f.write_str(suffix)
279 }
280 }
281
282 match self.unit {
283 TimeUnit::Second => write_with_suffix(f, self.quantity, "S"),
284 TimeUnit::Minute => write!(f, "{}", self.quantity),
285 TimeUnit::Day => write_with_suffix(f, self.quantity, "D"),
286 TimeUnit::Week => write_with_suffix(f, self.quantity, "W"),
287 TimeUnit::Month => write_with_suffix(f, self.quantity, "M"),
288 TimeUnit::Tick => write_with_suffix(f, self.quantity, "T"),
289 }
290 }
291}
292
293impl Serialize for TimeFrame {
294 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
295 where
296 S: serde::Serializer,
297 {
298 serializer.collect_str(self)
299 }
300}
301
302impl<'de> Deserialize<'de> for TimeFrame {
303 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
304 where
305 D: Deserializer<'de>,
306 {
307 String::deserialize(deserializer)?
308 .parse()
309 .map_err(serde::de::Error::custom)
310 }
311}
312
313#[cfg(test)]
314mod tests {
315 use super::*;
316
317 #[test]
318 fn parse_time_frame() {
319 let tf: TimeFrame = "5".parse().unwrap();
320 assert_eq!(tf.quantity.get(), 5);
321 assert_eq!(tf.unit, TimeUnit::Minute);
322
323 let tf: TimeFrame = "D".parse().unwrap();
324 assert_eq!(tf.quantity.get(), 1);
325 assert_eq!(tf.unit, TimeUnit::Day);
326
327 let tf: TimeFrame = "10W".parse().unwrap();
328 assert_eq!(tf.quantity.get(), 10);
329 assert_eq!(tf.unit, TimeUnit::Week);
330
331 let tf: TimeFrame = "M".parse().unwrap();
332 assert_eq!(tf.quantity.get(), 1);
333 assert_eq!(tf.unit, TimeUnit::Month);
334
335 let tf: TimeFrame = "30S".parse().unwrap();
336 assert_eq!(tf.quantity.get(), 30);
337 assert_eq!(tf.unit, TimeUnit::Second);
338
339 let tf: TimeFrame = "T".parse().unwrap();
340 assert_eq!(tf.quantity.get(), 1);
341 assert_eq!(tf.unit, TimeUnit::Tick);
342 }
343}