Skip to main content

uqa_sql/expr/time/
interval.rs

1//
2// Unified Query Algebra
3//
4// Copyright (c) 2023-2026 Cognica, Inc.
5//
6
7//! Interval arithmetic as `PostgreSQL` 18's `timestamp.c` performs it: negation, field-wise addition and subtraction, and scaling by a double precision factor. Infinite operands retain their signs except for indeterminate operations; finite overflow, including a finite operation landing on a reserved endpoint, reports `interval out of range` (SQLSTATE 22008).
8
9use uqa_core::{TemporalInfinity, TemporalValue};
10
11use super::super::{datetime_out_of_range, division_by_zero};
12use crate::error::Result;
13
14/// The months, days and microseconds of an interval.
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
16pub struct IntervalFields {
17    pub months: i32,
18    pub days: i32,
19    pub micros: i64,
20}
21
22const DAYS_PER_MONTH_INT: i32 = 30;
23const DAYS_PER_MONTH: f64 = 30.0;
24const SECS_PER_DAY: f64 = 86_400.0;
25const USECS_PER_SEC: f64 = 1_000_000.0;
26const USECS_PER_DAY: i64 = 86_400_000_000;
27
28impl IntervalFields {
29    pub const ZERO: Self = Self {
30        months: 0,
31        days: 0,
32        micros: 0,
33    };
34
35    /// The fields of an interval value, or `None` for any other temporal value.
36    pub fn of(value: &TemporalValue) -> Option<Self> {
37        match *value {
38            TemporalValue::Interval {
39                months,
40                days,
41                micros,
42            } => Some(Self {
43                months,
44                days,
45                micros,
46            }),
47            _ => None,
48        }
49    }
50
51    pub fn value(self) -> TemporalValue {
52        TemporalValue::Interval {
53            months: self.months,
54            days: self.days,
55            micros: self.micros,
56        }
57    }
58
59    /// `INTERVAL_NOT_FINITE`: the field values `interval '-infinity'` and `interval 'infinity'` are stored as.
60    fn is_reserved(self) -> bool {
61        !self.value().is_finite()
62    }
63
64    fn infinite(infinity: TemporalInfinity) -> Self {
65        Self::of(&infinity.interval()).expect("interval endpoint")
66    }
67
68    fn finite(self) -> Result<Self> {
69        if self.is_reserved() {
70            Err(datetime_out_of_range("interval"))
71        } else {
72            Ok(self)
73        }
74    }
75
76    /// `interval_um_internal`.
77    pub fn negate(self) -> Result<Self> {
78        if let Some(infinity) = self.value().infinity() {
79            return Ok(Self::infinite(infinity.negate()));
80        }
81        match (
82            self.months.checked_neg(),
83            self.days.checked_neg(),
84            self.micros.checked_neg(),
85        ) {
86            (Some(months), Some(days), Some(micros)) => Self {
87                months,
88                days,
89                micros,
90            }
91            .finite(),
92            _ => Err(datetime_out_of_range("interval")),
93        }
94    }
95
96    /// `finite_interval_pl`.
97    pub fn plus(self, other: Self) -> Result<Self> {
98        if let Some(infinity) = super::infinite::add(
99            self.value().infinity(),
100            other.value().infinity(),
101            "interval",
102        )? {
103            return Ok(Self::infinite(infinity));
104        }
105        match (
106            self.months.checked_add(other.months),
107            self.days.checked_add(other.days),
108            self.micros.checked_add(other.micros),
109        ) {
110            (Some(months), Some(days), Some(micros)) => Self {
111                months,
112                days,
113                micros,
114            }
115            .finite(),
116            _ => Err(datetime_out_of_range("interval")),
117        }
118    }
119
120    /// `finite_interval_mi`.
121    pub fn minus(self, other: Self) -> Result<Self> {
122        if let Some(infinity) =
123            super::infinite::subtract(self.value().infinity(), other.value().infinity())?
124        {
125            return Ok(Self::infinite(infinity));
126        }
127        match (
128            self.months.checked_sub(other.months),
129            self.days.checked_sub(other.days),
130            self.micros.checked_sub(other.micros),
131        ) {
132            (Some(months), Some(days), Some(micros)) => Self {
133                months,
134                days,
135                micros,
136            }
137            .finite(),
138            _ => Err(datetime_out_of_range("interval")),
139        }
140    }
141
142    /// `interval_mul`, including sign propagation and indeterminate products.
143    pub fn multiply(self, factor: f64) -> Result<Self> {
144        if factor.is_nan() {
145            return Err(datetime_out_of_range("interval"));
146        }
147        if let Some(infinity) = self.value().infinity() {
148            if factor == 0.0 {
149                return Err(datetime_out_of_range("interval"));
150            }
151            return Ok(Self::infinite(if factor < 0.0 {
152                infinity.negate()
153            } else {
154                infinity
155            }));
156        }
157        if factor.is_infinite() {
158            let ordering = self.value().cmp(&Self::ZERO.value());
159            if ordering.is_eq() {
160                return Err(datetime_out_of_range("interval"));
161            }
162            return Ok(Self::infinite(
163                if ordering.is_lt() == factor.is_sign_negative() {
164                    TemporalInfinity::Positive
165                } else {
166                    TemporalInfinity::Negative
167                },
168            ));
169        }
170        self.scale(|field| field * factor)
171    }
172
173    /// `interval_div`; zero division precedes non-finite result checks.
174    pub fn divide(self, factor: f64) -> Result<Self> {
175        if factor == 0.0 {
176            return Err(division_by_zero());
177        }
178        if factor.is_nan() {
179            return Err(datetime_out_of_range("interval"));
180        }
181        if let Some(infinity) = self.value().infinity() {
182            if factor.is_infinite() {
183                return Err(datetime_out_of_range("interval"));
184            }
185            return Ok(Self::infinite(if factor < 0.0 {
186                infinity.negate()
187            } else {
188                infinity
189            }));
190        }
191        self.scale(|field| field / factor)
192    }
193
194    /// `interval_justify_hours`: whole days of the time field move into the day field, then a day and a time of opposite signs trade one day so that both take one sign.
195    pub fn justify_hours(self) -> Result<Self> {
196        if self.is_reserved() {
197            return Ok(self);
198        }
199        let whole_days = self.micros / USECS_PER_DAY;
200        let days = self
201            .days
202            .checked_add(whole_days as i32)
203            .ok_or_else(|| datetime_out_of_range("interval"))?;
204        let (days, micros) = align_time_with_days(days, self.micros - whole_days * USECS_PER_DAY);
205        Ok(Self {
206            months: self.months,
207            days,
208            micros,
209        })
210    }
211
212    /// `interval_justify_days`: whole 30-day months of the day field move into the month field, then a month and a day of opposite signs trade one month.
213    pub fn justify_days(self) -> Result<Self> {
214        if self.is_reserved() {
215            return Ok(self);
216        }
217        let (months, days) = carry_whole_months(self.months, self.days)?;
218        let (months, days) = if months > 0 && days < 0 {
219            (months - 1, days + DAYS_PER_MONTH_INT)
220        } else if months < 0 && days > 0 {
221            (months + 1, days - DAYS_PER_MONTH_INT)
222        } else {
223            (months, days)
224        };
225        Ok(Self {
226            months,
227            days,
228            micros: self.micros,
229        })
230    }
231
232    /// `interval_justify_interval`: `justify_hours` and `justify_days` together, so that all three fields take one sign. Days are carried into months first when day and time share a sign, which keeps the day field from overflowing.
233    pub fn justify_interval(self) -> Result<Self> {
234        if self.is_reserved() {
235            return Ok(self);
236        }
237        let (mut months, mut days) = (self.months, self.days);
238        if (days > 0 && self.micros > 0) || (days < 0 && self.micros < 0) {
239            (months, days) = carry_whole_months(months, days)?;
240        }
241        let whole_days = self.micros / USECS_PER_DAY;
242        let mut micros = self.micros - whole_days * USECS_PER_DAY;
243        // Either the pre-carry left fewer than 30 days or day and time differ in sign, so this cannot overflow.
244        days = i32::try_from(i64::from(days) + whole_days)
245            .map_err(|_| datetime_out_of_range("interval"))?;
246        (months, days) = carry_whole_months(months, days)?;
247        if months > 0 && (days < 0 || (days == 0 && micros < 0)) {
248            days += DAYS_PER_MONTH_INT;
249            months -= 1;
250        } else if months < 0 && (days > 0 || (days == 0 && micros > 0)) {
251            days -= DAYS_PER_MONTH_INT;
252            months += 1;
253        }
254        (days, micros) = align_time_with_days(days, micros);
255        Ok(Self {
256            months,
257            days,
258            micros,
259        })
260    }
261
262    /// The body `interval_mul` and `interval_div` share: each field is scaled on its own and truncated, the fractional month cascades into days at 30 days a month and the fractional day into seconds at 86400 seconds a day, and nothing cascades upward. `TSROUND` keeps the cascaded fractions from drifting off whole microseconds.
263    fn scale(self, scale: impl Fn(f64) -> f64) -> Result<Self> {
264        let months = truncate_to_i32(scale(f64::from(self.months)))?;
265        let days = truncate_to_i32(scale(f64::from(self.days)))?;
266        let month_remainder_days =
267            timestamp_round((scale(f64::from(self.months)) - f64::from(months)) * DAYS_PER_MONTH);
268        let mut sec_remainder = timestamp_round(
269            (scale(f64::from(self.days)) - f64::from(days) + month_remainder_days
270                - f64::from(month_remainder_days as i32))
271                * SECS_PER_DAY,
272        );
273        let mut days = days;
274        // Rounding can leave a whole day of seconds, and the cascades together can exceed one.
275        if sec_remainder.abs() >= SECS_PER_DAY {
276            let whole_days = (sec_remainder / SECS_PER_DAY) as i32;
277            days = days
278                .checked_add(whole_days)
279                .ok_or_else(|| datetime_out_of_range("interval"))?;
280            sec_remainder -= f64::from(whole_days) * SECS_PER_DAY;
281        }
282        let days = days
283            .checked_add(month_remainder_days as i32)
284            .ok_or_else(|| datetime_out_of_range("interval"))?;
285        let micros = (scale(self.micros as f64) + sec_remainder * USECS_PER_SEC).round_ties_even();
286        if micros.is_nan() || !(micros >= i64::MIN as f64 && micros < -(i64::MIN as f64)) {
287            return Err(datetime_out_of_range("interval"));
288        }
289        Self {
290            months,
291            days,
292            micros: micros as i64,
293        }
294        .finite()
295    }
296}
297
298/// Move the whole 30-day months of `days` into `months`, the day count's sign kept as C's truncating division keeps it.
299fn carry_whole_months(months: i32, days: i32) -> Result<(i32, i32)> {
300    let whole_months = days / DAYS_PER_MONTH_INT;
301    let months = months
302        .checked_add(whole_months)
303        .ok_or_else(|| datetime_out_of_range("interval"))?;
304    Ok((months, days - whole_months * DAYS_PER_MONTH_INT))
305}
306
307/// Trade one day between a day count and a time of opposite signs, as the `justify` functions finish.
308fn align_time_with_days(days: i32, micros: i64) -> (i32, i64) {
309    if days > 0 && micros < 0 {
310        (days - 1, micros + USECS_PER_DAY)
311    } else if days < 0 && micros > 0 {
312        (days + 1, micros - USECS_PER_DAY)
313    } else {
314        (days, micros)
315    }
316}
317
318/// `FLOAT8_FITS_IN_INT32` followed by C's truncating conversion.
319fn truncate_to_i32(value: f64) -> Result<i32> {
320    if value.is_nan() || !(value >= f64::from(i32::MIN) && value < -f64::from(i32::MIN)) {
321        return Err(datetime_out_of_range("interval"));
322    }
323    Ok(value as i32)
324}
325
326/// `TSROUND`: round to the microsecond precision of a timestamp's seconds, halves to even as `rint` rounds.
327fn timestamp_round(value: f64) -> f64 {
328    (value * USECS_PER_SEC).round_ties_even() / USECS_PER_SEC
329}
330
331#[cfg(test)]
332mod tests;