Skip to main content

mz_repr/adt/
interval.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10//! A time interval abstract data type.
11
12use std::fmt::{self, Write};
13use std::sync::LazyLock;
14use std::time::Duration;
15
16use anyhow::{anyhow, bail};
17use mz_persist_types::columnar::FixedSizeCodec;
18use mz_proto::{RustType, TryFromProtoError};
19use num_traits::CheckedMul;
20#[cfg(any(test, feature = "proptest"))]
21use proptest::prelude::{Arbitrary, BoxedStrategy, Strategy, any};
22use serde::{Deserialize, Serialize};
23
24use crate::adt::datetime::DateTimeField;
25use crate::adt::numeric::{DecimalLike, Numeric};
26
27include!(concat!(env!("OUT_DIR"), "/mz_repr.adt.interval.rs"));
28
29/// An interval of time meant to express SQL intervals.
30///
31/// Obtained by parsing an `INTERVAL '<value>' <unit> [TO <precision>]`.
32#[derive(
33    Debug,
34    Clone,
35    Copy,
36    PartialEq,
37    Eq,
38    PartialOrd,
39    Ord,
40    Serialize,
41    Hash,
42    Deserialize
43)]
44pub struct Interval {
45    /// A possibly negative number of months for field types like `YEAR`
46    pub months: i32,
47    /// A possibly negative number of days.
48    ///
49    /// Irrespective of values, `days` will not be carried over into `months`.
50    pub days: i32,
51    /// A timespan represented in microseconds.
52    ///
53    /// Irrespective of values, `micros` will not be carried over into `days` or
54    /// `months`.
55    pub micros: i64,
56}
57
58impl Default for Interval {
59    fn default() -> Self {
60        Self {
61            months: 0,
62            days: 0,
63            micros: 0,
64        }
65    }
66}
67
68impl RustType<ProtoInterval> for Interval {
69    fn into_proto(&self) -> ProtoInterval {
70        ProtoInterval {
71            months: self.months,
72            days: self.days,
73            micros: self.micros,
74        }
75    }
76
77    fn from_proto(proto: ProtoInterval) -> Result<Self, TryFromProtoError> {
78        Ok(Interval {
79            months: proto.months,
80            days: proto.days,
81            micros: proto.micros,
82        })
83    }
84}
85
86impl num_traits::ops::checked::CheckedNeg for Interval {
87    fn checked_neg(&self) -> Option<Self> {
88        if let (Some(months), Some(days), Some(micros)) = (
89            self.months.checked_neg(),
90            self.days.checked_neg(),
91            self.micros.checked_neg(),
92        ) {
93            Some(Self::new(months, days, micros))
94        } else {
95            None
96        }
97    }
98}
99
100impl std::str::FromStr for Interval {
101    type Err = anyhow::Error;
102
103    fn from_str(s: &str) -> Result<Self, Self::Err> {
104        crate::strconv::parse_interval(s).map_err(|e| anyhow!(e))
105    }
106}
107
108static MONTH_OVERFLOW_ERROR: LazyLock<String> = LazyLock::new(|| {
109    format!(
110        "Overflows maximum months; cannot exceed {}/{} microseconds",
111        i32::MAX,
112        i32::MIN,
113    )
114});
115static DAY_OVERFLOW_ERROR: LazyLock<String> = LazyLock::new(|| {
116    format!(
117        "Overflows maximum days; cannot exceed {}/{} microseconds",
118        i32::MAX,
119        i32::MIN,
120    )
121});
122pub static USECS_PER_DAY: LazyLock<i64> = LazyLock::new(|| {
123    Interval::convert_date_time_unit(DateTimeField::Day, DateTimeField::Microseconds, 1i64).unwrap()
124});
125
126#[derive(Debug, Clone)]
127pub enum RoundBehavior {
128    Truncate,
129    Nearest,
130}
131
132impl Interval {
133    pub const CENTURY_PER_MILLENNIUM: u16 = 10;
134    pub const DECADE_PER_CENTURY: u16 = 10;
135    pub const YEAR_PER_DECADE: u16 = 10;
136    pub const MONTH_PER_YEAR: u16 = 12;
137    // Interval type considers 30 days == 1 month
138    pub const DAY_PER_MONTH: u16 = 30;
139    // Interval type considers 24 hours == 1 day
140    pub const HOUR_PER_DAY: u16 = 24;
141    pub const MINUTE_PER_HOUR: u16 = 60;
142    pub const SECOND_PER_MINUTE: u16 = 60;
143    pub const MILLISECOND_PER_SECOND: u16 = 1_000;
144    pub const MICROSECOND_PER_MILLISECOND: u16 = 1_000;
145    pub const NANOSECOND_PER_MICROSECOND: u16 = 1_000;
146    // PostgreSQL actually has a bug where when using EXTRACT it truncates this value to 365, but
147    // when using date_part it does not truncate this value. Therefore our EXTRACT function may differ
148    // from PostgreSQL.
149    // EXTRACT: https://github.com/postgres/postgres/blob/c2e8bd27519f47ff56987b30eb34a01969b9a9e8/src/backend/utils/adt/timestamp.c#L5270-L5273
150    // date_part: https://github.com/postgres/postgres/blob/c2e8bd27519f47ff56987b30eb34a01969b9a9e8/src/backend/utils/adt/timestamp.c#L5301
151    pub const EPOCH_DAYS_PER_YEAR: f64 = 365.25;
152
153    /// Constructs a new `Interval` with the specified units of time.
154    pub const fn new(months: i32, days: i32, micros: i64) -> Interval {
155        Interval {
156            months,
157            days,
158            micros,
159        }
160    }
161
162    /// Constructs a new `Interval` from component counts held in a wider type,
163    /// returning `None` if any of them overflows its field.
164    ///
165    /// This is the narrowing step for accumulations that sum the three
166    /// components independently in `i128`, as `sum(interval)` does.
167    pub fn try_new(months: i128, days: i128, micros: i128) -> Option<Interval> {
168        Some(Interval::new(
169            i32::try_from(months).ok()?,
170            i32::try_from(days).ok()?,
171            i64::try_from(micros).ok()?,
172        ))
173    }
174
175    /// Converts a `Duration` to an `Interval`. The resulting `Interval` will only have
176    /// microseconds. Errors if
177    /// - the number of microseconds doesn't fit in i64, or
178    /// - the `Duration` involves fractional microseconds.
179    pub fn from_duration(duration: &Duration) -> Result<Interval, anyhow::Error> {
180        if duration.subsec_nanos() % 1000 != 0 {
181            return Err(anyhow!(
182                "cannot convert Duration to Interval due to fractional microseconds"
183            ));
184        }
185        Ok(Interval {
186            months: 0,
187            days: 0,
188            micros: duration.as_micros().try_into()?,
189        })
190    }
191
192    /// Converts a `chrono::Duration` to an `Interval`. The resulting `Interval` will only have
193    /// microseconds, with the nanoseconds truncated.
194    pub fn from_chrono_duration(duration: chrono::Duration) -> Result<Self, anyhow::Error> {
195        let Some(micros) = duration.num_microseconds() else {
196            bail!("cannot convert Duration to Interval due to overflowed microseconds");
197        };
198        Ok(Self {
199            months: 0,
200            days: 0,
201            micros,
202        })
203    }
204
205    pub fn checked_add(&self, other: &Self) -> Option<Self> {
206        let months = match self.months.checked_add(other.months) {
207            Some(m) => m,
208            None => return None,
209        };
210        let days = match self.days.checked_add(other.days) {
211            Some(d) => d,
212            None => return None,
213        };
214        let micros = match self.micros.checked_add(other.micros) {
215            Some(us) => us,
216            None => return None,
217        };
218
219        Some(Self::new(months, days, micros))
220    }
221
222    pub fn checked_mul(&self, other: f64) -> Option<Self> {
223        self.checked_op(other, |f1, f2| f1 * f2)
224    }
225
226    pub fn checked_div(&self, other: f64) -> Option<Self> {
227        self.checked_op(other, |f1, f2| f1 / f2)
228    }
229
230    // TODO(benesch): the use of `as` in this function looks very sketchy.
231    // Rewrite.
232    #[allow(clippy::as_conversions)]
233    fn checked_op<F1>(&self, other: f64, op: F1) -> Option<Self>
234    where
235        F1: Fn(f64, f64) -> f64,
236    {
237        let months = op(f64::from(self.months), other);
238        if months.is_nan()
239            || months.is_infinite()
240            || months < i32::MIN.into()
241            || months > i32::MAX.into()
242        {
243            return None;
244        }
245
246        let days =
247            op(f64::from(self.days), other) + months.fract() * f64::from(Self::DAY_PER_MONTH);
248        if days.is_nan() || days.is_infinite() || days < i32::MIN.into() || days > i32::MAX.into() {
249            return None;
250        }
251
252        let micros = op(self.micros as f64, other)
253            + days.fract()
254                * f64::from(Self::HOUR_PER_DAY)
255                * f64::from(Self::MINUTE_PER_HOUR)
256                * f64::from(Self::SECOND_PER_MINUTE)
257                * f64::from(Self::MILLISECOND_PER_SECOND)
258                * f64::from(Self::MICROSECOND_PER_MILLISECOND);
259
260        if micros.is_nan()
261            || micros.is_infinite()
262            || Numeric::from(micros) < Numeric::from(i64::MIN)
263            || Numeric::from(micros) > Numeric::from(i64::MAX)
264        {
265            return None;
266        }
267
268        Some(Self::new(months as i32, days as i32, micros as i64))
269    }
270
271    /// Computes the millennium part of the interval.
272    ///
273    /// The millennium part is the number of whole millennia in the interval. For example,
274    /// this function returns `3` for the interval `3400 years`.
275    pub fn millennia(&self) -> i32 {
276        Self::convert_date_time_unit(DateTimeField::Month, DateTimeField::Millennium, self.months)
277            .unwrap()
278    }
279
280    /// Computes the century part of the interval.
281    ///
282    /// The century part is the number of whole centuries in the interval. For example,
283    /// this function returns `3` for the interval `340 years`.
284    pub fn centuries(&self) -> i32 {
285        Self::convert_date_time_unit(DateTimeField::Month, DateTimeField::Century, self.months)
286            .unwrap()
287    }
288
289    /// Computes the decade part of the interval.
290    ///
291    /// The decade part is the number of whole decades in the interval. For example,
292    /// this function returns `3` for the interval `34 years`.
293    pub fn decades(&self) -> i32 {
294        Self::convert_date_time_unit(DateTimeField::Month, DateTimeField::Decade, self.months)
295            .unwrap()
296    }
297
298    /// Computes the year part of the interval.
299    ///
300    /// The year part is the number of whole years in the interval. For example,
301    /// this function returns `3` for the interval `3 years 4 months`.
302    pub fn years(&self) -> i32 {
303        Self::convert_date_time_unit(DateTimeField::Month, DateTimeField::Year, self.months)
304            .unwrap()
305    }
306
307    /// Computes the quarter part of the interval.
308    ///
309    /// The quarter part is obtained from taking the number of whole months modulo 12,
310    /// and assigning quarter #1 for months 0-2, #2 for 3-5, #3 for 6-8 and #4 for 9-11.
311    /// For example, this function returns `4` for the interval `11 months`.
312    pub fn quarters(&self) -> i32 {
313        self.months() / 3 + 1
314    }
315
316    /// Computes the month part of the interval.
317    ///
318    /// The month part is the number of whole months in the interval, modulo 12.
319    /// For example, this function returns `4` for the interval `3 years 4
320    /// months`.
321    pub fn months(&self) -> i32 {
322        self.months % i32::from(Self::MONTH_PER_YEAR)
323    }
324
325    /// Computes the day part of the interval.
326    ///
327    /// The day part is the number of whole days in the interval. For example,
328    /// this function returns `5` for the interval `5 days 4 hours 3 minutes
329    /// 2.1 seconds`.
330    pub fn days(&self) -> i64 {
331        self.days.into()
332    }
333
334    /// Computes the hour part of the interval.
335    ///
336    /// The hour part is the number of whole hours in the interval, modulo 24.
337    /// For example, this function returns `4` for the interval `5 days 4
338    /// hours 3 minutes 2.1 seconds`.
339    pub fn hours(&self) -> i64 {
340        Self::convert_date_time_unit(
341            DateTimeField::Microseconds,
342            DateTimeField::Hour,
343            self.micros,
344        )
345        .unwrap()
346            % i64::from(Self::HOUR_PER_DAY)
347    }
348
349    /// Computes the minute part of the interval.
350    ///
351    /// The minute part is the number of whole minutes in the interval, modulo
352    /// 60. For example, this function returns `3` for the interval `5 days 4
353    /// hours 3 minutes 2.1 seconds`.
354    pub fn minutes(&self) -> i64 {
355        Self::convert_date_time_unit(
356            DateTimeField::Microseconds,
357            DateTimeField::Minute,
358            self.micros,
359        )
360        .unwrap()
361            % i64::from(Self::MINUTE_PER_HOUR)
362    }
363
364    /// Computes the second part of the interval.
365    ///
366    /// The second part is the number of fractional seconds in the interval,
367    /// modulo 60.0.
368    pub fn seconds<T>(&self) -> T
369    where
370        T: DecimalLike,
371    {
372        T::lossy_from(self.micros % 60_000_000) / T::from(1e6)
373    }
374
375    /// Computes the second part of the interval displayed in milliseconds.
376    ///
377    /// The second part is the number of fractional seconds in the interval,
378    /// modulo 60.0.
379    pub fn milliseconds<T>(&self) -> T
380    where
381        T: DecimalLike,
382    {
383        T::lossy_from(self.micros % 60_000_000) / T::from(1e3)
384    }
385
386    /// Computes the second part of the interval displayed in microseconds.
387    ///
388    /// The second part is the number of fractional seconds in the interval,
389    /// modulo 60.0.
390    pub fn microseconds<T>(&self) -> T
391    where
392        T: DecimalLike,
393    {
394        T::lossy_from(self.micros % 60_000_000)
395    }
396
397    /// Computes the nanosecond part of the interval.
398    pub fn nanoseconds(&self) -> i32 {
399        (self.micros % 1_000_000 * 1_000).try_into().unwrap()
400    }
401
402    /// Computes the total number of epoch seconds in the interval.
403    /// When extracting an epoch, PostgreSQL considers a year
404    /// 365.25 days.
405    pub fn as_epoch_seconds<T>(&self) -> T
406    where
407        T: DecimalLike,
408    {
409        let days = T::from(self.years()) * T::from(Self::EPOCH_DAYS_PER_YEAR)
410            + T::from(self.months()) * T::from(Self::DAY_PER_MONTH)
411            + T::from(self.days);
412        let seconds = days
413            * T::from(Self::HOUR_PER_DAY)
414            * T::from(Self::MINUTE_PER_HOUR)
415            * T::from(Self::SECOND_PER_MINUTE);
416
417        seconds
418            + T::lossy_from(self.micros)
419                / (T::from(Self::MICROSECOND_PER_MILLISECOND)
420                    * T::from(Self::MILLISECOND_PER_SECOND))
421    }
422
423    /// Computes the total number of microseconds in the interval.
424    pub fn as_microseconds(&self) -> i128 {
425        // unwrap is safe because i32::MAX/i32::MIN number of months will not overflow an i128 when
426        // converted to microseconds.
427        Self::convert_date_time_unit(
428            DateTimeField::Month,
429            DateTimeField::Microseconds,
430            i128::from(self.months),
431        ).unwrap() +
432        // unwrap is safe because i32::MAX/i32::MIN number of days will not overflow an i128 when
433        // converted to microseconds.
434        Self::convert_date_time_unit(
435            DateTimeField::Day,
436            DateTimeField::Microseconds,
437            i128::from(self.days),
438        ).unwrap() +
439        i128::from(self.micros)
440    }
441
442    /// Computes the total number of milliseconds in the interval. Discards fractional milliseconds!
443    pub fn as_milliseconds(&self) -> i128 {
444        self.as_microseconds() / 1000
445    }
446
447    /// Converts this `Interval`'s duration into `chrono::Duration`.
448    pub fn duration_as_chrono(&self) -> chrono::Duration {
449        use chrono::Duration;
450        Duration::try_days(self.days.into()).unwrap() + Duration::microseconds(self.micros)
451    }
452
453    pub fn duration(&self) -> Result<Duration, anyhow::Error> {
454        if self.months != 0 {
455            bail!("cannot convert interval with months to duration");
456        }
457        if self.is_negative() {
458            bail!("cannot convert negative interval to duration");
459        }
460        // Phrase the overflow ourselves rather than letting `TryFromIntError`
461        // through. Its `Display` is std's, and std has reworded it between
462        // toolchains, which would otherwise rewrite a user-facing error message
463        // out from under us.
464        let Ok(micros) = u64::try_from(self.as_microseconds()) else {
465            bail!("interval is too large to convert to duration");
466        };
467        Ok(Duration::from_micros(micros))
468    }
469
470    /// Truncate the "tail" of the interval, removing all time units less than `f`.
471    /// # Arguments
472    /// - `f`: Round the interval down to the specified time unit.
473    /// - `fsec_max_precision`: If `Some(x)`, keep only `x` places of microsecond precision.
474    ///    Must be `(0,6)`.
475    ///
476    /// # Errors
477    /// - If `fsec_max_precision` is not None or within (0,6).
478    pub fn truncate_low_fields(
479        &mut self,
480        f: DateTimeField,
481        fsec_max_precision: Option<u64>,
482        round_behavior: RoundBehavior,
483    ) -> Result<(), anyhow::Error> {
484        use DateTimeField::*;
485        match f {
486            Millennium => {
487                self.months -= self.months % (12 * 1000);
488                self.days = 0;
489                self.micros = 0;
490            }
491            Century => {
492                self.months -= self.months % (12 * 100);
493                self.days = 0;
494                self.micros = 0;
495            }
496            Decade => {
497                self.months -= self.months % (12 * 10);
498                self.days = 0;
499                self.micros = 0;
500            }
501            Year => {
502                self.months -= self.months % 12;
503                self.days = 0;
504                self.micros = 0;
505            }
506            Month => {
507                self.days = 0;
508                self.micros = 0;
509            }
510            // Round microseconds.
511            Second => {
512                let default_precision = 6;
513                let precision = match fsec_max_precision {
514                    Some(p) => p,
515                    None => default_precision,
516                };
517
518                if precision > default_precision {
519                    bail!(
520                        "SECOND precision must be (0, 6), have SECOND({})",
521                        precision
522                    )
523                }
524
525                let precision = match u32::try_from(precision) {
526                    Ok(p) => p,
527                    Err(_) => bail!(
528                        "SECOND precision must be (0, 6), have SECOND({})",
529                        precision
530                    ),
531                };
532                // Truncate sub-second part.
533                let remainder = self.micros % 10_i64.pow(6 - precision);
534                self.micros -= remainder;
535                // Check if value should round up/down to nearest fractional place.
536                if matches!(round_behavior, RoundBehavior::Nearest)
537                    && u64::from(precision) != default_precision
538                {
539                    let rounding_digit = remainder / 10_i64.pow(5 - precision);
540                    let micros = if rounding_digit > 4 {
541                        self.micros.checked_add(10_i64.pow(6 - precision))
542                    } else if rounding_digit < -4 {
543                        self.micros.checked_sub(10_i64.pow(6 - precision))
544                    } else {
545                        Some(self.micros)
546                    };
547                    let Some(micros) = micros else {
548                        bail!("interval field value out of range: \"{self}\"");
549                    };
550                    self.micros = micros;
551                }
552            }
553            Day => {
554                self.micros = 0;
555            }
556            Hour | Minute | Milliseconds | Microseconds => {
557                self.micros -= self.micros % f.micros_multiplier();
558            }
559        }
560        Ok(())
561    }
562
563    /// Returns a new Interval with only the time component
564    pub fn as_time_interval(&self) -> Self {
565        Self::new(0, 0, self.micros)
566    }
567
568    /// Returns true if combining all fields results in a negative number, false otherwise
569    pub fn is_negative(&self) -> bool {
570        self.as_microseconds() < 0
571    }
572
573    /// Convert val from source unit to dest unit. Does not maintain fractional values.
574    /// Returns None if the result overflows/underflows.
575    ///
576    /// WARNING: Due to the fact that Intervals consider months to have 30 days, you may get
577    /// unexpected and incorrect results when trying to convert from a non-year type to a year type
578    /// and vice versa. For example from years to days.
579    pub fn convert_date_time_unit<T>(
580        source: DateTimeField,
581        dest: DateTimeField,
582        val: T,
583    ) -> Option<T>
584    where
585        T: From<u16> + CheckedMul + std::ops::DivAssign,
586    {
587        if source < dest {
588            Self::convert_date_time_unit_increasing(source, dest, val)
589        } else if source > dest {
590            Self::convert_date_time_unit_decreasing(source, dest, val)
591        } else {
592            Some(val)
593        }
594    }
595
596    fn convert_date_time_unit_increasing<T>(
597        source: DateTimeField,
598        dest: DateTimeField,
599        val: T,
600    ) -> Option<T>
601    where
602        T: From<u16> + std::ops::DivAssign,
603    {
604        let mut cur_unit = source;
605        let mut res = val;
606        while cur_unit < dest {
607            let divisor: T = match cur_unit {
608                DateTimeField::Millennium => 1.into(),
609                DateTimeField::Century => Self::CENTURY_PER_MILLENNIUM.into(),
610                DateTimeField::Decade => Self::DECADE_PER_CENTURY.into(),
611                DateTimeField::Year => Self::YEAR_PER_DECADE.into(),
612                DateTimeField::Month => Self::MONTH_PER_YEAR.into(),
613                DateTimeField::Day => Self::DAY_PER_MONTH.into(),
614                DateTimeField::Hour => Self::HOUR_PER_DAY.into(),
615                DateTimeField::Minute => Self::MINUTE_PER_HOUR.into(),
616                DateTimeField::Second => Self::SECOND_PER_MINUTE.into(),
617                DateTimeField::Milliseconds => Self::MILLISECOND_PER_SECOND.into(),
618                DateTimeField::Microseconds => Self::MICROSECOND_PER_MILLISECOND.into(),
619            };
620            res /= divisor;
621            cur_unit = cur_unit.next_largest();
622        }
623
624        Some(res)
625    }
626
627    fn convert_date_time_unit_decreasing<T>(
628        source: DateTimeField,
629        dest: DateTimeField,
630        val: T,
631    ) -> Option<T>
632    where
633        T: From<u16> + CheckedMul,
634    {
635        let mut cur_unit = source;
636        let mut res = val;
637        while cur_unit > dest {
638            let multiplier: T = match cur_unit {
639                DateTimeField::Millennium => Self::CENTURY_PER_MILLENNIUM.into(),
640                DateTimeField::Century => Self::DECADE_PER_CENTURY.into(),
641                DateTimeField::Decade => Self::YEAR_PER_DECADE.into(),
642                DateTimeField::Year => Self::MONTH_PER_YEAR.into(),
643                DateTimeField::Month => Self::DAY_PER_MONTH.into(),
644                DateTimeField::Day => Self::HOUR_PER_DAY.into(),
645                DateTimeField::Hour => Self::MINUTE_PER_HOUR.into(),
646                DateTimeField::Minute => Self::SECOND_PER_MINUTE.into(),
647                DateTimeField::Second => Self::MILLISECOND_PER_SECOND.into(),
648                DateTimeField::Milliseconds => Self::MICROSECOND_PER_MILLISECOND.into(),
649                DateTimeField::Microseconds => 1.into(),
650            };
651            res = match res.checked_mul(&multiplier) {
652                Some(r) => r,
653                None => return None,
654            };
655            cur_unit = cur_unit.next_smallest();
656        }
657
658        Some(res)
659    }
660
661    /// Adjust interval so 'days' contains less than 30 days, adding the excess to 'months'.
662    pub fn justify_days(&self) -> Result<Self, anyhow::Error> {
663        let days_per_month = i32::from(Self::DAY_PER_MONTH);
664        let (mut months, mut days) = Self::justify_days_inner(self.months, self.days)?;
665        if months > 0 && days < 0 {
666            days += days_per_month;
667            months -= 1;
668        } else if months < 0 && days > 0 {
669            days -= days_per_month;
670            months += 1;
671        }
672
673        Ok(Self::new(months, days, self.micros))
674    }
675
676    fn justify_days_inner(months: i32, days: i32) -> Result<(i32, i32), anyhow::Error> {
677        let days_per_month = i32::from(Self::DAY_PER_MONTH);
678        let whole_month = days / days_per_month;
679        let days = days - whole_month * days_per_month;
680
681        let months = months
682            .checked_add(whole_month)
683            .ok_or_else(|| anyhow!(&*MONTH_OVERFLOW_ERROR))?;
684
685        Ok((months, days))
686    }
687
688    /// Adjust interval so 'micros' contains less than a whole day, adding the excess to 'days'.
689    pub fn justify_hours(&self) -> Result<Self, anyhow::Error> {
690        let (mut days, mut micros) = Self::justify_hours_inner(self.days, self.micros)?;
691        if days > 0 && micros < 0 {
692            micros += &*USECS_PER_DAY;
693            days -= 1;
694        } else if days < 0 && micros > 0 {
695            micros -= &*USECS_PER_DAY;
696            days += 1;
697        }
698
699        Ok(Self::new(self.months, days, micros))
700    }
701
702    fn justify_hours_inner(days: i32, micros: i64) -> Result<(i32, i64), anyhow::Error> {
703        let days = i32::try_from(micros / &*USECS_PER_DAY)
704            .ok()
705            .and_then(|d| days.checked_add(d))
706            .ok_or_else(|| anyhow!(&*DAY_OVERFLOW_ERROR))?;
707        let micros = micros % &*USECS_PER_DAY;
708
709        Ok((days, micros))
710    }
711
712    /// Adjust interval so 'days' contains less than 30 days, adding the excess to 'months'.
713    /// Adjust interval so 'micros' contains less than a whole day, adding the excess to 'days'.
714    /// Also, the sign bit on all three fields is made equal, so either all three fields are negative or all are positive.
715    pub fn justify_interval(&self) -> Result<Self, anyhow::Error> {
716        let days_per_month = i32::from(Self::DAY_PER_MONTH);
717        let mut months = self.months;
718        let mut days = self.days;
719        let micros = self.micros;
720        // We justify days twice to try to avoid an intermediate overflow of days if it would be
721        // able to fit in months.
722        if (days > 0 && micros > 0) || (days < 0 && micros < 0) {
723            let (m, d) = Self::justify_days_inner(self.months, self.days)?;
724            months = m;
725            days = d;
726        }
727        let (days, mut micros) = Self::justify_hours_inner(days, micros)?;
728        let (mut months, mut days) = Self::justify_days_inner(months, days)?;
729
730        if months > 0 && (days < 0 || (days == 0 && micros < 0)) {
731            days += days_per_month;
732            months -= 1;
733        } else if months < 0 && (days > 0 || (days == 0 && micros > 0)) {
734            days -= days_per_month;
735            months += 1;
736        }
737
738        if days > 0 && micros < 0 {
739            micros += &*USECS_PER_DAY;
740            days -= 1;
741        } else if days < 0 && micros > 0 {
742            micros -= &*USECS_PER_DAY;
743            days += 1;
744        }
745
746        Ok(Self::new(months, days, micros))
747    }
748}
749
750/// Format an interval in a human form
751///
752/// Example outputs:
753///
754/// * 1 year 2 mons 5 days 03:04:00
755/// * -1 year +5 days +18:59:29.3
756/// * 00:00:00
757impl fmt::Display for Interval {
758    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
759        let neg_months = self.months < 0;
760        let years = (self.months / 12).abs();
761        let months = (self.months % 12).abs();
762
763        let neg_days = self.days < 0;
764        let days = i64::from(self.days).abs();
765
766        let mut nanos = self.nanoseconds().abs();
767        let mut secs = (self.micros / 1_000_000).abs();
768
769        let sec_per_hr = 60 * 60;
770        let hours = secs / sec_per_hr;
771        secs %= sec_per_hr;
772
773        let sec_per_min = 60;
774        let minutes = secs / sec_per_min;
775        secs %= sec_per_min;
776
777        if years > 0 {
778            if neg_months {
779                f.write_char('-')?;
780            }
781            write!(f, "{} year", years)?;
782            if years > 1 || neg_months {
783                f.write_char('s')?;
784            }
785        }
786
787        if months > 0 {
788            if years != 0 {
789                f.write_char(' ')?;
790            }
791            if neg_months {
792                f.write_char('-')?;
793            }
794            write!(f, "{} mon", months)?;
795            if months > 1 || neg_months {
796                f.write_char('s')?;
797            }
798        }
799
800        if days != 0 {
801            if years > 0 || months > 0 {
802                f.write_char(' ')?;
803            }
804            if neg_months && !neg_days {
805                f.write_char('+')?;
806            }
807            write!(f, "{} day", self.days)?;
808            if self.days != 1 {
809                f.write_char('s')?;
810            }
811        }
812
813        let non_zero_hmsn = hours > 0 || minutes > 0 || secs > 0 || nanos > 0;
814
815        if (years == 0 && months == 0 && days == 0) || non_zero_hmsn {
816            if years > 0 || months > 0 || days > 0 {
817                f.write_char(' ')?;
818            }
819            if self.micros < 0 && non_zero_hmsn {
820                f.write_char('-')?;
821            } else if neg_days || (days == 0 && neg_months) {
822                f.write_char('+')?;
823            }
824            write!(f, "{:02}:{:02}:{:02}", hours, minutes, secs)?;
825            if nanos > 0 {
826                let mut width = 9;
827                while nanos % 10 == 0 {
828                    width -= 1;
829                    nanos /= 10;
830                }
831                write!(f, ".{:0width$}", nanos, width = width)?;
832            }
833        }
834
835        Ok(())
836    }
837}
838
839#[cfg(any(test, feature = "proptest"))]
840impl Arbitrary for Interval {
841    type Strategy = BoxedStrategy<Self>;
842    type Parameters = ();
843
844    fn arbitrary_with(_: Self::Parameters) -> Self::Strategy {
845        (
846            any::<i32>(),
847            any::<i32>(),
848            ((((i64::from(i32::MIN) * 60) - 59) * 60) * 1_000_000 - 59_999_999
849                ..(((i64::from(i32::MAX) * 60) + 59) * 60) * 1_000_000 + 59_999_999),
850        )
851            .prop_map(|(months, days, micros)| Interval {
852                months,
853                days,
854                micros,
855            })
856            .boxed()
857    }
858}
859
860/// An encoded packed variant of [`Interval`].
861///
862/// We uphold the variant that [`PackedInterval`] sorts the same as [`Interval`].
863#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
864pub struct PackedInterval([u8; Self::SIZE]);
865
866// `as` conversions are okay here because we're doing bit level logic to make
867// sure the sort order of the packed binary is correct. This is implementation
868// is proptest-ed below.
869#[allow(clippy::as_conversions)]
870impl FixedSizeCodec<Interval> for PackedInterval {
871    const SIZE: usize = 16;
872
873    fn as_bytes(&self) -> &[u8] {
874        &self.0[..]
875    }
876
877    fn from_bytes(slice: &[u8]) -> Result<Self, String> {
878        let buf: [u8; Self::SIZE] = slice.try_into().map_err(|_| {
879            format!(
880                "size for PackedInterval is {} bytes, got {}",
881                Self::SIZE,
882                slice.len()
883            )
884        })?;
885        Ok(PackedInterval(buf))
886    }
887
888    #[inline]
889    fn from_value(value: Interval) -> Self {
890        let mut buf = [0u8; 16];
891
892        // Note: We XOR the values to get correct sorting of negative values.
893
894        let months = (value.months as u32) ^ (0x8000_0000u32);
895        let days = (value.days as u32) ^ (0x8000_0000u32);
896        let micros = (value.micros as u64) ^ (0x8000_0000_0000_0000u64);
897
898        buf[..4].copy_from_slice(&months.to_be_bytes());
899        buf[4..8].copy_from_slice(&days.to_be_bytes());
900        buf[8..].copy_from_slice(&micros.to_be_bytes());
901
902        PackedInterval(buf)
903    }
904
905    #[inline]
906    fn into_value(self) -> Interval {
907        // Note: We XOR the values to get correct sorting of negative values.
908
909        let mut months = [0; 4];
910        months.copy_from_slice(&self.0[..4]);
911        let months = u32::from_be_bytes(months) ^ 0x8000_0000u32;
912
913        let mut days = [0; 4];
914        days.copy_from_slice(&self.0[4..8]);
915        let days = u32::from_be_bytes(days) ^ 0x8000_0000u32;
916
917        let mut micros = [0; 8];
918        micros.copy_from_slice(&self.0[8..]);
919        let micros = u64::from_be_bytes(micros) ^ 0x8000_0000_0000_0000u64;
920
921        Interval {
922            months: months as i32,
923            days: days as i32,
924            micros: micros as i64,
925        }
926    }
927}
928
929#[cfg(test)]
930mod test {
931    use mz_ore::assert_ok;
932    use mz_proto::protobuf_roundtrip;
933
934    use super::*;
935    use proptest::prelude::*;
936
937    #[mz_ore::test]
938    fn interval_fmt() {
939        fn mon(mon: i32) -> String {
940            Interval {
941                months: mon,
942                ..Default::default()
943            }
944            .to_string()
945        }
946
947        assert_eq!(mon(1), "1 mon");
948        assert_eq!(mon(12), "1 year");
949        assert_eq!(mon(13), "1 year 1 mon");
950        assert_eq!(mon(24), "2 years");
951        assert_eq!(mon(25), "2 years 1 mon");
952        assert_eq!(mon(26), "2 years 2 mons");
953
954        fn dur(days: i32, micros: i64) -> String {
955            Interval::new(0, days, micros).to_string()
956        }
957        assert_eq!(&dur(2, 0), "2 days");
958        assert_eq!(&dur(2, 3 * 60 * 60 * 1_000_000), "2 days 03:00:00");
959        assert_eq!(
960            &dur(
961                2,
962                (3 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (6 * 1_000_000)
963            ),
964            "2 days 03:45:06"
965        );
966        assert_eq!(
967            &dur(2, (3 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000)),
968            "2 days 03:45:00"
969        );
970        assert_eq!(&dur(2, 6 * 1_000_000), "2 days 00:00:06");
971        assert_eq!(
972            &dur(2, (45 * 60 * 1_000_000) + (6 * 1_000_000)),
973            "2 days 00:45:06"
974        );
975        assert_eq!(
976            &dur(2, (3 * 60 * 60 * 1_000_000) + (6 * 1_000_000)),
977            "2 days 03:00:06"
978        );
979        assert_eq!(
980            &dur(
981                0,
982                (3 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (6 * 1_000_000)
983            ),
984            "03:45:06"
985        );
986        assert_eq!(
987            &dur(0, (3 * 60 * 60 * 1_000_000) + (6 * 1_000_000)),
988            "03:00:06"
989        );
990        assert_eq!(&dur(0, 3 * 60 * 60 * 1_000_000), "03:00:00");
991        assert_eq!(&dur(0, (45 * 60 * 1_000_000) + (6 * 1_000_000)), "00:45:06");
992        assert_eq!(&dur(0, 45 * 60 * 1_000_000), "00:45:00");
993        assert_eq!(&dur(0, 6 * 1_000_000), "00:00:06");
994
995        assert_eq!(&dur(-2, -6 * 1_000_000), "-2 days -00:00:06");
996        assert_eq!(
997            &dur(-2, (-45 * 60 * 1_000_000) + (-6 * 1_000_000)),
998            "-2 days -00:45:06"
999        );
1000        assert_eq!(
1001            &dur(-2, (-3 * 60 * 60 * 1_000_000) + (-6 * 1_000_000)),
1002            "-2 days -03:00:06"
1003        );
1004        assert_eq!(
1005            &dur(
1006                0,
1007                (-3 * 60 * 60 * 1_000_000) + (-45 * 60 * 1_000_000) + (-6 * 1_000_000)
1008            ),
1009            "-03:45:06"
1010        );
1011        assert_eq!(
1012            &dur(0, (-3 * 60 * 60 * 1_000_000) + (-6 * 1_000_000)),
1013            "-03:00:06"
1014        );
1015        assert_eq!(&dur(0, -3 * 60 * 60 * 1_000_000), "-03:00:00");
1016        assert_eq!(
1017            &dur(0, (-45 * 60 * 1_000_000) + (-6 * 1_000_000)),
1018            "-00:45:06"
1019        );
1020        assert_eq!(&dur(0, -45 * 60 * 1_000_000), "-00:45:00");
1021        assert_eq!(&dur(0, -6 * 1_000_000), "-00:00:06");
1022
1023        fn mon_dur(mon: i32, days: i32, micros: i64) -> String {
1024            Interval::new(mon, days, micros).to_string()
1025        }
1026        assert_eq!(&mon_dur(1, 2, 6 * 1_000_000), "1 mon 2 days 00:00:06");
1027        assert_eq!(
1028            &mon_dur(1, 2, (45 * 60 * 1_000_000) + (6 * 1_000_000)),
1029            "1 mon 2 days 00:45:06"
1030        );
1031        assert_eq!(
1032            &mon_dur(1, 2, (3 * 60 * 60 * 1_000_000) + (6 * 1_000_000)),
1033            "1 mon 2 days 03:00:06"
1034        );
1035        assert_eq!(
1036            &mon_dur(
1037                26,
1038                0,
1039                (3 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (6 * 1_000_000)
1040            ),
1041            "2 years 2 mons 03:45:06"
1042        );
1043        assert_eq!(
1044            &mon_dur(26, 0, (3 * 60 * 60 * 1_000_000) + (6 * 1_000_000)),
1045            "2 years 2 mons 03:00:06"
1046        );
1047        assert_eq!(
1048            &mon_dur(26, 0, 3 * 60 * 60 * 1_000_000),
1049            "2 years 2 mons 03:00:00"
1050        );
1051        assert_eq!(
1052            &mon_dur(26, 0, (45 * 60 * 1_000_000) + (6 * 1_000_000)),
1053            "2 years 2 mons 00:45:06"
1054        );
1055        assert_eq!(
1056            &mon_dur(26, 0, 45 * 60 * 1_000_000),
1057            "2 years 2 mons 00:45:00"
1058        );
1059        assert_eq!(&mon_dur(26, 0, 6 * 1_000_000), "2 years 2 mons 00:00:06");
1060
1061        assert_eq!(
1062            &mon_dur(26, -2, -6 * 1_000_000),
1063            "2 years 2 mons -2 days -00:00:06"
1064        );
1065        assert_eq!(
1066            &mon_dur(26, -2, (-45 * 60 * 1_000_000) + (-6 * 1_000_000)),
1067            "2 years 2 mons -2 days -00:45:06"
1068        );
1069        assert_eq!(
1070            &mon_dur(26, -2, (-3 * 60 * 60 * 1_000_000) + (-6 * 1_000_000)),
1071            "2 years 2 mons -2 days -03:00:06"
1072        );
1073        assert_eq!(
1074            &mon_dur(
1075                26,
1076                0,
1077                (-3 * 60 * 60 * 1_000_000) + (-45 * 60 * 1_000_000) + (-6 * 1_000_000)
1078            ),
1079            "2 years 2 mons -03:45:06"
1080        );
1081        assert_eq!(
1082            &mon_dur(26, 0, (-3 * 60 * 60 * 1_000_000) + (-6 * 1_000_000)),
1083            "2 years 2 mons -03:00:06"
1084        );
1085        assert_eq!(
1086            &mon_dur(26, 0, -3 * 60 * 60 * 1_000_000),
1087            "2 years 2 mons -03:00:00"
1088        );
1089        assert_eq!(
1090            &mon_dur(26, 0, (-45 * 60 * 1_000_000) + (-6 * 1_000_000)),
1091            "2 years 2 mons -00:45:06"
1092        );
1093        assert_eq!(
1094            &mon_dur(26, 0, -45 * 60 * 1_000_000),
1095            "2 years 2 mons -00:45:00"
1096        );
1097        assert_eq!(&mon_dur(26, 0, -6 * 1_000_000), "2 years 2 mons -00:00:06");
1098
1099        assert_eq!(&mon_dur(-1, 2, 6 * 1_000_000), "-1 mons +2 days 00:00:06");
1100        assert_eq!(
1101            &mon_dur(-1, 2, (45 * 60 * 1_000_000) + (6 * 1_000_000)),
1102            "-1 mons +2 days 00:45:06"
1103        );
1104        assert_eq!(
1105            &mon_dur(-1, 2, (3 * 60 * 60 * 1_000_000) + (6 * 1_000_000)),
1106            "-1 mons +2 days 03:00:06"
1107        );
1108        assert_eq!(
1109            &mon_dur(
1110                -26,
1111                0,
1112                (3 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (6 * 1_000_000)
1113            ),
1114            "-2 years -2 mons +03:45:06"
1115        );
1116        assert_eq!(
1117            &mon_dur(-26, 0, (3 * 60 * 60 * 1_000_000) + (6 * 1_000_000)),
1118            "-2 years -2 mons +03:00:06"
1119        );
1120        assert_eq!(
1121            &mon_dur(-26, 0, 3 * 60 * 60 * 1_000_000),
1122            "-2 years -2 mons +03:00:00"
1123        );
1124        assert_eq!(
1125            &mon_dur(-26, 0, (45 * 60 * 1_000_000) + (6 * 1_000_000)),
1126            "-2 years -2 mons +00:45:06"
1127        );
1128        assert_eq!(
1129            &mon_dur(-26, 0, 45 * 60 * 1_000_000),
1130            "-2 years -2 mons +00:45:00"
1131        );
1132        assert_eq!(
1133            &mon_dur(-26, 0, 6 * 1_000_000),
1134            "-2 years -2 mons +00:00:06"
1135        );
1136
1137        assert_eq!(
1138            &mon_dur(-26, -2, -6 * 1_000_000),
1139            "-2 years -2 mons -2 days -00:00:06"
1140        );
1141        assert_eq!(
1142            &mon_dur(-26, -2, (-45 * 60 * 1_000_000) + (-6 * 1_000_000)),
1143            "-2 years -2 mons -2 days -00:45:06"
1144        );
1145        assert_eq!(
1146            &mon_dur(-26, -2, (-3 * 60 * 60 * 1_000_000) + (-6 * 1_000_000)),
1147            "-2 years -2 mons -2 days -03:00:06"
1148        );
1149        assert_eq!(
1150            &mon_dur(
1151                -26,
1152                0,
1153                (-3 * 60 * 60 * 1_000_000) + (-45 * 60 * 1_000_000) + (-6 * 1_000_000)
1154            ),
1155            "-2 years -2 mons -03:45:06"
1156        );
1157        assert_eq!(
1158            &mon_dur(-26, 0, (-3 * 60 * 60 * 1_000_000) + (-6 * 1_000_000)),
1159            "-2 years -2 mons -03:00:06"
1160        );
1161        assert_eq!(
1162            &mon_dur(-26, 0, -3 * 60 * 60 * 1_000_000),
1163            "-2 years -2 mons -03:00:00"
1164        );
1165        assert_eq!(
1166            &mon_dur(-26, 0, (-45 * 60 * 1_000_000) + (-6 * 1_000_000)),
1167            "-2 years -2 mons -00:45:06"
1168        );
1169        assert_eq!(
1170            &mon_dur(-26, 0, -45 * 60 * 1_000_000),
1171            "-2 years -2 mons -00:45:00"
1172        );
1173        assert_eq!(
1174            &mon_dur(-26, 0, -6 * 1_000_000),
1175            "-2 years -2 mons -00:00:06"
1176        );
1177    }
1178
1179    #[mz_ore::test]
1180    fn test_interval_value_truncate_low_fields() {
1181        use DateTimeField::*;
1182
1183        let mut test_cases = [
1184            (
1185                Year,
1186                None,
1187                (
1188                    321,
1189                    7,
1190                    (13 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (21 * 1_000_000) + 321_000,
1191                ),
1192                (26 * 12, 0, 0),
1193            ),
1194            (
1195                Month,
1196                None,
1197                (
1198                    321,
1199                    7,
1200                    (13 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (21 * 1_000_000) + 321_000,
1201                ),
1202                (321, 0, 0),
1203            ),
1204            (
1205                Day,
1206                None,
1207                (
1208                    321,
1209                    7,
1210                    (13 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (21 * 1_000_000) + 321_000,
1211                ),
1212                (321, 7, 0),
1213            ),
1214            (
1215                Hour,
1216                None,
1217                (
1218                    321,
1219                    7,
1220                    (13 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (21 * 1_000_000) + 321_000,
1221                ),
1222                (321, 7, 13 * 60 * 60 * 1_000_000),
1223            ),
1224            (
1225                Minute,
1226                None,
1227                (
1228                    321,
1229                    7,
1230                    (13 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (21 * 1_000_000) + 321_000,
1231                ),
1232                (321, 7, (13 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000)),
1233            ),
1234            (
1235                Second,
1236                None,
1237                (
1238                    321,
1239                    7,
1240                    (13 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (21 * 1_000_000) + 321_000,
1241                ),
1242                (
1243                    321,
1244                    7,
1245                    (13 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (21 * 1_000_000) + 321_000,
1246                ),
1247            ),
1248            (
1249                Second,
1250                Some(1),
1251                (
1252                    321,
1253                    7,
1254                    (13 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (21 * 1_000_000) + 321_000,
1255                ),
1256                (
1257                    321,
1258                    7,
1259                    (13 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (21 * 1_000_000) + 300_000,
1260                ),
1261            ),
1262            (
1263                Second,
1264                Some(0),
1265                (
1266                    321,
1267                    7,
1268                    (13 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (21 * 1_000_000) + 321_000,
1269                ),
1270                (
1271                    321,
1272                    7,
1273                    (13 * 60 * 60 * 1_000_000) + (45 * 60 * 1_000_000) + (21 * 1_000_000),
1274                ),
1275            ),
1276        ];
1277
1278        for test in test_cases.iter_mut() {
1279            let mut i = Interval::new((test.2).0, (test.2).1, (test.2).2);
1280            let j = Interval::new((test.3).0, (test.3).1, (test.3).2);
1281
1282            i.truncate_low_fields(test.0, test.1, RoundBehavior::Nearest)
1283                .unwrap();
1284
1285            if i != j {
1286                panic!(
1287                    "test_interval_value_truncate_low_fields failed on {} \n actual: {:?} \n expected: {:?}",
1288                    test.0, i, j
1289                );
1290            }
1291        }
1292    }
1293
1294    #[mz_ore::test]
1295    fn test_convert_date_time_unit() {
1296        assert_eq!(
1297            Some(1_123_200_000_000),
1298            Interval::convert_date_time_unit(
1299                DateTimeField::Day,
1300                DateTimeField::Microseconds,
1301                13i64
1302            )
1303        );
1304
1305        assert_eq!(
1306            Some(3_558_399_705),
1307            Interval::convert_date_time_unit(
1308                DateTimeField::Milliseconds,
1309                DateTimeField::Month,
1310                i64::MAX
1311            )
1312        );
1313
1314        assert_eq!(
1315            None,
1316            Interval::convert_date_time_unit(
1317                DateTimeField::Minute,
1318                DateTimeField::Second,
1319                i32::MAX
1320            )
1321        );
1322
1323        assert_eq!(
1324            Some(1),
1325            Interval::convert_date_time_unit(DateTimeField::Day, DateTimeField::Year, 365)
1326        );
1327
1328        // Strange behavior due to months having 30 days
1329        assert_eq!(
1330            Some(360),
1331            Interval::convert_date_time_unit(DateTimeField::Year, DateTimeField::Day, 1)
1332        );
1333    }
1334
1335    #[mz_ore::test]
1336    fn proptest_packed_interval_roundtrips() {
1337        fn roundtrip_interval(og: Interval) {
1338            let packed = PackedInterval::from_value(og);
1339            let rnd = packed.into_value();
1340
1341            assert_eq!(og, rnd);
1342        }
1343
1344        proptest!(|(interval in any::<Interval>())| {
1345            roundtrip_interval(interval);
1346        });
1347    }
1348
1349    #[mz_ore::test]
1350    #[cfg_attr(miri, ignore)] // too slow
1351    fn proptest_packed_interval_sorts() {
1352        fn sort_intervals(mut og: Vec<Interval>) {
1353            let mut packed: Vec<_> = og.iter().copied().map(PackedInterval::from_value).collect();
1354
1355            og.sort();
1356            packed.sort();
1357
1358            let rnd: Vec<_> = packed.into_iter().map(PackedInterval::into_value).collect();
1359
1360            assert_eq!(og, rnd);
1361        }
1362
1363        proptest!(|(interval in any::<Vec<Interval>>())| {
1364            sort_intervals(interval);
1365        });
1366    }
1367
1368    // `Interval` <-> `ProtoInterval` is a field-for-field copy today, so this only
1369    // bites once the two structs drift: a field added to `Interval` but not carried
1370    // through `ProtoInterval` silently decodes as that field's default. NOTE: the
1371    // guard is blind unless `Interval::arbitrary` also generates the new field, so
1372    // extend the strategy alongside the field.
1373    proptest! {
1374        #[mz_ore::test]
1375        fn interval_protobuf_roundtrip(expect in any::<Interval>()) {
1376            let actual = protobuf_roundtrip::<_, ProtoInterval>(&expect);
1377            assert_ok!(actual);
1378            assert_eq!(actual.unwrap(), expect);
1379        }
1380    }
1381}