1use chrono::{DateTime, Duration, Months, NaiveDate, NaiveDateTime, TimeZone};
2use itertools::{Either, Itertools};
3use regex::Regex;
4use std::{
5 convert::Infallible,
6 ops::{Add, Sub},
7};
8
9use chrono::ParseError;
10use raphtory_api::core::storage::timeindex::{AsTime, TimeIndexEntry};
11use std::num::ParseIntError;
12
13#[derive(thiserror::Error, Debug, Clone, PartialEq)]
14pub enum ParseTimeError {
15 #[error("the interval string doesn't contain a complete number of number-unit pairs")]
16 InvalidPairs,
17 #[error("one of the tokens in the interval string supposed to be a number couldn't be parsed")]
18 ParseInt {
19 #[from]
20 source: ParseIntError,
21 },
22 #[error("'{0}' is not a valid unit")]
23 InvalidUnit(String),
24 #[error(transparent)]
25 ParseError(#[from] ParseError),
26 #[error("negative interval is not supported")]
27 NegativeInt,
28 #[error("0 size step is not supported")]
29 ZeroSizeStep,
30 #[error("'{0}' is not a valid datetime, valid formats are RFC3339, RFC2822, %Y-%m-%d, %Y-%m-%dT%H:%M:%S%.3f, %Y-%m-%dT%H:%M:%S%, %Y-%m-%d %H:%M:%S%.3f and %Y-%m-%d %H:%M:%S%")]
31 InvalidDateTimeString(String),
32}
33
34impl From<Infallible> for ParseTimeError {
35 fn from(value: Infallible) -> Self {
36 match value {}
37 }
38}
39
40pub trait IntoTime {
41 fn into_time(self) -> i64;
42}
43
44impl IntoTime for i64 {
45 fn into_time(self) -> i64 {
46 self
47 }
48}
49
50impl<Tz: TimeZone> IntoTime for DateTime<Tz> {
51 fn into_time(self) -> i64 {
52 self.timestamp_millis()
53 }
54}
55
56impl IntoTime for NaiveDateTime {
57 fn into_time(self) -> i64 {
58 self.and_utc().timestamp_millis()
59 }
60}
61
62pub trait TryIntoTime {
63 fn try_into_time(self) -> Result<i64, ParseTimeError>;
64}
65
66impl<T: IntoTime> TryIntoTime for T {
67 fn try_into_time(self) -> Result<i64, ParseTimeError> {
68 Ok(self.into_time())
69 }
70}
71
72impl TryIntoTime for &str {
73 fn try_into_time(self) -> Result<i64, ParseTimeError> {
76 let rfc_result = DateTime::parse_from_rfc3339(self);
77 if let Ok(datetime) = rfc_result {
78 return Ok(datetime.timestamp_millis());
79 }
80
81 let result = DateTime::parse_from_rfc2822(self);
82 if let Ok(datetime) = result {
83 return Ok(datetime.timestamp_millis());
84 }
85
86 let result = NaiveDate::parse_from_str(self, "%Y-%m-%d");
87 if let Ok(date) = result {
88 return Ok(date
89 .and_hms_opt(00, 00, 00)
90 .unwrap()
91 .and_utc()
92 .timestamp_millis());
93 }
94
95 let result = NaiveDateTime::parse_from_str(self, "%Y-%m-%dT%H:%M:%S%.3f");
96 if let Ok(datetime) = result {
97 return Ok(datetime.and_utc().timestamp_millis());
98 }
99
100 let result = NaiveDateTime::parse_from_str(self, "%Y-%m-%dT%H:%M:%S%");
101 if let Ok(datetime) = result {
102 return Ok(datetime.and_utc().timestamp_millis());
103 }
104
105 let result = NaiveDateTime::parse_from_str(self, "%Y-%m-%d %H:%M:%S%.3f");
106 if let Ok(datetime) = result {
107 return Ok(datetime.and_utc().timestamp_millis());
108 }
109
110 let result = NaiveDateTime::parse_from_str(self, "%Y-%m-%d %H:%M:%S%");
111 if let Ok(datetime) = result {
112 return Ok(datetime.and_utc().timestamp_millis());
113 }
114
115 Err(ParseTimeError::InvalidDateTimeString(self.to_string()))
116 }
117}
118
119pub enum InputTime {
121 Simple(i64),
122 Indexed(i64, usize),
123}
124
125pub trait TryIntoInputTime {
126 fn try_into_input_time(self) -> Result<InputTime, ParseTimeError>;
127}
128
129impl TryIntoInputTime for InputTime {
130 fn try_into_input_time(self) -> Result<InputTime, ParseTimeError> {
131 Ok(self)
132 }
133}
134
135impl TryIntoInputTime for TimeIndexEntry {
136 fn try_into_input_time(self) -> Result<InputTime, ParseTimeError> {
137 Ok(InputTime::Indexed(self.t(), self.i()))
138 }
139}
140
141impl<T: TryIntoTime> TryIntoInputTime for T {
142 fn try_into_input_time(self) -> Result<InputTime, ParseTimeError> {
143 Ok(InputTime::Simple(self.try_into_time()?))
144 }
145}
146
147impl<T: TryIntoTime> TryIntoInputTime for (T, usize) {
148 fn try_into_input_time(self) -> Result<InputTime, ParseTimeError> {
149 Ok(InputTime::Indexed(self.0.try_into_time()?, self.1))
150 }
151}
152
153pub trait IntoTimeWithFormat {
154 fn parse_time(&self, fmt: &str) -> Result<i64, ParseTimeError>;
155}
156
157impl IntoTimeWithFormat for &str {
158 fn parse_time(&self, fmt: &str) -> Result<i64, ParseTimeError> {
159 Ok(NaiveDateTime::parse_from_str(self, fmt)?
160 .and_utc()
161 .timestamp_millis())
162 }
163}
164
165#[derive(Clone, Copy, Debug, PartialEq)]
166pub enum IntervalSize {
167 Discrete(u64),
168 Temporal { millis: u64, months: u32 },
169}
170
171impl IntervalSize {
172 fn months(months: i64) -> Self {
173 Self::Temporal {
174 millis: 0,
175 months: months as u32,
176 }
177 }
178
179 fn add_temporal(&self, other: IntervalSize) -> IntervalSize {
180 match (self, other) {
181 (
182 Self::Temporal {
183 millis: ml1,
184 months: mt1,
185 },
186 Self::Temporal {
187 millis: ml2,
188 months: mt2,
189 },
190 ) => Self::Temporal {
191 millis: ml1 + ml2,
192 months: mt1 + mt2,
193 },
194 _ => panic!("this function is not supposed to be used with discrete intervals"),
195 }
196 }
197}
198
199impl From<Duration> for IntervalSize {
200 fn from(value: Duration) -> Self {
201 Self::Temporal {
202 millis: value.num_milliseconds() as u64,
203 months: 0,
204 }
205 }
206}
207
208#[derive(Clone, Copy, Debug, PartialEq)]
209pub struct Interval {
210 pub epoch_alignment: bool,
211 pub size: IntervalSize,
212}
213
214impl Default for Interval {
215 fn default() -> Self {
216 Self {
217 epoch_alignment: false,
218 size: IntervalSize::Discrete(1),
219 }
220 }
221}
222
223impl TryFrom<String> for Interval {
224 type Error = ParseTimeError;
225
226 fn try_from(value: String) -> Result<Self, Self::Error> {
227 Self::try_from(value.as_str())
228 }
229}
230
231impl TryFrom<&str> for Interval {
232 type Error = ParseTimeError;
233 fn try_from(value: &str) -> Result<Self, Self::Error> {
234 let trimmed = value.trim();
235 let no_and = trimmed.replace("and", "");
236 let cleaned = {
237 let re = Regex::new(r"[\s&,]+").unwrap();
238 re.replace_all(&no_and, " ")
239 };
240
241 let tokens = cleaned.split(' ').collect_vec();
242
243 if tokens.len() < 2 || tokens.len() % 2 != 0 {
244 return Err(ParseTimeError::InvalidPairs);
245 }
246
247 let (intervals, errors): (Vec<IntervalSize>, Vec<ParseTimeError>) = tokens
248 .chunks(2)
249 .map(|chunk| Self::parse_duration(chunk[0], chunk[1]))
250 .partition_map(|d| match d {
251 Ok(d) => Either::Left(d),
252 Err(e) => Either::Right(e),
253 });
254
255 if errors.is_empty() {
256 Ok(Self {
257 epoch_alignment: true,
258 size: intervals
259 .into_iter()
260 .reduce(|a, b| a.add_temporal(b))
261 .unwrap(),
262 })
263 } else {
264 Err(errors.first().unwrap().clone())
265 }
266 }
267}
268
269impl TryFrom<u64> for Interval {
270 type Error = ParseTimeError;
271 fn try_from(value: u64) -> Result<Self, Self::Error> {
272 Ok(Self {
273 epoch_alignment: false,
274 size: IntervalSize::Discrete(value),
275 })
276 }
277}
278
279impl TryFrom<u32> for Interval {
280 type Error = ParseTimeError;
281 fn try_from(value: u32) -> Result<Self, Self::Error> {
282 Ok(Self {
283 epoch_alignment: false,
284 size: IntervalSize::Discrete(value as u64),
285 })
286 }
287}
288
289impl TryFrom<i32> for Interval {
290 type Error = ParseTimeError;
291 fn try_from(value: i32) -> Result<Self, Self::Error> {
292 if value >= 0 {
293 Ok(Self {
294 epoch_alignment: false,
295 size: IntervalSize::Discrete(value as u64),
296 })
297 } else {
298 Err(ParseTimeError::NegativeInt)
299 }
300 }
301}
302
303impl TryFrom<i64> for Interval {
304 type Error = ParseTimeError;
305
306 fn try_from(value: i64) -> Result<Self, Self::Error> {
307 if value >= 0 {
308 Ok(Self {
309 epoch_alignment: false,
310 size: IntervalSize::Discrete(value as u64),
311 })
312 } else {
313 Err(ParseTimeError::NegativeInt)
314 }
315 }
316}
317
318pub trait TryIntoInterval {
319 fn try_into_interval(self) -> Result<Interval, ParseTimeError>;
320}
321
322impl<T> TryIntoInterval for T
323where
324 Interval: TryFrom<T>,
325 ParseTimeError: From<<Interval as TryFrom<T>>::Error>,
326{
327 fn try_into_interval(self) -> Result<Interval, ParseTimeError> {
328 Ok(self.try_into()?)
329 }
330}
331
332impl Interval {
333 pub fn to_millis(&self) -> Option<u64> {
335 match self.size {
336 IntervalSize::Discrete(millis) => Some(millis),
337 IntervalSize::Temporal { millis, months } => (months == 0).then_some(millis),
338 }
339 }
340
341 fn parse_duration(number: &str, unit: &str) -> Result<IntervalSize, ParseTimeError> {
342 let number: i64 = number.parse::<u64>()? as i64;
343 let duration = match unit {
344 "year" | "years" => IntervalSize::months(number * 12),
345 "month" | "months" => IntervalSize::months(number),
346 "week" | "weeks" => Duration::weeks(number).into(),
347 "day" | "days" => Duration::days(number).into(),
348 "hour" | "hours" => Duration::hours(number).into(),
349 "minute" | "minutes" => Duration::minutes(number).into(),
350 "second" | "seconds" => Duration::seconds(number).into(),
351 "millisecond" | "milliseconds" => Duration::milliseconds(number).into(),
352 unit => return Err(ParseTimeError::InvalidUnit(unit.to_string())),
353 };
354 Ok(duration)
355 }
356
357 pub fn discrete(num: u64) -> Self {
358 Interval {
359 epoch_alignment: false,
360 size: IntervalSize::Discrete(num),
361 }
362 }
363
364 pub fn milliseconds(ms: i64) -> Self {
365 Interval {
366 epoch_alignment: true,
367 size: IntervalSize::from(Duration::milliseconds(ms)),
368 }
369 }
370
371 pub fn seconds(seconds: i64) -> Self {
372 Interval {
373 epoch_alignment: true,
374 size: IntervalSize::from(Duration::seconds(seconds)),
375 }
376 }
377
378 pub fn minutes(minutes: i64) -> Self {
379 Interval {
380 epoch_alignment: true,
381 size: IntervalSize::from(Duration::minutes(minutes)),
382 }
383 }
384
385 pub fn hours(hours: i64) -> Self {
386 Interval {
387 epoch_alignment: true,
388 size: IntervalSize::from(Duration::hours(hours)),
389 }
390 }
391
392 pub fn days(days: i64) -> Self {
393 Interval {
394 epoch_alignment: true,
395 size: IntervalSize::from(Duration::days(days)),
396 }
397 }
398
399 pub fn weeks(weeks: i64) -> Self {
400 Interval {
401 epoch_alignment: true,
402 size: IntervalSize::from(Duration::weeks(weeks)),
403 }
404 }
405
406 pub fn months(months: i64) -> Self {
407 Interval {
408 epoch_alignment: true,
409 size: IntervalSize::months(months),
410 }
411 }
412
413 pub fn years(years: i64) -> Self {
414 Interval {
415 epoch_alignment: true,
416 size: IntervalSize::months(12 * years),
417 }
418 }
419
420 pub fn and(&self, other: &Self) -> Result<Self, IntervalTypeError> {
421 match (self.size, other.size) {
422 (IntervalSize::Discrete(l), IntervalSize::Discrete(r)) => Ok(Interval {
423 epoch_alignment: false,
424 size: IntervalSize::Discrete(l + r),
425 }),
426 (IntervalSize::Temporal { .. }, IntervalSize::Temporal { .. }) => Ok(Interval {
427 epoch_alignment: true,
428 size: self.size.add_temporal(other.size),
429 }),
430 (_, _) => Err(IntervalTypeError()),
431 }
432 }
433}
434
435#[derive(thiserror::Error, Debug)]
436#[error("Discrete and temporal intervals cannot be combined")]
437pub struct IntervalTypeError();
438
439impl Sub<Interval> for i64 {
440 type Output = i64;
441 fn sub(self, rhs: Interval) -> Self::Output {
442 match rhs.size {
443 IntervalSize::Discrete(number) => self - (number as i64),
444 IntervalSize::Temporal { millis, months } => {
445 let datetime = DateTime::from_timestamp_millis(self - millis as i64)
449 .unwrap_or_else(|| {
450 panic!("{self} cannot be interpreted as a milliseconds timestamp")
451 })
452 .naive_utc();
453 (datetime - Months::new(months))
454 .and_utc()
455 .timestamp_millis()
456 }
457 }
458 }
459}
460
461impl Add<Interval> for i64 {
462 type Output = i64;
463 fn add(self, rhs: Interval) -> Self::Output {
464 match rhs.size {
465 IntervalSize::Discrete(number) => self + (number as i64),
466 IntervalSize::Temporal { millis, months } => {
467 let datetime = DateTime::from_timestamp_millis(self)
471 .unwrap_or_else(|| {
472 panic!("{self} cannot be interpreted as a milliseconds timestamp")
473 })
474 .naive_utc();
475 (datetime + Months::new(months))
476 .and_utc()
477 .timestamp_millis()
478 + millis as i64
479 }
480 }
481 }
482}
483
484#[cfg(test)]
485mod time_tests {
486 use crate::utils::time::{Interval, ParseTimeError, TryIntoTime};
487
488 #[test]
489 fn interval_parsing() {
490 let second: u64 = 1000;
491 let minute = 60 * second;
492 let hour = 60 * minute;
493 let day = 24 * hour;
494 let week = 7 * day;
495
496 let interval: Interval = "1 day".try_into().unwrap();
497 assert_eq!(interval.to_millis().unwrap(), day);
498
499 let interval: Interval = "1 week".try_into().unwrap();
500 assert_eq!(interval.to_millis().unwrap(), week);
501
502 let interval: Interval = "4 weeks and 1 day".try_into().unwrap();
503 assert_eq!(interval.to_millis().unwrap(), 4 * week + day);
504
505 let interval: Interval = "2 days & 1 millisecond".try_into().unwrap();
506 assert_eq!(interval.to_millis().unwrap(), 2 * day + 1);
507
508 let interval: Interval = "2 days, 1 hour, and 2 minutes".try_into().unwrap();
509 assert_eq!(interval.to_millis().unwrap(), 2 * day + hour + 2 * minute);
510
511 let interval: Interval = "1 weeks , 1 minute".try_into().unwrap();
512 assert_eq!(interval.to_millis().unwrap(), week + minute);
513
514 let interval: Interval = "23 seconds and 34 millisecond and 1 minute"
515 .try_into()
516 .unwrap();
517 assert_eq!(interval.to_millis().unwrap(), 23 * second + 34 + minute);
518 }
519
520 #[test]
521 fn interval_parsing_with_months_and_years() {
522 let dt = "2020-01-01 00:00:00".try_into_time().unwrap();
523
524 let two_months: Interval = "2 months".try_into().unwrap();
525 let dt_plus_2_months = "2020-03-01 00:00:00".try_into_time().unwrap();
526 assert_eq!(dt + two_months, dt_plus_2_months);
527
528 let two_years: Interval = "2 years".try_into().unwrap();
529 let dt_plus_2_years = "2022-01-01 00:00:00".try_into_time().unwrap();
530 assert_eq!(dt + two_years, dt_plus_2_years);
531
532 let mix_interval: Interval = "1 year 1 month and 1 second".try_into().unwrap();
533 let dt_mix = "2021-02-01 00:00:01".try_into_time().unwrap();
534 assert_eq!(dt + mix_interval, dt_mix);
535 }
536
537 #[test]
538 fn invalid_intervals() {
539 let result: Result<Interval, ParseTimeError> = "".try_into();
540 assert_eq!(result, Err(ParseTimeError::InvalidPairs));
541
542 let result: Result<Interval, ParseTimeError> = "1".try_into();
543 assert_eq!(result, Err(ParseTimeError::InvalidPairs));
544
545 let result: Result<Interval, ParseTimeError> = "1 day and 5".try_into();
546 assert_eq!(result, Err(ParseTimeError::InvalidPairs));
547
548 let result: Result<Interval, ParseTimeError> = "1 daay".try_into();
549 assert_eq!(result, Err(ParseTimeError::InvalidUnit("daay".to_string())));
550
551 let result: Result<Interval, ParseTimeError> = "day 1".try_into();
552
553 match result {
554 Err(ParseTimeError::ParseInt { .. }) => (),
555 _ => panic!(),
556 }
557 }
558}