Skip to main content

raphtory_core/utils/
time.rs

1use chrono::{DateTime, Datelike, Duration, Months, NaiveDate};
2use itertools::Itertools;
3use raphtory_api::core::{storage::timeindex::EventTime, utils::time::ParseTimeError};
4use regex::Regex;
5use std::ops::{Add, Mul, Sub};
6
7pub(crate) const SECOND_MS: i64 = 1000;
8pub(crate) const MINUTE_MS: i64 = 60 * SECOND_MS;
9pub(crate) const HOUR_MS: i64 = 60 * MINUTE_MS;
10pub(crate) const DAY_MS: i64 = 24 * HOUR_MS;
11pub(crate) const WEEK_MS: i64 = 7 * DAY_MS;
12
13#[derive(Clone, Copy, Debug, PartialEq)]
14pub enum IntervalSize {
15    Discrete(u64),
16    /// `months` is u32 because chrono::Months works with u32.
17    Temporal {
18        millis: u64,
19        months: u32,
20    },
21}
22
23impl IntervalSize {
24    /// Creates a 0 sized temporal `IntervalSize`. Should not be used to create Windows.
25    pub fn empty_temporal() -> Self {
26        IntervalSize::Temporal {
27            millis: 0,
28            months: 0,
29        }
30    }
31
32    fn months(months: i64) -> Self {
33        Self::Temporal {
34            millis: 0,
35            months: months as u32,
36        }
37    }
38
39    fn add_temporal(&self, other: IntervalSize) -> IntervalSize {
40        match (self, other) {
41            (
42                Self::Temporal {
43                    millis: ml1,
44                    months: mt1,
45                },
46                Self::Temporal {
47                    millis: ml2,
48                    months: mt2,
49                },
50            ) => Self::Temporal {
51                millis: ml1 + ml2,
52                months: mt1 + mt2,
53            },
54            _ => panic!("this function is not supposed to be used with discrete intervals"),
55        }
56    }
57}
58
59impl From<Duration> for IntervalSize {
60    fn from(value: Duration) -> Self {
61        Self::Temporal {
62            millis: value.num_milliseconds() as u64,
63            months: 0,
64        }
65    }
66}
67
68/// Used to keep track of the smallest unit provided, so that we can line up windows
69/// (eg. at the start of the hour, week, year, ...)
70#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
71pub enum AlignmentUnit {
72    Unaligned, // note that there is no functional difference between millisecond and unaligned for the time being
73    Millisecond,
74    Second,
75    Minute,
76    Hour,
77    Day,
78    Week,
79    Month,
80    Year,
81}
82
83impl AlignmentUnit {
84    /// Floors a UTC timestamp in milliseconds since Unix epoch to the nearest alignment unit.
85    pub fn align_timestamp(&self, timestamp: i64) -> i64 {
86        match self {
87            AlignmentUnit::Unaligned => timestamp,
88            AlignmentUnit::Millisecond => timestamp,
89            AlignmentUnit::Second => Self::floor_ms(timestamp, SECOND_MS),
90            AlignmentUnit::Minute => Self::floor_ms(timestamp, MINUTE_MS),
91            AlignmentUnit::Hour => Self::floor_ms(timestamp, HOUR_MS),
92            AlignmentUnit::Day => Self::floor_ms(timestamp, DAY_MS),
93            AlignmentUnit::Week => Self::floor_ms(timestamp, WEEK_MS),
94            // Month and Year are variable (28, 30, or 31 days / 365 or 366 days so we can't simply use division)
95            AlignmentUnit::Month => {
96                let naive = DateTime::from_timestamp_millis(timestamp)
97                    .unwrap_or_else(|| {
98                        panic!("{timestamp} cannot be interpreted as a milliseconds timestamp.")
99                    })
100                    .naive_utc();
101                let y = naive.year();
102                let m = naive.month();
103                NaiveDate::from_ymd_opt(y, m, 1)
104                    .unwrap()
105                    .and_hms_milli_opt(0, 0, 0, 0)
106                    .unwrap()
107                    .and_utc()
108                    .timestamp_millis()
109            }
110            AlignmentUnit::Year => {
111                let naive = DateTime::from_timestamp_millis(timestamp)
112                    .unwrap_or_else(|| {
113                        panic!("{timestamp} cannot be interpreted as a milliseconds timestamp.")
114                    })
115                    .naive_utc();
116                let y = naive.year();
117                NaiveDate::from_ymd_opt(y, 1, 1)
118                    .unwrap()
119                    .and_hms_milli_opt(0, 0, 0, 0)
120                    .unwrap()
121                    .and_utc()
122                    .timestamp_millis()
123            }
124        }
125    }
126
127    /// Floors `ts` to a multiple of `unit_ms` using a remainder that is always non-negative,
128    /// so the result is the boundary at or before `ts`, even for negative timestamps.
129    #[inline]
130    fn floor_ms(ts: i64, unit_ms: i64) -> i64 {
131        ts - ts.rem_euclid(unit_ms)
132    }
133}
134
135impl TryFrom<String> for AlignmentUnit {
136    type Error = ParseTimeError;
137
138    fn try_from(value: String) -> Result<Self, Self::Error> {
139        Self::try_from(value.as_str())
140    }
141}
142
143impl TryFrom<&str> for AlignmentUnit {
144    type Error = ParseTimeError;
145
146    fn try_from(value: &str) -> Result<Self, Self::Error> {
147        let unit = match value.to_lowercase().as_str() {
148            "year" | "years" => AlignmentUnit::Year,
149            "month" | "months" => AlignmentUnit::Month,
150            "week" | "weeks" => AlignmentUnit::Week,
151            "day" | "days" => AlignmentUnit::Day,
152            "hour" | "hours" => AlignmentUnit::Hour,
153            "minute" | "minutes" => AlignmentUnit::Minute,
154            "second" | "seconds" => AlignmentUnit::Second,
155            "millisecond" | "milliseconds" => AlignmentUnit::Millisecond,
156            "unaligned" => AlignmentUnit::Unaligned,
157            unit => return Err(ParseTimeError::InvalidAlignmentUnit(unit.to_string())),
158        };
159        Ok(unit)
160    }
161}
162
163#[derive(Clone, Copy, Debug, PartialEq)]
164pub struct Interval {
165    /// Used if the `IntervalSize` is Temporal, keeps track of the smallest unit passed to line up windows.
166    /// (eg. at the start of the hour, week, year, ...). If the IntervalSize is discrete, this is `None`.
167    pub alignment_unit: Option<AlignmentUnit>,
168    /// The interval.
169    pub size: IntervalSize,
170}
171
172impl Default for Interval {
173    fn default() -> Self {
174        Self {
175            alignment_unit: None,
176            size: IntervalSize::Discrete(1),
177        }
178    }
179}
180
181impl TryFrom<String> for Interval {
182    type Error = ParseTimeError;
183
184    fn try_from(value: String) -> Result<Self, Self::Error> {
185        Self::try_from(value.as_str())
186    }
187}
188
189impl TryFrom<&str> for Interval {
190    type Error = ParseTimeError;
191    fn try_from(value: &str) -> Result<Self, Self::Error> {
192        let trimmed = value.trim();
193        let no_and = trimmed.replace("and", "");
194        let cleaned = {
195            let re = Regex::new(r"[\s&,]+").unwrap();
196            re.replace_all(&no_and, " ")
197        };
198
199        let tokens = cleaned.split(' ').collect_vec();
200
201        if tokens.len() < 2 || tokens.len() % 2 != 0 {
202            return Err(ParseTimeError::InvalidPairs);
203        }
204
205        let (temporal_sum, smallest_unit): (IntervalSize, AlignmentUnit) =
206            tokens.chunks(2).try_fold(
207                (IntervalSize::empty_temporal(), AlignmentUnit::Year), // start with the largest alignment unit
208                |(sum, smallest), chunk| {
209                    let (interval, unit) = Self::parse_duration(chunk[0], chunk[1])?;
210                    Ok::<_, ParseTimeError>((sum.add_temporal(interval), smallest.min(unit)))
211                },
212            )?;
213
214        Ok(Self {
215            alignment_unit: Some(smallest_unit),
216            size: temporal_sum,
217        })
218    }
219}
220
221impl TryFrom<u64> for Interval {
222    type Error = ParseTimeError;
223    fn try_from(value: u64) -> Result<Self, Self::Error> {
224        Ok(Self {
225            alignment_unit: None,
226            size: IntervalSize::Discrete(value),
227        })
228    }
229}
230
231impl TryFrom<u32> for Interval {
232    type Error = ParseTimeError;
233    fn try_from(value: u32) -> Result<Self, Self::Error> {
234        Ok(Self {
235            alignment_unit: None,
236            size: IntervalSize::Discrete(value as u64),
237        })
238    }
239}
240
241impl TryFrom<i32> for Interval {
242    type Error = ParseTimeError;
243    fn try_from(value: i32) -> Result<Self, Self::Error> {
244        if value >= 0 {
245            Ok(Self {
246                alignment_unit: None,
247                size: IntervalSize::Discrete(value as u64),
248            })
249        } else {
250            Err(ParseTimeError::NegativeInt)
251        }
252    }
253}
254
255impl TryFrom<i64> for Interval {
256    type Error = ParseTimeError;
257
258    fn try_from(value: i64) -> Result<Self, Self::Error> {
259        if value >= 0 {
260            Ok(Self {
261                alignment_unit: None,
262                size: IntervalSize::Discrete(value as u64),
263            })
264        } else {
265            Err(ParseTimeError::NegativeInt)
266        }
267    }
268}
269
270pub trait TryIntoInterval {
271    fn try_into_interval(self) -> Result<Interval, ParseTimeError>;
272}
273
274impl<T> TryIntoInterval for T
275where
276    Interval: TryFrom<T>,
277    ParseTimeError: From<<Interval as TryFrom<T>>::Error>,
278{
279    fn try_into_interval(self) -> Result<Interval, ParseTimeError> {
280        Ok(self.try_into()?)
281    }
282}
283
284impl Interval {
285    /// Return an option because there might be no exact translation to millis for some intervals
286    pub fn to_millis(&self) -> Option<u64> {
287        match self.size {
288            IntervalSize::Discrete(millis) => Some(millis),
289            IntervalSize::Temporal { millis, months } => (months == 0).then_some(millis),
290        }
291    }
292
293    fn parse_duration(
294        number: &str,
295        unit: &str,
296    ) -> Result<(IntervalSize, AlignmentUnit), ParseTimeError> {
297        let number: i64 = number.parse::<u64>()? as i64;
298        let duration = match unit {
299            "year" | "years" => (IntervalSize::months(number * 12), AlignmentUnit::Year),
300            "month" | "months" => (IntervalSize::months(number), AlignmentUnit::Month),
301            "week" | "weeks" => (Duration::weeks(number).into(), AlignmentUnit::Week),
302            "day" | "days" => (Duration::days(number).into(), AlignmentUnit::Day),
303            "hour" | "hours" => (Duration::hours(number).into(), AlignmentUnit::Hour),
304            "minute" | "minutes" => (Duration::minutes(number).into(), AlignmentUnit::Minute),
305            "second" | "seconds" => (Duration::seconds(number).into(), AlignmentUnit::Second),
306            "millisecond" | "milliseconds" => (
307                Duration::milliseconds(number).into(),
308                AlignmentUnit::Millisecond,
309            ),
310            unit => return Err(ParseTimeError::InvalidUnit(unit.to_string())),
311        };
312        Ok(duration)
313    }
314
315    pub fn discrete(num: u64) -> Self {
316        Interval {
317            alignment_unit: None,
318            size: IntervalSize::Discrete(num),
319        }
320    }
321
322    pub fn milliseconds(ms: i64) -> Self {
323        Interval {
324            alignment_unit: Some(AlignmentUnit::Millisecond),
325            size: IntervalSize::from(Duration::milliseconds(ms)),
326        }
327    }
328
329    pub fn seconds(seconds: i64) -> Self {
330        Interval {
331            alignment_unit: Some(AlignmentUnit::Second),
332            size: IntervalSize::from(Duration::seconds(seconds)),
333        }
334    }
335
336    pub fn minutes(minutes: i64) -> Self {
337        Interval {
338            alignment_unit: Some(AlignmentUnit::Minute),
339            size: IntervalSize::from(Duration::minutes(minutes)),
340        }
341    }
342
343    pub fn hours(hours: i64) -> Self {
344        Interval {
345            alignment_unit: Some(AlignmentUnit::Hour),
346            size: IntervalSize::from(Duration::hours(hours)),
347        }
348    }
349
350    pub fn days(days: i64) -> Self {
351        Interval {
352            alignment_unit: Some(AlignmentUnit::Day),
353            size: IntervalSize::from(Duration::days(days)),
354        }
355    }
356
357    pub fn weeks(weeks: i64) -> Self {
358        Interval {
359            alignment_unit: Some(AlignmentUnit::Week),
360            size: IntervalSize::from(Duration::weeks(weeks)),
361        }
362    }
363
364    pub fn months(months: i64) -> Self {
365        Interval {
366            alignment_unit: Some(AlignmentUnit::Month),
367            size: IntervalSize::months(months),
368        }
369    }
370
371    pub fn years(years: i64) -> Self {
372        Interval {
373            alignment_unit: Some(AlignmentUnit::Year),
374            size: IntervalSize::months(12 * years),
375        }
376    }
377
378    pub fn and(&self, other: &Self) -> Result<Self, IntervalTypeError> {
379        match (self.size, other.size) {
380            (IntervalSize::Discrete(l), IntervalSize::Discrete(r)) => Ok(Interval {
381                alignment_unit: None,
382                size: IntervalSize::Discrete(l + r),
383            }),
384            (IntervalSize::Temporal { .. }, IntervalSize::Temporal { .. }) => Ok(Interval {
385                alignment_unit: self.alignment_unit.min(other.alignment_unit),
386                size: self.size.add_temporal(other.size),
387            }),
388            (_, _) => Err(IntervalTypeError()),
389        }
390    }
391}
392
393#[derive(thiserror::Error, Debug)]
394#[error("Discrete and temporal intervals cannot be combined")]
395pub struct IntervalTypeError();
396
397impl Sub<Interval> for i64 {
398    type Output = i64;
399    fn sub(self, rhs: Interval) -> Self::Output {
400        match rhs.size {
401            IntervalSize::Discrete(number)
402            | IntervalSize::Temporal {
403                millis: number,
404                months: 0,
405            } => self - (number as i64),
406            IntervalSize::Temporal { millis, months } => {
407                // first we subtract the number of milliseconds and then the number of months for
408                // consistency with the implementation of Add (we revert back the steps) so we
409                // guarantee that:  time + interval - interval = time
410                let datetime = DateTime::from_timestamp_millis(self - millis as i64)
411                    .unwrap_or_else(|| {
412                        panic!("{self} cannot be interpreted as a milliseconds timestamp")
413                    })
414                    .naive_utc();
415                (datetime - Months::new(months))
416                    .and_utc()
417                    .timestamp_millis()
418            }
419        }
420    }
421}
422
423impl Add<Interval> for i64 {
424    type Output = i64;
425    fn add(self, rhs: Interval) -> Self::Output {
426        match rhs.size {
427            IntervalSize::Discrete(number)
428            | IntervalSize::Temporal {
429                millis: number,
430                months: 0,
431            } => self + (number as i64),
432            IntervalSize::Temporal { millis, months } => {
433                // first we add the number of months and then the number of milliseconds for
434                // consistency with the implementation of Sub (we revert back the steps) so we
435                // guarantee that:  time + interval - interval = time
436                let datetime = DateTime::from_timestamp_millis(self)
437                    .unwrap_or_else(|| {
438                        panic!("{self} cannot be interpreted as a milliseconds timestamp")
439                    })
440                    .naive_utc();
441                (datetime + Months::new(months))
442                    .and_utc()
443                    .timestamp_millis()
444                    + millis as i64
445            }
446        }
447    }
448}
449
450// since all IntervalSize values (discrete number and temporal millis/months) are unsigned,
451// we can only multiply with unsigned numbers.
452impl Mul<Interval> for u32 {
453    type Output = Interval;
454
455    fn mul(self, rhs: Interval) -> Self::Output {
456        match rhs.size {
457            IntervalSize::Discrete(number) => Interval {
458                alignment_unit: rhs.alignment_unit, // alignment_unit should be None
459                size: IntervalSize::Discrete((self as u64) * number),
460            },
461            IntervalSize::Temporal { millis, months } => Interval {
462                alignment_unit: rhs.alignment_unit,
463                size: IntervalSize::Temporal {
464                    millis: (self as u64) * millis,
465                    months: self * months,
466                },
467            },
468        }
469    }
470}
471
472impl Add<Interval> for EventTime {
473    type Output = EventTime;
474    fn add(self, rhs: Interval) -> Self::Output {
475        match rhs.size {
476            IntervalSize::Discrete(number) => EventTime(self.0 + (number as i64), self.1),
477            IntervalSize::Temporal { millis, months } => {
478                // first we add the number of months and then the number of milliseconds for
479                // consistency with the implementation of Sub (we revert back the steps) so we
480                // guarantee that:  time + interval - interval = time
481                let datetime = DateTime::from_timestamp_millis(self.0)
482                    .unwrap_or_else(|| {
483                        panic!("{self} cannot be interpreted as a milliseconds timestamp")
484                    })
485                    .naive_utc();
486                let timestamp = (datetime + Months::new(months))
487                    .and_utc()
488                    .timestamp_millis()
489                    + millis as i64;
490                EventTime(timestamp, self.1)
491            }
492        }
493    }
494}
495
496#[cfg(test)]
497mod time_tests {
498    use crate::utils::time::Interval;
499    use raphtory_api::core::utils::time::{ParseTimeError, TryIntoTime};
500
501    #[test]
502    fn interval_parsing() {
503        let second: u64 = 1000;
504        let minute = 60 * second;
505        let hour = 60 * minute;
506        let day = 24 * hour;
507        let week = 7 * day;
508
509        let interval: Interval = "1 day".try_into().unwrap();
510        assert_eq!(interval.to_millis().unwrap(), day);
511
512        let interval: Interval = "1 week".try_into().unwrap();
513        assert_eq!(interval.to_millis().unwrap(), week);
514
515        let interval: Interval = "4 weeks and 1 day".try_into().unwrap();
516        assert_eq!(interval.to_millis().unwrap(), 4 * week + day);
517
518        let interval: Interval = "2 days & 1 millisecond".try_into().unwrap();
519        assert_eq!(interval.to_millis().unwrap(), 2 * day + 1);
520
521        let interval: Interval = "2 days, 1 hour, and 2 minutes".try_into().unwrap();
522        assert_eq!(interval.to_millis().unwrap(), 2 * day + hour + 2 * minute);
523
524        let interval: Interval = "1 weeks ,   1 minute".try_into().unwrap();
525        assert_eq!(interval.to_millis().unwrap(), week + minute);
526
527        let interval: Interval = "23 seconds  and 34 millisecond and 1 minute"
528            .try_into()
529            .unwrap();
530        assert_eq!(interval.to_millis().unwrap(), 23 * second + 34 + minute);
531    }
532
533    #[test]
534    fn interval_parsing_with_months_and_years() {
535        let dt = "2020-01-01 00:00:00".try_into_time().unwrap();
536
537        let two_months: Interval = "2 months".try_into().unwrap();
538        let dt_plus_2_months = "2020-03-01 00:00:00".try_into_time().unwrap();
539        assert_eq!(dt + two_months, dt_plus_2_months);
540
541        let two_years: Interval = "2 years".try_into().unwrap();
542        let dt_plus_2_years = "2022-01-01 00:00:00".try_into_time().unwrap();
543        assert_eq!(dt + two_years, dt_plus_2_years);
544
545        let mix_interval: Interval = "1 year 1 month and 1 second".try_into().unwrap();
546        let dt_mix = "2021-02-01 00:00:01".try_into_time().unwrap();
547        assert_eq!(dt + mix_interval, dt_mix);
548    }
549
550    #[test]
551    fn invalid_intervals() {
552        let result: Result<Interval, ParseTimeError> = "".try_into();
553        assert_eq!(result, Err(ParseTimeError::InvalidPairs));
554
555        let result: Result<Interval, ParseTimeError> = "1".try_into();
556        assert_eq!(result, Err(ParseTimeError::InvalidPairs));
557
558        let result: Result<Interval, ParseTimeError> = "1 day and 5".try_into();
559        assert_eq!(result, Err(ParseTimeError::InvalidPairs));
560
561        let result: Result<Interval, ParseTimeError> = "1 daay".try_into();
562        assert_eq!(result, Err(ParseTimeError::InvalidUnit("daay".to_string())));
563
564        let result: Result<Interval, ParseTimeError> = "day 1".try_into();
565
566        match result {
567            Err(ParseTimeError::ParseInt { .. }) => (),
568            _ => panic!(),
569        }
570    }
571}