raphtory_core/utils/
time.rs

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    /// Tries to parse the timestamp as RFC3339 and then as ISO 8601 with local format and all
74    /// fields mandatory except for milliseconds and allows replacing the T with a space
75    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
119/// Used to handle automatic injection of secondary index if not explicitly provided
120pub 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    /// Return an option because there might be no exact translation to millis for some intervals
334    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                // first we subtract the number of milliseconds and then the number of months for
446                // consistency with the implementation of Add (we revert back the steps) so we
447                // guarantee that:  time + interval - interval = time
448                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                // first we add the number of months and then the number of milliseconds for
468                // consistency with the implementation of Sub (we revert back the steps) so we
469                // guarantee that:  time + interval - interval = time
470                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}