Skip to main content

mz_repr/adt/
date.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 Date abstract data type.
11
12use std::convert::TryFrom;
13use std::fmt;
14use std::ops::Sub;
15use std::sync::LazyLock;
16
17use anyhow::anyhow;
18use chrono::NaiveDate;
19use mz_proto::{RustType, TryFromProtoError};
20use serde::{Deserialize, Serialize};
21use thiserror::Error;
22
23include!(concat!(env!("OUT_DIR"), "/mz_repr.adt.date.rs"));
24
25#[derive(Debug, Error)]
26pub enum DateError {
27    #[error("data out of range")]
28    OutOfRange,
29}
30
31/// A Postgres-compatible Date. Additionally clamp valid dates for the range
32/// that chrono supports to allow for safe string operations. Infinite dates are
33/// not yet supported.
34#[derive(
35    Debug,
36    Clone,
37    Copy,
38    PartialEq,
39    Eq,
40    PartialOrd,
41    Ord,
42    Serialize,
43    Hash,
44    Deserialize
45)]
46pub struct Date {
47    /// Number of days from the postgres epoch (2000-01-01).
48    days: i32,
49}
50
51impl RustType<ProtoDate> for Date {
52    fn into_proto(&self) -> ProtoDate {
53        ProtoDate { days: self.days }
54    }
55
56    fn from_proto(proto: ProtoDate) -> Result<Self, TryFromProtoError> {
57        // Go through `from_pg_epoch` so out-of-range days are rejected here.
58        // Pushing them into a `Row` succeeds, but `read_datum` would later
59        // panic when reconstructing the date.
60        Date::from_pg_epoch(proto.days)
61            .map_err(|err| TryFromProtoError::InvalidFieldError(err.to_string()))
62    }
63}
64
65impl std::str::FromStr for Date {
66    type Err = anyhow::Error;
67
68    fn from_str(s: &str) -> Result<Self, Self::Err> {
69        crate::strconv::parse_date(s).map_err(|e| anyhow!(e))
70    }
71}
72
73static PG_EPOCH: LazyLock<NaiveDate> =
74    LazyLock::new(|| NaiveDate::from_ymd_opt(2000, 1, 1).unwrap());
75
76impl Date {
77    pub const UNIX_EPOCH_TO_PG_EPOCH: i32 = 10957; // Number of days from 1970-01-01 to 2000-01-01.
78    const CE_EPOCH_TO_PG_EPOCH: i32 = 730120; // Number of days since 0001-01-01 to 2000-01-01.
79    pub const LOW_DAYS: i32 = -2451545; // 4714-11-24 BC
80
81    /// Largest date support by Materialize. Although Postgres can go up to
82    /// 5874897-12-31, chrono is limited to December 31, 262142, which we mirror
83    /// here so we can use chrono's formatting methods and have guaranteed safe
84    /// conversions.
85    pub const HIGH_DAYS: i32 = 95_015_279;
86
87    /// Constructs a new `Date` as the days since the postgres epoch
88    /// (2000-01-01).
89    pub fn from_pg_epoch(days: i32) -> Result<Date, DateError> {
90        if days < Self::LOW_DAYS || days > Self::HIGH_DAYS {
91            Err(DateError::OutOfRange)
92        } else {
93            Ok(Date { days })
94        }
95    }
96
97    /// Constructs a new `Date` as the days since the Unix epoch.
98    pub fn from_unix_epoch(unix_days: i32) -> Result<Date, DateError> {
99        let pg_days = unix_days.saturating_sub(Self::UNIX_EPOCH_TO_PG_EPOCH);
100        if pg_days == i32::MIN {
101            return Err(DateError::OutOfRange);
102        }
103        Self::from_pg_epoch(pg_days)
104    }
105
106    /// Returns the number of days since the postgres epoch.
107    pub fn pg_epoch_days(&self) -> i32 {
108        self.days
109    }
110
111    /// Returns whether this is the infinity or -infinity date.
112    ///
113    /// Currently we do not support these, so this function is a light
114    /// protection against if they are added for functions that will produce
115    /// incorrect results for these values.
116    pub fn is_finite(&self) -> bool {
117        self.days != i32::MAX && self.days != i32::MIN
118    }
119
120    /// Returns the number of days since the Unix epoch.
121    pub fn unix_epoch_days(&self) -> i32 {
122        assert!(self.is_finite());
123        // Guaranteed to be safe because we clamp the high date by less than the
124        // result of this.
125        self.days + Self::UNIX_EPOCH_TO_PG_EPOCH
126    }
127
128    /// Returns this date with `days` added to it.
129    pub fn checked_add(self, days: i32) -> Result<Date, DateError> {
130        let days = if let Some(days) = self.days.checked_add(days) {
131            days
132        } else {
133            return Err(DateError::OutOfRange);
134        };
135        Self::from_pg_epoch(days)
136    }
137}
138
139impl Sub for Date {
140    type Output = i32;
141
142    fn sub(self, rhs: Self) -> Self::Output {
143        assert!(self.is_finite());
144        self.days - rhs.days
145    }
146}
147
148impl From<Date> for NaiveDate {
149    fn from(date: Date) -> Self {
150        Self::from(&date)
151    }
152}
153
154impl From<&Date> for NaiveDate {
155    fn from(date: &Date) -> Self {
156        let days = date
157            .pg_epoch_days()
158            .checked_add(Date::CE_EPOCH_TO_PG_EPOCH)
159            .expect("out of range date are prevented");
160        NaiveDate::from_num_days_from_ce_opt(days).unwrap()
161    }
162}
163
164impl TryFrom<NaiveDate> for Date {
165    type Error = DateError;
166
167    fn try_from(value: NaiveDate) -> Result<Self, Self::Error> {
168        let d = value.signed_duration_since(*PG_EPOCH);
169        let days: i32 = d.num_days().try_into().map_err(|_| DateError::OutOfRange)?;
170        Self::from_pg_epoch(days)
171    }
172}
173
174/// Format an Date in a human form
175impl fmt::Display for Date {
176    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
177        let d: NaiveDate = (*self).into();
178        d.format("%Y-%m-%d").fmt(f)
179    }
180}
181
182#[cfg(test)]
183mod test {
184    use super::*;
185
186    #[mz_ore::test]
187    fn test_date() {
188        let pgepoch = Date::from_pg_epoch(0).unwrap();
189        let unixepoch = Date::from_unix_epoch(0).unwrap();
190        assert_eq!(pgepoch.pg_epoch_days(), 0);
191        assert_eq!(pgepoch.unix_epoch_days(), 10957);
192        assert_eq!(unixepoch.pg_epoch_days(), -10957);
193        assert_eq!(unixepoch.unix_epoch_days(), 0);
194        assert_eq!(
195            NaiveDate::from(pgepoch),
196            NaiveDate::from_ymd_opt(2000, 1, 1).unwrap()
197        );
198        assert_eq!(
199            pgepoch,
200            Date::try_from(NaiveDate::from_ymd_opt(2000, 1, 1).unwrap()).unwrap()
201        );
202        assert_eq!(
203            unixepoch,
204            Date::try_from(NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).unwrap()
205        );
206        assert_eq!(
207            unixepoch,
208            Date::try_from(NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).unwrap()
209        );
210        assert!(pgepoch > unixepoch);
211    }
212}