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 => {
94 let offset = DAY_MS * 4; Self::floor_ms(timestamp - offset, WEEK_MS) + offset
96 }
97 AlignmentUnit::Month => {
99 let naive = DateTime::from_timestamp_millis(timestamp)
100 .unwrap_or_else(|| {
101 panic!("{timestamp} cannot be interpreted as a milliseconds timestamp.")
102 })
103 .naive_utc();
104 let y = naive.year();
105 let m = naive.month();
106 NaiveDate::from_ymd_opt(y, m, 1)
107 .unwrap()
108 .and_hms_milli_opt(0, 0, 0, 0)
109 .unwrap()
110 .and_utc()
111 .timestamp_millis()
112 }
113 AlignmentUnit::Year => {
114 let naive = DateTime::from_timestamp_millis(timestamp)
115 .unwrap_or_else(|| {
116 panic!("{timestamp} cannot be interpreted as a milliseconds timestamp.")
117 })
118 .naive_utc();
119 let y = naive.year();
120 NaiveDate::from_ymd_opt(y, 1, 1)
121 .unwrap()
122 .and_hms_milli_opt(0, 0, 0, 0)
123 .unwrap()
124 .and_utc()
125 .timestamp_millis()
126 }
127 }
128 }
129
130 #[inline]
133 fn floor_ms(ts: i64, unit_ms: i64) -> i64 {
134 ts - ts.rem_euclid(unit_ms)
135 }
136}
137
138impl TryFrom<String> for AlignmentUnit {
139 type Error = ParseTimeError;
140
141 fn try_from(value: String) -> Result<Self, Self::Error> {
142 Self::try_from(value.as_str())
143 }
144}
145
146impl TryFrom<&str> for AlignmentUnit {
147 type Error = ParseTimeError;
148
149 fn try_from(value: &str) -> Result<Self, Self::Error> {
150 let unit = match value.to_lowercase().as_str() {
151 "year" | "years" => AlignmentUnit::Year,
152 "month" | "months" => AlignmentUnit::Month,
153 "week" | "weeks" => AlignmentUnit::Week,
154 "day" | "days" => AlignmentUnit::Day,
155 "hour" | "hours" => AlignmentUnit::Hour,
156 "minute" | "minutes" => AlignmentUnit::Minute,
157 "second" | "seconds" => AlignmentUnit::Second,
158 "millisecond" | "milliseconds" => AlignmentUnit::Millisecond,
159 "unaligned" => AlignmentUnit::Unaligned,
160 unit => return Err(ParseTimeError::InvalidAlignmentUnit(unit.to_string())),
161 };
162 Ok(unit)
163 }
164}
165
166#[derive(Clone, Copy, Debug, PartialEq)]
167pub struct Interval {
168 pub alignment_unit: Option<AlignmentUnit>,
171 pub size: IntervalSize,
173}
174
175impl Default for Interval {
176 fn default() -> Self {
177 Self {
178 alignment_unit: None,
179 size: IntervalSize::Discrete(1),
180 }
181 }
182}
183
184impl TryFrom<String> for Interval {
185 type Error = ParseTimeError;
186
187 fn try_from(value: String) -> Result<Self, Self::Error> {
188 Self::try_from(value.as_str())
189 }
190}
191
192impl TryFrom<&str> for Interval {
193 type Error = ParseTimeError;
194 fn try_from(value: &str) -> Result<Self, Self::Error> {
195 let trimmed = value.trim();
196 let no_and = trimmed.replace("and", "");
197 let cleaned = {
198 let re = Regex::new(r"[\s&,]+").unwrap();
199 re.replace_all(&no_and, " ")
200 };
201
202 let tokens = cleaned.split(' ').collect_vec();
203
204 if tokens.len() < 2 || tokens.len() % 2 != 0 {
205 return Err(ParseTimeError::InvalidPairs);
206 }
207
208 let (temporal_sum, smallest_unit): (IntervalSize, AlignmentUnit) =
209 tokens.chunks(2).try_fold(
210 (IntervalSize::empty_temporal(), AlignmentUnit::Year), |(sum, smallest), chunk| {
212 let (interval, unit) = Self::parse_duration(chunk[0], chunk[1])?;
213 Ok::<_, ParseTimeError>((sum.add_temporal(interval), smallest.min(unit)))
214 },
215 )?;
216
217 Ok(Self {
218 alignment_unit: Some(smallest_unit),
219 size: temporal_sum,
220 })
221 }
222}
223
224impl TryFrom<u64> for Interval {
225 type Error = ParseTimeError;
226 fn try_from(value: u64) -> Result<Self, Self::Error> {
227 Ok(Self {
228 alignment_unit: None,
229 size: IntervalSize::Discrete(value),
230 })
231 }
232}
233
234impl TryFrom<u32> for Interval {
235 type Error = ParseTimeError;
236 fn try_from(value: u32) -> Result<Self, Self::Error> {
237 Ok(Self {
238 alignment_unit: None,
239 size: IntervalSize::Discrete(value as u64),
240 })
241 }
242}
243
244impl TryFrom<i32> for Interval {
245 type Error = ParseTimeError;
246 fn try_from(value: i32) -> Result<Self, Self::Error> {
247 if value >= 0 {
248 Ok(Self {
249 alignment_unit: None,
250 size: IntervalSize::Discrete(value as u64),
251 })
252 } else {
253 Err(ParseTimeError::NegativeInt)
254 }
255 }
256}
257
258impl TryFrom<i64> for Interval {
259 type Error = ParseTimeError;
260
261 fn try_from(value: i64) -> Result<Self, Self::Error> {
262 if value >= 0 {
263 Ok(Self {
264 alignment_unit: None,
265 size: IntervalSize::Discrete(value as u64),
266 })
267 } else {
268 Err(ParseTimeError::NegativeInt)
269 }
270 }
271}
272
273pub trait TryIntoInterval {
274 fn try_into_interval(self) -> Result<Interval, ParseTimeError>;
275}
276
277impl<T> TryIntoInterval for T
278where
279 Interval: TryFrom<T>,
280 ParseTimeError: From<<Interval as TryFrom<T>>::Error>,
281{
282 fn try_into_interval(self) -> Result<Interval, ParseTimeError> {
283 Ok(self.try_into()?)
284 }
285}
286
287impl Interval {
288 pub fn to_millis(&self) -> Option<u64> {
290 match self.size {
291 IntervalSize::Discrete(millis) => Some(millis),
292 IntervalSize::Temporal { millis, months } => (months == 0).then_some(millis),
293 }
294 }
295
296 fn parse_duration(
297 number: &str,
298 unit: &str,
299 ) -> Result<(IntervalSize, AlignmentUnit), ParseTimeError> {
300 let number: i64 = number.parse::<u64>()? as i64;
301 let duration = match unit {
302 "year" | "years" => (IntervalSize::months(number * 12), AlignmentUnit::Year),
303 "month" | "months" => (IntervalSize::months(number), AlignmentUnit::Month),
304 "week" | "weeks" => (Duration::weeks(number).into(), AlignmentUnit::Week),
305 "day" | "days" => (Duration::days(number).into(), AlignmentUnit::Day),
306 "hour" | "hours" => (Duration::hours(number).into(), AlignmentUnit::Hour),
307 "minute" | "minutes" => (Duration::minutes(number).into(), AlignmentUnit::Minute),
308 "second" | "seconds" => (Duration::seconds(number).into(), AlignmentUnit::Second),
309 "millisecond" | "milliseconds" => (
310 Duration::milliseconds(number).into(),
311 AlignmentUnit::Millisecond,
312 ),
313 unit => return Err(ParseTimeError::InvalidUnit(unit.to_string())),
314 };
315 Ok(duration)
316 }
317
318 pub fn discrete(num: u64) -> Self {
319 Interval {
320 alignment_unit: None,
321 size: IntervalSize::Discrete(num),
322 }
323 }
324
325 pub fn milliseconds(ms: i64) -> Self {
326 Interval {
327 alignment_unit: Some(AlignmentUnit::Millisecond),
328 size: IntervalSize::from(Duration::milliseconds(ms)),
329 }
330 }
331
332 pub fn seconds(seconds: i64) -> Self {
333 Interval {
334 alignment_unit: Some(AlignmentUnit::Second),
335 size: IntervalSize::from(Duration::seconds(seconds)),
336 }
337 }
338
339 pub fn minutes(minutes: i64) -> Self {
340 Interval {
341 alignment_unit: Some(AlignmentUnit::Minute),
342 size: IntervalSize::from(Duration::minutes(minutes)),
343 }
344 }
345
346 pub fn hours(hours: i64) -> Self {
347 Interval {
348 alignment_unit: Some(AlignmentUnit::Hour),
349 size: IntervalSize::from(Duration::hours(hours)),
350 }
351 }
352
353 pub fn days(days: i64) -> Self {
354 Interval {
355 alignment_unit: Some(AlignmentUnit::Day),
356 size: IntervalSize::from(Duration::days(days)),
357 }
358 }
359
360 pub fn weeks(weeks: i64) -> Self {
361 Interval {
362 alignment_unit: Some(AlignmentUnit::Week),
363 size: IntervalSize::from(Duration::weeks(weeks)),
364 }
365 }
366
367 pub fn months(months: i64) -> Self {
368 Interval {
369 alignment_unit: Some(AlignmentUnit::Month),
370 size: IntervalSize::months(months),
371 }
372 }
373
374 pub fn years(years: i64) -> Self {
375 Interval {
376 alignment_unit: Some(AlignmentUnit::Year),
377 size: IntervalSize::months(12 * years),
378 }
379 }
380
381 pub fn and(&self, other: &Self) -> Result<Self, IntervalTypeError> {
382 match (self.size, other.size) {
383 (IntervalSize::Discrete(l), IntervalSize::Discrete(r)) => Ok(Interval {
384 alignment_unit: None,
385 size: IntervalSize::Discrete(l + r),
386 }),
387 (IntervalSize::Temporal { .. }, IntervalSize::Temporal { .. }) => Ok(Interval {
388 alignment_unit: self.alignment_unit.min(other.alignment_unit),
389 size: self.size.add_temporal(other.size),
390 }),
391 (_, _) => Err(IntervalTypeError()),
392 }
393 }
394}
395
396#[derive(thiserror::Error, Debug)]
397#[error("Discrete and temporal intervals cannot be combined")]
398pub struct IntervalTypeError();
399
400impl Sub<Interval> for i64 {
401 type Output = i64;
402 fn sub(self, rhs: Interval) -> Self::Output {
403 match rhs.size {
404 IntervalSize::Discrete(number)
405 | IntervalSize::Temporal {
406 millis: number,
407 months: 0,
408 } => self - (number as i64),
409 IntervalSize::Temporal { millis, months } => {
410 let datetime = DateTime::from_timestamp_millis(self - millis as i64)
414 .unwrap_or_else(|| {
415 panic!("{self} cannot be interpreted as a milliseconds timestamp")
416 })
417 .naive_utc();
418 (datetime - Months::new(months))
419 .and_utc()
420 .timestamp_millis()
421 }
422 }
423 }
424}
425
426impl Add<Interval> for i64 {
427 type Output = i64;
428 fn add(self, rhs: Interval) -> Self::Output {
429 match rhs.size {
430 IntervalSize::Discrete(number)
431 | IntervalSize::Temporal {
432 millis: number,
433 months: 0,
434 } => self + (number as i64),
435 IntervalSize::Temporal { millis, months } => {
436 let datetime = DateTime::from_timestamp_millis(self)
440 .unwrap_or_else(|| {
441 panic!("{self} cannot be interpreted as a milliseconds timestamp")
442 })
443 .naive_utc();
444 (datetime + Months::new(months))
445 .and_utc()
446 .timestamp_millis()
447 + millis as i64
448 }
449 }
450 }
451}
452
453impl Mul<Interval> for u32 {
456 type Output = Interval;
457
458 fn mul(self, rhs: Interval) -> Self::Output {
459 match rhs.size {
460 IntervalSize::Discrete(number) => Interval {
461 alignment_unit: rhs.alignment_unit, size: IntervalSize::Discrete((self as u64) * number),
463 },
464 IntervalSize::Temporal { millis, months } => Interval {
465 alignment_unit: rhs.alignment_unit,
466 size: IntervalSize::Temporal {
467 millis: (self as u64) * millis,
468 months: self * months,
469 },
470 },
471 }
472 }
473}
474
475impl Add<Interval> for EventTime {
476 type Output = EventTime;
477 fn add(self, rhs: Interval) -> Self::Output {
478 match rhs.size {
479 IntervalSize::Discrete(number) => EventTime(self.0 + (number as i64), self.1),
480 IntervalSize::Temporal { millis, months } => {
481 let datetime = DateTime::from_timestamp_millis(self.0)
485 .unwrap_or_else(|| {
486 panic!("{self} cannot be interpreted as a milliseconds timestamp")
487 })
488 .naive_utc();
489 let timestamp = (datetime + Months::new(months))
490 .and_utc()
491 .timestamp_millis()
492 + millis as i64;
493 EventTime(timestamp, self.1)
494 }
495 }
496 }
497}
498
499#[cfg(test)]
500mod time_tests {
501 use crate::utils::time::{AlignmentUnit, Interval, WEEK_MS};
502 use chrono::{DateTime, Datelike, NaiveTime, Utc, Weekday};
503 use proptest::{arbitrary::any, prelude::Strategy, proptest};
504 use raphtory_api::core::{
505 storage::timeindex::AsTime,
506 utils::time::{ParseTimeError, TryIntoTime},
507 };
508
509 #[test]
510 fn alignment_week_proptest() {
511 proptest!(|(dt in (-8334601228800000i64..8210266876800000).prop_filter_map("not a valid date", DateTime::from_timestamp_millis))| {
512 let ts = dt.timestamp_millis();
513 let aligned = AlignmentUnit::Week.align_timestamp(ts);
514 let aligned_dt = aligned.dt().unwrap();
515 assert_eq!(aligned_dt, aligned_dt.with_time(NaiveTime::from_num_seconds_from_midnight_opt(0, 0).unwrap()).unwrap());
516 assert!(ts - aligned < WEEK_MS);
517 assert_eq!(aligned_dt.weekday(), Weekday::Mon);
518
519
520 })
521 }
522
523 #[test]
524 fn interval_parsing() {
525 let second: u64 = 1000;
526 let minute = 60 * second;
527 let hour = 60 * minute;
528 let day = 24 * hour;
529 let week = 7 * day;
530
531 let interval: Interval = "1 day".try_into().unwrap();
532 assert_eq!(interval.to_millis().unwrap(), day);
533
534 let interval: Interval = "1 week".try_into().unwrap();
535 assert_eq!(interval.to_millis().unwrap(), week);
536
537 let interval: Interval = "4 weeks and 1 day".try_into().unwrap();
538 assert_eq!(interval.to_millis().unwrap(), 4 * week + day);
539
540 let interval: Interval = "2 days & 1 millisecond".try_into().unwrap();
541 assert_eq!(interval.to_millis().unwrap(), 2 * day + 1);
542
543 let interval: Interval = "2 days, 1 hour, and 2 minutes".try_into().unwrap();
544 assert_eq!(interval.to_millis().unwrap(), 2 * day + hour + 2 * minute);
545
546 let interval: Interval = "1 weeks , 1 minute".try_into().unwrap();
547 assert_eq!(interval.to_millis().unwrap(), week + minute);
548
549 let interval: Interval = "23 seconds and 34 millisecond and 1 minute"
550 .try_into()
551 .unwrap();
552 assert_eq!(interval.to_millis().unwrap(), 23 * second + 34 + minute);
553 }
554
555 #[test]
556 fn interval_parsing_with_months_and_years() {
557 let dt = "2020-01-01 00:00:00".try_into_time().unwrap();
558
559 let two_months: Interval = "2 months".try_into().unwrap();
560 let dt_plus_2_months = "2020-03-01 00:00:00".try_into_time().unwrap();
561 assert_eq!(dt + two_months, dt_plus_2_months);
562
563 let two_years: Interval = "2 years".try_into().unwrap();
564 let dt_plus_2_years = "2022-01-01 00:00:00".try_into_time().unwrap();
565 assert_eq!(dt + two_years, dt_plus_2_years);
566
567 let mix_interval: Interval = "1 year 1 month and 1 second".try_into().unwrap();
568 let dt_mix = "2021-02-01 00:00:01".try_into_time().unwrap();
569 assert_eq!(dt + mix_interval, dt_mix);
570 }
571
572 #[test]
573 fn invalid_intervals() {
574 let result: Result<Interval, ParseTimeError> = "".try_into();
575 assert_eq!(result, Err(ParseTimeError::InvalidPairs));
576
577 let result: Result<Interval, ParseTimeError> = "1".try_into();
578 assert_eq!(result, Err(ParseTimeError::InvalidPairs));
579
580 let result: Result<Interval, ParseTimeError> = "1 day and 5".try_into();
581 assert_eq!(result, Err(ParseTimeError::InvalidPairs));
582
583 let result: Result<Interval, ParseTimeError> = "1 daay".try_into();
584 assert_eq!(result, Err(ParseTimeError::InvalidUnit("daay".to_string())));
585
586 let result: Result<Interval, ParseTimeError> = "day 1".try_into();
587
588 match result {
589 Err(ParseTimeError::ParseInt { .. }) => (),
590 _ => panic!(),
591 }
592 }
593}