1use 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#[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 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 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; const CE_EPOCH_TO_PG_EPOCH: i32 = 730120; pub const LOW_DAYS: i32 = -2451545; pub const HIGH_DAYS: i32 = 95_015_279;
86
87 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 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 pub fn pg_epoch_days(&self) -> i32 {
108 self.days
109 }
110
111 pub fn is_finite(&self) -> bool {
117 self.days != i32::MAX && self.days != i32::MIN
118 }
119
120 pub fn unix_epoch_days(&self) -> i32 {
122 assert!(self.is_finite());
123 self.days + Self::UNIX_EPOCH_TO_PG_EPOCH
126 }
127
128 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
174impl 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}