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 Temporal {
18 millis: u64,
19 months: u32,
20 },
21}
22
23impl IntervalSize {
24 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#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
71pub enum AlignmentUnit {
72 Unaligned, Millisecond,
74 Second,
75 Minute,
76 Hour,
77 Day,
78 Week,
79 Month,
80 Year,
81}
82
83impl AlignmentUnit {
84 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 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 #[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 pub alignment_unit: Option<AlignmentUnit>,
168 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), |(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 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 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 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
450impl 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, 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 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}