raphtory_core/utils/
time.rs

1use chrono::{DateTime, Datelike, Duration, Months, NaiveDate, NaiveDateTime, TimeZone};
2use itertools::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, ops::Mul};
12
13pub(crate) const SECOND_MS: i64 = 1000;
14pub(crate) const MINUTE_MS: i64 = 60 * SECOND_MS;
15pub(crate) const HOUR_MS: i64 = 60 * MINUTE_MS;
16pub(crate) const DAY_MS: i64 = 24 * HOUR_MS;
17pub(crate) const WEEK_MS: i64 = 7 * DAY_MS;
18
19#[derive(thiserror::Error, Debug, Clone, PartialEq)]
20pub enum ParseTimeError {
21    #[error("the interval string doesn't contain a complete number of number-unit pairs")]
22    InvalidPairs,
23    #[error("one of the tokens in the interval string supposed to be a number couldn't be parsed")]
24    ParseInt {
25        #[from]
26        source: ParseIntError,
27    },
28    #[error("'{0}' is not a valid unit. Valid units are year(s), month(s), week(s), day(s), hour(s), minute(s), second(s) and millisecond(s).")]
29    InvalidUnit(String),
30    #[error("'{0}' is not a valid unit. Valid units are year(s), month(s), week(s), day(s), hour(s), minute(s), second(s), millisecond(s), and unaligned.")]
31    InvalidAlignmentUnit(String),
32    #[error(transparent)]
33    ParseError(#[from] ParseError),
34    #[error("negative interval is not supported")]
35    NegativeInt,
36    #[error("0 size step is not supported")]
37    ZeroSizeStep,
38    #[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%")]
39    InvalidDateTimeString(String),
40}
41
42impl From<Infallible> for ParseTimeError {
43    fn from(value: Infallible) -> Self {
44        match value {}
45    }
46}
47
48pub trait IntoTime {
49    fn into_time(self) -> i64;
50}
51
52impl IntoTime for i64 {
53    fn into_time(self) -> i64 {
54        self
55    }
56}
57
58impl<Tz: TimeZone> IntoTime for DateTime<Tz> {
59    fn into_time(self) -> i64 {
60        self.timestamp_millis()
61    }
62}
63
64impl IntoTime for NaiveDateTime {
65    fn into_time(self) -> i64 {
66        self.and_utc().timestamp_millis()
67    }
68}
69
70pub trait TryIntoTime {
71    fn try_into_time(self) -> Result<i64, ParseTimeError>;
72}
73
74impl<T: IntoTime> TryIntoTime for T {
75    fn try_into_time(self) -> Result<i64, ParseTimeError> {
76        Ok(self.into_time())
77    }
78}
79
80impl TryIntoTime for &str {
81    /// Tries to parse the timestamp as RFC3339 and then as ISO 8601 with local format and all
82    /// fields mandatory except for milliseconds and allows replacing the T with a space
83    fn try_into_time(self) -> Result<i64, ParseTimeError> {
84        let rfc_result = DateTime::parse_from_rfc3339(self);
85        if let Ok(datetime) = rfc_result {
86            return Ok(datetime.timestamp_millis());
87        }
88
89        let result = DateTime::parse_from_rfc2822(self);
90        if let Ok(datetime) = result {
91            return Ok(datetime.timestamp_millis());
92        }
93
94        let result = NaiveDate::parse_from_str(self, "%Y-%m-%d");
95        if let Ok(date) = result {
96            return Ok(date
97                .and_hms_opt(00, 00, 00)
98                .unwrap()
99                .and_utc()
100                .timestamp_millis());
101        }
102
103        let result = NaiveDateTime::parse_from_str(self, "%Y-%m-%dT%H:%M:%S%.3f");
104        if let Ok(datetime) = result {
105            return Ok(datetime.and_utc().timestamp_millis());
106        }
107
108        let result = NaiveDateTime::parse_from_str(self, "%Y-%m-%dT%H:%M:%S%");
109        if let Ok(datetime) = result {
110            return Ok(datetime.and_utc().timestamp_millis());
111        }
112
113        let result = NaiveDateTime::parse_from_str(self, "%Y-%m-%d %H:%M:%S%.3f");
114        if let Ok(datetime) = result {
115            return Ok(datetime.and_utc().timestamp_millis());
116        }
117
118        let result = NaiveDateTime::parse_from_str(self, "%Y-%m-%d %H:%M:%S%");
119        if let Ok(datetime) = result {
120            return Ok(datetime.and_utc().timestamp_millis());
121        }
122
123        Err(ParseTimeError::InvalidDateTimeString(self.to_string()))
124    }
125}
126
127/// Used to handle automatic injection of secondary index if not explicitly provided
128pub enum InputTime {
129    Simple(i64),
130    Indexed(i64, usize),
131}
132
133pub trait TryIntoInputTime {
134    fn try_into_input_time(self) -> Result<InputTime, ParseTimeError>;
135}
136
137impl TryIntoInputTime for InputTime {
138    fn try_into_input_time(self) -> Result<InputTime, ParseTimeError> {
139        Ok(self)
140    }
141}
142
143impl TryIntoInputTime for TimeIndexEntry {
144    fn try_into_input_time(self) -> Result<InputTime, ParseTimeError> {
145        Ok(InputTime::Indexed(self.t(), self.i()))
146    }
147}
148
149impl<T: TryIntoTime> TryIntoInputTime for T {
150    fn try_into_input_time(self) -> Result<InputTime, ParseTimeError> {
151        Ok(InputTime::Simple(self.try_into_time()?))
152    }
153}
154
155impl<T: TryIntoTime> TryIntoInputTime for (T, usize) {
156    fn try_into_input_time(self) -> Result<InputTime, ParseTimeError> {
157        Ok(InputTime::Indexed(self.0.try_into_time()?, self.1))
158    }
159}
160
161pub trait IntoTimeWithFormat {
162    fn parse_time(&self, fmt: &str) -> Result<i64, ParseTimeError>;
163}
164
165impl IntoTimeWithFormat for &str {
166    fn parse_time(&self, fmt: &str) -> Result<i64, ParseTimeError> {
167        Ok(NaiveDateTime::parse_from_str(self, fmt)?
168            .and_utc()
169            .timestamp_millis())
170    }
171}
172
173#[derive(Clone, Copy, Debug, PartialEq)]
174pub enum IntervalSize {
175    Discrete(u64),
176    /// `months` is u32 because chrono::Months works with u32.
177    Temporal {
178        millis: u64,
179        months: u32,
180    },
181}
182
183impl IntervalSize {
184    /// Creates a 0 sized temporal `IntervalSize`. Should not be used to create Windows.
185    pub fn empty_temporal() -> Self {
186        IntervalSize::Temporal {
187            millis: 0,
188            months: 0,
189        }
190    }
191
192    fn months(months: i64) -> Self {
193        Self::Temporal {
194            millis: 0,
195            months: months as u32,
196        }
197    }
198
199    fn add_temporal(&self, other: IntervalSize) -> IntervalSize {
200        match (self, other) {
201            (
202                Self::Temporal {
203                    millis: ml1,
204                    months: mt1,
205                },
206                Self::Temporal {
207                    millis: ml2,
208                    months: mt2,
209                },
210            ) => Self::Temporal {
211                millis: ml1 + ml2,
212                months: mt1 + mt2,
213            },
214            _ => panic!("this function is not supposed to be used with discrete intervals"),
215        }
216    }
217}
218
219impl From<Duration> for IntervalSize {
220    fn from(value: Duration) -> Self {
221        Self::Temporal {
222            millis: value.num_milliseconds() as u64,
223            months: 0,
224        }
225    }
226}
227
228/// Used to keep track of the smallest unit provided, so that we can line up windows
229/// (eg. at the start of the hour, week, year, ...)
230#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
231pub enum AlignmentUnit {
232    Unaligned, // note that there is no functional difference between millisecond and unaligned for the time being
233    Millisecond,
234    Second,
235    Minute,
236    Hour,
237    Day,
238    Week,
239    Month,
240    Year,
241}
242
243impl AlignmentUnit {
244    /// Floors a UTC timestamp in milliseconds since Unix epoch to the nearest alignment unit.
245    pub fn align_timestamp(&self, timestamp: i64) -> i64 {
246        match self {
247            AlignmentUnit::Unaligned => timestamp,
248            AlignmentUnit::Millisecond => timestamp,
249            AlignmentUnit::Second => Self::floor_ms(timestamp, SECOND_MS),
250            AlignmentUnit::Minute => Self::floor_ms(timestamp, MINUTE_MS),
251            AlignmentUnit::Hour => Self::floor_ms(timestamp, HOUR_MS),
252            AlignmentUnit::Day => Self::floor_ms(timestamp, DAY_MS),
253            AlignmentUnit::Week => Self::floor_ms(timestamp, WEEK_MS),
254            // Month and Year are variable (28, 30, or 31 days / 365 or 366 days so we can't simply use division)
255            AlignmentUnit::Month => {
256                let naive = DateTime::from_timestamp_millis(timestamp)
257                    .unwrap_or_else(|| {
258                        panic!("{timestamp} cannot be interpreted as a milliseconds timestamp.")
259                    })
260                    .naive_utc();
261                let y = naive.year();
262                let m = naive.month();
263                NaiveDate::from_ymd_opt(y, m, 1)
264                    .unwrap()
265                    .and_hms_milli_opt(0, 0, 0, 0)
266                    .unwrap()
267                    .and_utc()
268                    .timestamp_millis()
269            }
270            AlignmentUnit::Year => {
271                let naive = DateTime::from_timestamp_millis(timestamp)
272                    .unwrap_or_else(|| {
273                        panic!("{timestamp} cannot be interpreted as a milliseconds timestamp.")
274                    })
275                    .naive_utc();
276                let y = naive.year();
277                NaiveDate::from_ymd_opt(y, 1, 1)
278                    .unwrap()
279                    .and_hms_milli_opt(0, 0, 0, 0)
280                    .unwrap()
281                    .and_utc()
282                    .timestamp_millis()
283            }
284        }
285    }
286
287    /// Floors `ts` to a multiple of `unit_ms` using a remainder that is always non-negative,
288    /// so the result is the boundary at or before `ts`, even for negative timestamps.
289    #[inline]
290    fn floor_ms(ts: i64, unit_ms: i64) -> i64 {
291        ts - ts.rem_euclid(unit_ms)
292    }
293}
294
295impl TryFrom<String> for AlignmentUnit {
296    type Error = ParseTimeError;
297
298    fn try_from(value: String) -> Result<Self, Self::Error> {
299        Self::try_from(value.as_str())
300    }
301}
302
303impl TryFrom<&str> for AlignmentUnit {
304    type Error = ParseTimeError;
305
306    fn try_from(value: &str) -> Result<Self, Self::Error> {
307        let unit = match value.to_lowercase().as_str() {
308            "year" | "years" => AlignmentUnit::Year,
309            "month" | "months" => AlignmentUnit::Month,
310            "week" | "weeks" => AlignmentUnit::Week,
311            "day" | "days" => AlignmentUnit::Day,
312            "hour" | "hours" => AlignmentUnit::Hour,
313            "minute" | "minutes" => AlignmentUnit::Minute,
314            "second" | "seconds" => AlignmentUnit::Second,
315            "millisecond" | "milliseconds" => AlignmentUnit::Millisecond,
316            "unaligned" => AlignmentUnit::Unaligned,
317            unit => return Err(ParseTimeError::InvalidAlignmentUnit(unit.to_string())),
318        };
319        Ok(unit)
320    }
321}
322
323#[derive(Clone, Copy, Debug, PartialEq)]
324pub struct Interval {
325    /// Used if the `IntervalSize` is Temporal, keeps track of the smallest unit passed to line up windows.
326    /// (eg. at the start of the hour, week, year, ...). If the IntervalSize is discrete, this is `None`.
327    pub alignment_unit: Option<AlignmentUnit>,
328    /// The interval.
329    pub size: IntervalSize,
330}
331
332impl Default for Interval {
333    fn default() -> Self {
334        Self {
335            alignment_unit: None,
336            size: IntervalSize::Discrete(1),
337        }
338    }
339}
340
341impl TryFrom<String> for Interval {
342    type Error = ParseTimeError;
343
344    fn try_from(value: String) -> Result<Self, Self::Error> {
345        Self::try_from(value.as_str())
346    }
347}
348
349impl TryFrom<&str> for Interval {
350    type Error = ParseTimeError;
351    fn try_from(value: &str) -> Result<Self, Self::Error> {
352        let trimmed = value.trim();
353        let no_and = trimmed.replace("and", "");
354        let cleaned = {
355            let re = Regex::new(r"[\s&,]+").unwrap();
356            re.replace_all(&no_and, " ")
357        };
358
359        let tokens = cleaned.split(' ').collect_vec();
360
361        if tokens.len() < 2 || tokens.len() % 2 != 0 {
362            return Err(ParseTimeError::InvalidPairs);
363        }
364
365        let (temporal_sum, smallest_unit): (IntervalSize, AlignmentUnit) =
366            tokens.chunks(2).try_fold(
367                (IntervalSize::empty_temporal(), AlignmentUnit::Year), // start with the largest alignment unit
368                |(sum, smallest), chunk| {
369                    let (interval, unit) = Self::parse_duration(chunk[0], chunk[1])?;
370                    Ok::<_, ParseTimeError>((sum.add_temporal(interval), smallest.min(unit)))
371                },
372            )?;
373
374        Ok(Self {
375            alignment_unit: Some(smallest_unit),
376            size: temporal_sum,
377        })
378    }
379}
380
381impl TryFrom<u64> for Interval {
382    type Error = ParseTimeError;
383    fn try_from(value: u64) -> Result<Self, Self::Error> {
384        Ok(Self {
385            alignment_unit: None,
386            size: IntervalSize::Discrete(value),
387        })
388    }
389}
390
391impl TryFrom<u32> for Interval {
392    type Error = ParseTimeError;
393    fn try_from(value: u32) -> Result<Self, Self::Error> {
394        Ok(Self {
395            alignment_unit: None,
396            size: IntervalSize::Discrete(value as u64),
397        })
398    }
399}
400
401impl TryFrom<i32> for Interval {
402    type Error = ParseTimeError;
403    fn try_from(value: i32) -> Result<Self, Self::Error> {
404        if value >= 0 {
405            Ok(Self {
406                alignment_unit: None,
407                size: IntervalSize::Discrete(value as u64),
408            })
409        } else {
410            Err(ParseTimeError::NegativeInt)
411        }
412    }
413}
414
415impl TryFrom<i64> for Interval {
416    type Error = ParseTimeError;
417
418    fn try_from(value: i64) -> Result<Self, Self::Error> {
419        if value >= 0 {
420            Ok(Self {
421                alignment_unit: None,
422                size: IntervalSize::Discrete(value as u64),
423            })
424        } else {
425            Err(ParseTimeError::NegativeInt)
426        }
427    }
428}
429
430pub trait TryIntoInterval {
431    fn try_into_interval(self) -> Result<Interval, ParseTimeError>;
432}
433
434impl<T> TryIntoInterval for T
435where
436    Interval: TryFrom<T>,
437    ParseTimeError: From<<Interval as TryFrom<T>>::Error>,
438{
439    fn try_into_interval(self) -> Result<Interval, ParseTimeError> {
440        Ok(self.try_into()?)
441    }
442}
443
444impl Interval {
445    /// Return an option because there might be no exact translation to millis for some intervals
446    pub fn to_millis(&self) -> Option<u64> {
447        match self.size {
448            IntervalSize::Discrete(millis) => Some(millis),
449            IntervalSize::Temporal { millis, months } => (months == 0).then_some(millis),
450        }
451    }
452
453    fn parse_duration(
454        number: &str,
455        unit: &str,
456    ) -> Result<(IntervalSize, AlignmentUnit), ParseTimeError> {
457        let number: i64 = number.parse::<u64>()? as i64;
458        let duration = match unit {
459            "year" | "years" => (IntervalSize::months(number * 12), AlignmentUnit::Year),
460            "month" | "months" => (IntervalSize::months(number), AlignmentUnit::Month),
461            "week" | "weeks" => (Duration::weeks(number).into(), AlignmentUnit::Week),
462            "day" | "days" => (Duration::days(number).into(), AlignmentUnit::Day),
463            "hour" | "hours" => (Duration::hours(number).into(), AlignmentUnit::Hour),
464            "minute" | "minutes" => (Duration::minutes(number).into(), AlignmentUnit::Minute),
465            "second" | "seconds" => (Duration::seconds(number).into(), AlignmentUnit::Second),
466            "millisecond" | "milliseconds" => (
467                Duration::milliseconds(number).into(),
468                AlignmentUnit::Millisecond,
469            ),
470            unit => return Err(ParseTimeError::InvalidUnit(unit.to_string())),
471        };
472        Ok(duration)
473    }
474
475    pub fn discrete(num: u64) -> Self {
476        Interval {
477            alignment_unit: None,
478            size: IntervalSize::Discrete(num),
479        }
480    }
481
482    pub fn milliseconds(ms: i64) -> Self {
483        Interval {
484            alignment_unit: Some(AlignmentUnit::Millisecond),
485            size: IntervalSize::from(Duration::milliseconds(ms)),
486        }
487    }
488
489    pub fn seconds(seconds: i64) -> Self {
490        Interval {
491            alignment_unit: Some(AlignmentUnit::Second),
492            size: IntervalSize::from(Duration::seconds(seconds)),
493        }
494    }
495
496    pub fn minutes(minutes: i64) -> Self {
497        Interval {
498            alignment_unit: Some(AlignmentUnit::Minute),
499            size: IntervalSize::from(Duration::minutes(minutes)),
500        }
501    }
502
503    pub fn hours(hours: i64) -> Self {
504        Interval {
505            alignment_unit: Some(AlignmentUnit::Hour),
506            size: IntervalSize::from(Duration::hours(hours)),
507        }
508    }
509
510    pub fn days(days: i64) -> Self {
511        Interval {
512            alignment_unit: Some(AlignmentUnit::Day),
513            size: IntervalSize::from(Duration::days(days)),
514        }
515    }
516
517    pub fn weeks(weeks: i64) -> Self {
518        Interval {
519            alignment_unit: Some(AlignmentUnit::Week),
520            size: IntervalSize::from(Duration::weeks(weeks)),
521        }
522    }
523
524    pub fn months(months: i64) -> Self {
525        Interval {
526            alignment_unit: Some(AlignmentUnit::Month),
527            size: IntervalSize::months(months),
528        }
529    }
530
531    pub fn years(years: i64) -> Self {
532        Interval {
533            alignment_unit: Some(AlignmentUnit::Year),
534            size: IntervalSize::months(12 * years),
535        }
536    }
537
538    pub fn and(&self, other: &Self) -> Result<Self, IntervalTypeError> {
539        match (self.size, other.size) {
540            (IntervalSize::Discrete(l), IntervalSize::Discrete(r)) => Ok(Interval {
541                alignment_unit: None,
542                size: IntervalSize::Discrete(l + r),
543            }),
544            (IntervalSize::Temporal { .. }, IntervalSize::Temporal { .. }) => Ok(Interval {
545                alignment_unit: self.alignment_unit.min(other.alignment_unit),
546                size: self.size.add_temporal(other.size),
547            }),
548            (_, _) => Err(IntervalTypeError()),
549        }
550    }
551}
552
553#[derive(thiserror::Error, Debug)]
554#[error("Discrete and temporal intervals cannot be combined")]
555pub struct IntervalTypeError();
556
557impl Sub<Interval> for i64 {
558    type Output = i64;
559    fn sub(self, rhs: Interval) -> Self::Output {
560        match rhs.size {
561            IntervalSize::Discrete(number)
562            | IntervalSize::Temporal {
563                millis: number,
564                months: 0,
565            } => self - (number as i64),
566            IntervalSize::Temporal { millis, months } => {
567                // first we subtract the number of milliseconds and then the number of months for
568                // consistency with the implementation of Add (we revert back the steps) so we
569                // guarantee that:  time + interval - interval = time
570                let datetime = DateTime::from_timestamp_millis(self - millis as i64)
571                    .unwrap_or_else(|| {
572                        panic!("{self} cannot be interpreted as a milliseconds timestamp")
573                    })
574                    .naive_utc();
575                (datetime - Months::new(months))
576                    .and_utc()
577                    .timestamp_millis()
578            }
579        }
580    }
581}
582
583impl Add<Interval> for i64 {
584    type Output = i64;
585    fn add(self, rhs: Interval) -> Self::Output {
586        match rhs.size {
587            IntervalSize::Discrete(number)
588            | IntervalSize::Temporal {
589                millis: number,
590                months: 0,
591            } => self + (number as i64),
592            IntervalSize::Temporal { millis, months } => {
593                // first we add the number of months and then the number of milliseconds for
594                // consistency with the implementation of Sub (we revert back the steps) so we
595                // guarantee that:  time + interval - interval = time
596                let datetime = DateTime::from_timestamp_millis(self)
597                    .unwrap_or_else(|| {
598                        panic!("{self} cannot be interpreted as a milliseconds timestamp")
599                    })
600                    .naive_utc();
601                (datetime + Months::new(months))
602                    .and_utc()
603                    .timestamp_millis()
604                    + millis as i64
605            }
606        }
607    }
608}
609
610// since all IntervalSize values (discrete number and temporal millis/months) are unsigned,
611// we can only multiply with unsigned numbers.
612impl Mul<Interval> for u32 {
613    type Output = Interval;
614
615    fn mul(self, rhs: Interval) -> Self::Output {
616        match rhs.size {
617            IntervalSize::Discrete(number) => Interval {
618                alignment_unit: rhs.alignment_unit, // alignment_unit should be None
619                size: IntervalSize::Discrete((self as u64) * number),
620            },
621            IntervalSize::Temporal { millis, months } => Interval {
622                alignment_unit: rhs.alignment_unit,
623                size: IntervalSize::Temporal {
624                    millis: (self as u64) * millis,
625                    months: self * months,
626                },
627            },
628        }
629    }
630}
631
632impl Add<Interval> for TimeIndexEntry {
633    type Output = TimeIndexEntry;
634    fn add(self, rhs: Interval) -> Self::Output {
635        match rhs.size {
636            IntervalSize::Discrete(number) => TimeIndexEntry(self.0 + (number as i64), self.1),
637            IntervalSize::Temporal { millis, months } => {
638                // first we add the number of months and then the number of milliseconds for
639                // consistency with the implementation of Sub (we revert back the steps) so we
640                // guarantee that:  time + interval - interval = time
641                let datetime = DateTime::from_timestamp_millis(self.0)
642                    .unwrap_or_else(|| {
643                        panic!("TimeIndexEntry[{}, {}] cannot be interpreted as a milliseconds timestamp", self.0, self.1)
644                    })
645                    .naive_utc();
646                let timestamp = (datetime + Months::new(months))
647                    .and_utc()
648                    .timestamp_millis()
649                    + millis as i64;
650                TimeIndexEntry(timestamp, self.1)
651            }
652        }
653    }
654}
655
656#[cfg(test)]
657mod time_tests {
658    use crate::utils::time::{Interval, ParseTimeError, TryIntoTime};
659
660    #[test]
661    fn interval_parsing() {
662        let second: u64 = 1000;
663        let minute = 60 * second;
664        let hour = 60 * minute;
665        let day = 24 * hour;
666        let week = 7 * day;
667
668        let interval: Interval = "1 day".try_into().unwrap();
669        assert_eq!(interval.to_millis().unwrap(), day);
670
671        let interval: Interval = "1 week".try_into().unwrap();
672        assert_eq!(interval.to_millis().unwrap(), week);
673
674        let interval: Interval = "4 weeks and 1 day".try_into().unwrap();
675        assert_eq!(interval.to_millis().unwrap(), 4 * week + day);
676
677        let interval: Interval = "2 days & 1 millisecond".try_into().unwrap();
678        assert_eq!(interval.to_millis().unwrap(), 2 * day + 1);
679
680        let interval: Interval = "2 days, 1 hour, and 2 minutes".try_into().unwrap();
681        assert_eq!(interval.to_millis().unwrap(), 2 * day + hour + 2 * minute);
682
683        let interval: Interval = "1 weeks ,   1 minute".try_into().unwrap();
684        assert_eq!(interval.to_millis().unwrap(), week + minute);
685
686        let interval: Interval = "23 seconds  and 34 millisecond and 1 minute"
687            .try_into()
688            .unwrap();
689        assert_eq!(interval.to_millis().unwrap(), 23 * second + 34 + minute);
690    }
691
692    #[test]
693    fn interval_parsing_with_months_and_years() {
694        let dt = "2020-01-01 00:00:00".try_into_time().unwrap();
695
696        let two_months: Interval = "2 months".try_into().unwrap();
697        let dt_plus_2_months = "2020-03-01 00:00:00".try_into_time().unwrap();
698        assert_eq!(dt + two_months, dt_plus_2_months);
699
700        let two_years: Interval = "2 years".try_into().unwrap();
701        let dt_plus_2_years = "2022-01-01 00:00:00".try_into_time().unwrap();
702        assert_eq!(dt + two_years, dt_plus_2_years);
703
704        let mix_interval: Interval = "1 year 1 month and 1 second".try_into().unwrap();
705        let dt_mix = "2021-02-01 00:00:01".try_into_time().unwrap();
706        assert_eq!(dt + mix_interval, dt_mix);
707    }
708
709    #[test]
710    fn invalid_intervals() {
711        let result: Result<Interval, ParseTimeError> = "".try_into();
712        assert_eq!(result, Err(ParseTimeError::InvalidPairs));
713
714        let result: Result<Interval, ParseTimeError> = "1".try_into();
715        assert_eq!(result, Err(ParseTimeError::InvalidPairs));
716
717        let result: Result<Interval, ParseTimeError> = "1 day and 5".try_into();
718        assert_eq!(result, Err(ParseTimeError::InvalidPairs));
719
720        let result: Result<Interval, ParseTimeError> = "1 daay".try_into();
721        assert_eq!(result, Err(ParseTimeError::InvalidUnit("daay".to_string())));
722
723        let result: Result<Interval, ParseTimeError> = "day 1".try_into();
724
725        match result {
726            Err(ParseTimeError::ParseInt { .. }) => (),
727            _ => panic!(),
728        }
729    }
730}