Skip to main content

linkrs_core/value/
interval.rs

1//! Interval Type Module - PostgreSQL Compatible Time Interval
2//!
3//! This module provides Interval type support compatible with PostgreSQL.
4//!
5//! ## Features
6//! - Calendar interval (years, months)
7//! - Clock interval (days, hours, minutes, seconds, microseconds)
8//! - Mixed interval support (e.g., "1 year 2 days 3 hours")
9//! - ISO 8601 and PostgreSQL format parsing
10//! - Arithmetic operations with Date/DateTime types
11//!
12//! ## PostgreSQL Compatibility
13//! PostgreSQL stores intervals as:
14//! - months: i32 (calendar months)
15//! - days: i32 (clock days, separate from months for DST handling)
16//! - microseconds: i64 (clock time, can exceed 24 hours)
17
18use serde::{Deserialize, Serialize};
19use std::fmt;
20use std::hash::Hash;
21use std::ops::{Add, Neg, Sub};
22
23/// Interval Value Type - PostgreSQL Compatible
24///
25/// Represents a time span that can be:
26/// - Calendar-based (years, months)
27/// - Clock-based (days, hours, minutes, seconds)
28/// - Mixed (both calendar and clock components)
29///
30/// # Examples
31/// - "1 year 2 months" -> months: 14
32/// - "3 days 4 hours" -> days: 3, microseconds: 4 * 3600 * 1_000_000
33/// - "1 year 2 days" -> months: 12, days: 2
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
35pub struct IntervalValue {
36    /// Calendar months (can be negative)
37    pub months: i32,
38    /// Clock days (can be negative, separate from months for DST handling)
39    pub days: i32,
40    /// Clock microseconds (can be negative and exceed 24 hours)
41    pub microseconds: i64,
42}
43
44impl IntervalValue {
45    /// Create a new interval
46    pub const fn new(months: i32, days: i32, microseconds: i64) -> Self {
47        Self {
48            months,
49            days,
50            microseconds,
51        }
52    }
53
54    /// Create interval from years
55    pub const fn from_years(years: i32) -> Self {
56        Self {
57            months: years * 12,
58            days: 0,
59            microseconds: 0,
60        }
61    }
62
63    /// Create interval from months
64    pub const fn from_months(months: i32) -> Self {
65        Self {
66            months,
67            days: 0,
68            microseconds: 0,
69        }
70    }
71
72    /// Create interval from days
73    pub const fn from_days(days: i32) -> Self {
74        Self {
75            months: 0,
76            days,
77            microseconds: 0,
78        }
79    }
80
81    /// Create interval from hours
82    pub const fn from_hours(hours: i64) -> Self {
83        Self {
84            months: 0,
85            days: 0,
86            microseconds: hours * 3_600_000_000,
87        }
88    }
89
90    /// Create interval from minutes
91    pub const fn from_minutes(minutes: i64) -> Self {
92        Self {
93            months: 0,
94            days: 0,
95            microseconds: minutes * 60_000_000,
96        }
97    }
98
99    /// Create interval from seconds
100    pub const fn from_seconds(seconds: i64) -> Self {
101        Self {
102            months: 0,
103            days: 0,
104            microseconds: seconds * 1_000_000,
105        }
106    }
107
108    /// Create interval from milliseconds
109    pub const fn from_milliseconds(millis: i64) -> Self {
110        Self {
111            months: 0,
112            days: 0,
113            microseconds: millis * 1_000,
114        }
115    }
116
117    /// Create interval from microseconds
118    pub const fn from_microseconds(micros: i64) -> Self {
119        Self {
120            months: 0,
121            days: 0,
122            microseconds: micros,
123        }
124    }
125
126    /// Parse interval from PostgreSQL format string
127    ///
128    /// Supported formats:
129    /// - "1 year 2 months 3 days 4 hours 5 minutes 6 seconds"
130    /// - "1-2" (year-month)
131    /// - "3 4:05:06" (day time)
132    /// - "P1Y2M3DT4H5M6S" (ISO 8601)
133    pub fn parse(s: &str) -> Result<Self, IntervalError> {
134        let s = s.trim();
135
136        // Try ISO 8601 format first
137        if s.starts_with('P') {
138            return Self::parse_iso8601(s);
139        }
140
141        // Try PostgreSQL format
142        Self::parse_postgresql(s)
143    }
144
145    /// Parse ISO 8601 duration format: P[n]Y[n]M[n]DT[n]H[n]M[n]S
146    fn parse_iso8601(s: &str) -> Result<Self, IntervalError> {
147        if !s.starts_with('P') {
148            return Err(IntervalError::InvalidFormat(s.to_string()));
149        }
150
151        let mut months = 0i32;
152        let mut days = 0i32;
153        let mut microseconds = 0i64;
154
155        let chars = s[1..].chars().peekable();
156        let mut num_str = String::new();
157        let mut in_time_part = false;
158
159        for c in chars {
160            match c {
161                'T' => {
162                    in_time_part = true;
163                    num_str.clear();
164                }
165                'Y' => {
166                    let num: i32 = num_str
167                        .parse()
168                        .map_err(|_| IntervalError::InvalidFormat(s.to_string()))?;
169                    months += num * 12;
170                    num_str.clear();
171                }
172                'M' if !in_time_part => {
173                    let num: i32 = num_str
174                        .parse()
175                        .map_err(|_| IntervalError::InvalidFormat(s.to_string()))?;
176                    months += num;
177                    num_str.clear();
178                }
179                'D' => {
180                    let num: i32 = num_str
181                        .parse()
182                        .map_err(|_| IntervalError::InvalidFormat(s.to_string()))?;
183                    days += num;
184                    num_str.clear();
185                }
186                'H' => {
187                    let num: i64 = num_str
188                        .parse()
189                        .map_err(|_| IntervalError::InvalidFormat(s.to_string()))?;
190                    microseconds += num * 3_600_000_000;
191                    num_str.clear();
192                }
193                'M' if in_time_part => {
194                    let num: i64 = num_str
195                        .parse()
196                        .map_err(|_| IntervalError::InvalidFormat(s.to_string()))?;
197                    microseconds += num * 60_000_000;
198                    num_str.clear();
199                }
200                'S' => {
201                    let num: f64 = num_str
202                        .parse()
203                        .map_err(|_| IntervalError::InvalidFormat(s.to_string()))?;
204                    microseconds += (num * 1_000_000.0) as i64;
205                    num_str.clear();
206                }
207                c if c.is_ascii_digit() || c == '.' || c == '-' => {
208                    num_str.push(c);
209                }
210                _ => return Err(IntervalError::InvalidFormat(s.to_string())),
211            }
212        }
213
214        Ok(Self::new(months, days, microseconds))
215    }
216
217    /// Parse PostgreSQL interval format
218    fn parse_postgresql(s: &str) -> Result<Self, IntervalError> {
219        let mut months = 0i32;
220        let mut days = 0i32;
221        let mut microseconds = 0i64;
222
223        // Try year-month format: "1-2" (1 year 2 months)
224        if let Some(dash_pos) = s.find('-') {
225            let before = s[..dash_pos].trim();
226            let after = s[dash_pos + 1..].trim();
227            if before.parse::<i32>().is_ok() && after.parse::<i32>().is_ok() {
228                let years: i32 = before.parse().unwrap();
229                let mons: i32 = after.parse().unwrap();
230                return Ok(Self::new(years * 12 + mons, 0, 0));
231            }
232        }
233
234        // Try day time format: "3 4:05:06" (3 days 4 hours 5 minutes 6 seconds)
235        if let Some(space_pos) = s.find(' ') {
236            let before = s[..space_pos].trim();
237            let after = s[space_pos + 1..].trim();
238            if let Ok(d) = before.parse::<i32>() {
239                if after.contains(':') {
240                    days = d;
241                    microseconds = Self::parse_time(after)?;
242                    return Ok(Self::new(0, days, microseconds));
243                }
244            }
245        }
246
247        // Parse word format: "1 year 2 months 3 days ..."
248        let parts: Vec<&str> = s.split_whitespace().collect();
249        let mut i = 0;
250
251        while i < parts.len() {
252            let num: f64 = parts[i]
253                .parse()
254                .map_err(|_| IntervalError::InvalidFormat(s.to_string()))?;
255            i += 1;
256
257            if i >= parts.len() {
258                return Err(IntervalError::InvalidFormat(s.to_string()));
259            }
260
261            let unit = parts[i].to_lowercase();
262            i += 1;
263
264            // Handle plural forms
265            let unit = if unit.ends_with('s') && unit.len() > 1 {
266                &unit[..unit.len() - 1]
267            } else {
268                &unit
269            };
270
271            match unit {
272                "year" => months += (num * 12.0) as i32,
273                "month" => months += num as i32,
274                "day" => days += num as i32,
275                "hour" => microseconds += (num * 3_600_000_000.0) as i64,
276                "minute" => microseconds += (num * 60_000_000.0) as i64,
277                "second" => microseconds += (num * 1_000_000.0) as i64,
278                "millisecond" => microseconds += (num * 1_000.0) as i64,
279                "microsecond" => microseconds += num as i64,
280                "week" => days += (num * 7.0) as i32,
281                "century" | "centurie" => months += (num * 1200.0) as i32,
282                "decade" => months += (num * 120.0) as i32,
283                "millennium" => months += (num * 12000.0) as i32,
284                "quarter" => months += (num * 3.0) as i32,
285                _ => return Err(IntervalError::InvalidUnit(unit.to_string())),
286            }
287        }
288
289        Ok(Self::new(months, days, microseconds))
290    }
291
292    /// Parse time part (HH:MM:SS[.fraction])
293    fn parse_time(s: &str) -> Result<i64, IntervalError> {
294        let parts: Vec<&str> = s.split(':').collect();
295        if parts.len() != 3 {
296            return Err(IntervalError::InvalidFormat(s.to_string()));
297        }
298
299        let hours: i64 = parts[0]
300            .parse()
301            .map_err(|_| IntervalError::InvalidFormat(s.to_string()))?;
302        let minutes: i64 = parts[1]
303            .parse()
304            .map_err(|_| IntervalError::InvalidFormat(s.to_string()))?;
305
306        // Handle optional fractional seconds
307        let sec_parts: Vec<&str> = parts[2].split('.').collect();
308        let seconds: i64 = sec_parts[0]
309            .parse()
310            .map_err(|_| IntervalError::InvalidFormat(s.to_string()))?;
311
312        let mut micros = 0i64;
313        if sec_parts.len() > 1 {
314            let frac = format!("{:0<6}", sec_parts[1]);
315            let frac_6: i64 = frac[..6.min(frac.len())]
316                .parse()
317                .map_err(|_| IntervalError::InvalidFormat(s.to_string()))?;
318            micros = frac_6;
319        }
320
321        Ok(hours * 3_600_000_000 + minutes * 60_000_000 + seconds * 1_000_000 + micros)
322    }
323
324    /// Get total months (calendar component)
325    pub const fn total_months(&self) -> i32 {
326        self.months
327    }
328
329    /// Get total days (clock component, excluding months)
330    pub const fn total_days(&self) -> i32 {
331        self.days
332    }
333
334    /// Get total microseconds (clock component)
335    pub const fn total_microseconds(&self) -> i64 {
336        self.microseconds
337    }
338
339    /// Get years component
340    pub const fn years(&self) -> i32 {
341        self.months / 12
342    }
343
344    /// Get remaining months component (0-11)
345    pub const fn months_component(&self) -> i32 {
346        self.months % 12
347    }
348
349    /// Get hours component from microseconds
350    pub const fn hours(&self) -> i64 {
351        self.microseconds / 3_600_000_000
352    }
353
354    /// Get minutes component from microseconds
355    pub const fn minutes(&self) -> i64 {
356        (self.microseconds.abs() % 3_600_000_000) / 60_000_000
357    }
358
359    /// Get seconds component from microseconds
360    pub const fn seconds(&self) -> i64 {
361        (self.microseconds.abs() % 60_000_000) / 1_000_000
362    }
363
364    /// Get fractional seconds (microseconds component)
365    pub const fn fractional_seconds(&self) -> i64 {
366        self.microseconds.abs() % 1_000_000
367    }
368
369    /// Check if interval is positive
370    pub fn is_positive(&self) -> bool {
371        self.months > 0 || self.days > 0 || self.microseconds > 0
372    }
373
374    /// Check if interval is negative
375    pub fn is_negative(&self) -> bool {
376        self.months < 0 || self.days < 0 || self.microseconds < 0
377    }
378
379    /// Check if interval is zero
380    pub fn is_zero(&self) -> bool {
381        self.months == 0 && self.days == 0 && self.microseconds == 0
382    }
383
384    /// Normalize the interval (ensure consistent sign)
385    pub fn normalize(&self) -> Self {
386        // PostgreSQL doesn't fully normalize intervals
387        // Each component keeps its own sign
388        *self
389    }
390
391    /// Negate the interval
392    pub const fn neg(&self) -> Self {
393        Self::new(-self.months, -self.days, -self.microseconds)
394    }
395
396    /// Absolute value
397    pub fn abs(&self) -> Self {
398        Self::new(self.months.abs(), self.days.abs(), self.microseconds.abs())
399    }
400
401    /// Format as ISO 8601 duration
402    pub fn to_iso8601(&self) -> String {
403        let mut result = String::from("P");
404
405        let years = self.years();
406        let months = self.months_component();
407        let days = self.days;
408
409        if years != 0 {
410            result.push_str(&format!("{}Y", years));
411        }
412        if months != 0 {
413            result.push_str(&format!("{}M", months));
414        }
415        if days != 0 {
416            result.push_str(&format!("{}D", days));
417        }
418
419        let hours = self.hours();
420        let minutes = self.minutes();
421        let seconds = self.seconds();
422        let micros = self.fractional_seconds();
423
424        if hours != 0 || minutes != 0 || seconds != 0 || micros != 0 {
425            result.push('T');
426            if hours != 0 {
427                result.push_str(&format!("{}H", hours));
428            }
429            if minutes != 0 {
430                result.push_str(&format!("{}M", minutes));
431            }
432            if seconds != 0 || micros != 0 {
433                if micros != 0 {
434                    result.push_str(&format!("{}.{:06}S", seconds, micros));
435                } else {
436                    result.push_str(&format!("{}S", seconds));
437                }
438            }
439        }
440
441        // If no components, return zero seconds
442        if result == "P" {
443            result.push_str("T0S");
444        }
445
446        result
447    }
448
449    /// Format as PostgreSQL style string
450    pub fn to_postgresql(&self) -> String {
451        let mut parts = Vec::new();
452
453        let years = self.years();
454        let months = self.months_component();
455        let days = self.days;
456        let hours = self.hours();
457        let minutes = self.minutes();
458        let seconds = self.seconds();
459        let micros = self.fractional_seconds();
460
461        if years != 0 {
462            parts.push(format!(
463                "{} year{}",
464                years,
465                if years == 1 { "" } else { "s" }
466            ));
467        }
468        if months != 0 {
469            parts.push(format!(
470                "{} mon{}",
471                months,
472                if months == 1 { "" } else { "s" }
473            ));
474        }
475        if days != 0 {
476            parts.push(format!("{} day{}", days, if days == 1 { "" } else { "s" }));
477        }
478
479        if hours != 0 || minutes != 0 || seconds != 0 || micros != 0 {
480            let time_str = if micros != 0 {
481                format!("{:02}:{:02}:{:02}.{:06}", hours, minutes, seconds, micros)
482            } else {
483                format!("{:02}:{:02}:{:02}", hours, minutes, seconds)
484            };
485            parts.push(time_str);
486        }
487
488        if parts.is_empty() {
489            "00:00:00".to_string()
490        } else {
491            parts.join(" ")
492        }
493    }
494
495    /// Estimate memory usage
496    pub const fn estimated_size(&self) -> usize {
497        std::mem::size_of::<Self>()
498    }
499
500    /// Zero interval
501    pub const fn zero() -> Self {
502        Self::new(0, 0, 0)
503    }
504}
505
506impl Default for IntervalValue {
507    fn default() -> Self {
508        Self::zero()
509    }
510}
511
512impl fmt::Display for IntervalValue {
513    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
514        write!(f, "{}", self.to_postgresql())
515    }
516}
517
518impl Add for IntervalValue {
519    type Output = Self;
520
521    fn add(self, other: Self) -> Self {
522        Self::new(
523            self.months + other.months,
524            self.days + other.days,
525            self.microseconds + other.microseconds,
526        )
527    }
528}
529
530impl Sub for IntervalValue {
531    type Output = Self;
532
533    fn sub(self, other: Self) -> Self {
534        Self::new(
535            self.months - other.months,
536            self.days - other.days,
537            self.microseconds - other.microseconds,
538        )
539    }
540}
541
542impl Neg for IntervalValue {
543    type Output = Self;
544
545    fn neg(self) -> Self {
546        Self::new(-self.months, -self.days, -self.microseconds)
547    }
548}
549
550/// Interval Error Type
551#[derive(Debug, Clone, PartialEq, Eq)]
552pub enum IntervalError {
553    InvalidFormat(String),
554    InvalidUnit(String),
555}
556
557impl fmt::Display for IntervalError {
558    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
559        match self {
560            IntervalError::InvalidFormat(s) => write!(f, "Invalid interval format: {}", s),
561            IntervalError::InvalidUnit(u) => write!(f, "Invalid interval unit: {}", u),
562        }
563    }
564}
565
566impl std::error::Error for IntervalError {}
567
568#[cfg(test)]
569mod tests {
570    use super::*;
571
572    #[test]
573    fn test_interval_from_components() {
574        let iv = IntervalValue::new(14, 3, 4_500_000_000);
575        assert_eq!(iv.months, 14);
576        assert_eq!(iv.days, 3);
577        assert_eq!(iv.microseconds, 4_500_000_000);
578    }
579
580    #[test]
581    fn test_interval_from_years() {
582        let iv = IntervalValue::from_years(2);
583        assert_eq!(iv.months, 24);
584        assert_eq!(iv.days, 0);
585        assert_eq!(iv.microseconds, 0);
586    }
587
588    #[test]
589    fn test_interval_from_days() {
590        let iv = IntervalValue::from_days(5);
591        assert_eq!(iv.months, 0);
592        assert_eq!(iv.days, 5);
593        assert_eq!(iv.microseconds, 0);
594    }
595
596    #[test]
597    fn test_interval_from_hours() {
598        let iv = IntervalValue::from_hours(3);
599        assert_eq!(iv.microseconds, 3 * 3_600_000_000);
600    }
601
602    #[test]
603    fn test_interval_parse_iso8601() {
604        let iv = IntervalValue::parse("P1Y2M3DT4H5M6S").unwrap();
605        assert_eq!(iv.months, 14);
606        assert_eq!(iv.days, 3);
607        assert_eq!(
608            iv.microseconds,
609            4 * 3_600_000_000 + 5 * 60_000_000 + 6 * 1_000_000
610        );
611    }
612
613    #[test]
614    fn test_interval_parse_iso8601_fractional() {
615        let iv = IntervalValue::parse("PT1.5S").unwrap();
616        assert_eq!(iv.microseconds, 1_500_000);
617    }
618
619    #[test]
620    fn test_interval_parse_postgresql() {
621        let iv =
622            IntervalValue::parse("1 year 2 months 3 days 4 hours 5 minutes 6 seconds").unwrap();
623        assert_eq!(iv.months, 14);
624        assert_eq!(iv.days, 3);
625        assert_eq!(
626            iv.microseconds,
627            4 * 3_600_000_000 + 5 * 60_000_000 + 6 * 1_000_000
628        );
629    }
630
631    #[test]
632    fn test_interval_parse_year_month() {
633        let iv = IntervalValue::parse("1-2").unwrap();
634        assert_eq!(iv.months, 14);
635    }
636
637    #[test]
638    fn test_interval_parse_day_time() {
639        let iv = IntervalValue::parse("3 4:05:06").unwrap();
640        assert_eq!(iv.days, 3);
641        assert_eq!(
642            iv.microseconds,
643            4 * 3_600_000_000 + 5 * 60_000_000 + 6 * 1_000_000
644        );
645    }
646
647    #[test]
648    fn test_interval_parse_plural() {
649        let iv = IntervalValue::parse("2 years 5 days").unwrap();
650        assert_eq!(iv.months, 24);
651        assert_eq!(iv.days, 5);
652    }
653
654    #[test]
655    fn test_interval_add() {
656        let iv1 = IntervalValue::from_days(3);
657        let iv2 = IntervalValue::from_hours(12);
658        let result = iv1 + iv2;
659        assert_eq!(result.days, 3);
660        assert_eq!(result.microseconds, 12 * 3_600_000_000);
661    }
662
663    #[test]
664    fn test_interval_sub() {
665        let iv1 = IntervalValue::from_days(5);
666        let iv2 = IntervalValue::from_days(2);
667        let result = iv1 - iv2;
668        assert_eq!(result.days, 3);
669    }
670
671    #[test]
672    fn test_interval_neg() {
673        let iv = IntervalValue::new(1, 2, 3);
674        let neg = iv.neg();
675        assert_eq!(neg.months, -1);
676        assert_eq!(neg.days, -2);
677        assert_eq!(neg.microseconds, -3);
678    }
679
680    #[test]
681    fn test_interval_to_iso8601() {
682        let iv = IntervalValue::new(14, 3, 4 * 3_600_000_000 + 5 * 60_000_000 + 6 * 1_000_000);
683        assert_eq!(iv.to_iso8601(), "P1Y2M3DT4H5M6S");
684    }
685
686    #[test]
687    fn test_interval_to_postgresql() {
688        let iv = IntervalValue::new(14, 3, 4 * 3_600_000_000 + 5 * 60_000_000 + 6 * 1_000_000);
689        assert_eq!(iv.to_postgresql(), "1 year 2 mons 3 days 04:05:06");
690    }
691
692    #[test]
693    fn test_interval_zero() {
694        let iv = IntervalValue::zero();
695        assert!(iv.is_zero());
696        assert_eq!(iv.to_iso8601(), "PT0S");
697    }
698
699    #[test]
700    fn test_interval_display() {
701        let iv = IntervalValue::from_days(5);
702        assert_eq!(format!("{}", iv), "5 days");
703    }
704
705    #[test]
706    fn test_interval_years_months_components() {
707        let iv = IntervalValue::new(26, 0, 0);
708        assert_eq!(iv.years(), 2);
709        assert_eq!(iv.months_component(), 2);
710    }
711
712    #[test]
713    fn test_interval_time_components() {
714        let iv = IntervalValue::from_hours(25)
715            + IntervalValue::from_minutes(30)
716            + IntervalValue::from_seconds(45);
717        assert_eq!(iv.hours(), 25);
718        assert_eq!(iv.minutes(), 30);
719        assert_eq!(iv.seconds(), 45);
720    }
721
722    #[test]
723    fn test_interval_abs() {
724        let iv = IntervalValue::new(-1, -2, -3);
725        let abs = iv.abs();
726        assert_eq!(abs.months, 1);
727        assert_eq!(abs.days, 2);
728        assert_eq!(abs.microseconds, 3);
729    }
730
731    #[test]
732    fn test_interval_parse_weeks() {
733        let iv = IntervalValue::parse("2 weeks").unwrap();
734        assert_eq!(iv.days, 14);
735    }
736
737    #[test]
738    fn test_interval_parse_quarter() {
739        let iv = IntervalValue::parse("1 quarter").unwrap();
740        assert_eq!(iv.months, 3);
741    }
742
743    #[test]
744    fn test_interval_parse_century() {
745        let iv = IntervalValue::parse("1 century").unwrap();
746        assert_eq!(iv.months, 1200);
747    }
748
749    #[test]
750    fn test_interval_parse_millennium() {
751        let iv = IntervalValue::parse("1 millennium").unwrap();
752        assert_eq!(iv.months, 12000);
753    }
754
755    #[test]
756    fn test_interval_parse_invalid() {
757        let result = IntervalValue::parse("invalid");
758        assert!(result.is_err());
759    }
760
761    #[test]
762    fn test_interval_parse_invalid_unit() {
763        let result = IntervalValue::parse("1 foo");
764        assert!(result.is_err());
765    }
766
767    #[test]
768    fn test_interval_equality() {
769        let iv1 = IntervalValue::from_days(3);
770        let iv2 = IntervalValue::from_days(3);
771        let iv3 = IntervalValue::from_days(4);
772        assert_eq!(iv1, iv2);
773        assert_ne!(iv1, iv3);
774    }
775}