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 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
127pub 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 Temporal {
178 millis: u64,
179 months: u32,
180 },
181}
182
183impl IntervalSize {
184 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#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
231pub enum AlignmentUnit {
232 Unaligned, Millisecond,
234 Second,
235 Minute,
236 Hour,
237 Day,
238 Week,
239 Month,
240 Year,
241}
242
243impl AlignmentUnit {
244 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 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 #[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 pub alignment_unit: Option<AlignmentUnit>,
328 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), |(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 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 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 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
610impl 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, 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 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}