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(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Hash, Deserialize)]
35pub struct Date {
36 days: i32,
38}
39
40impl RustType<ProtoDate> for Date {
41 fn into_proto(&self) -> ProtoDate {
42 ProtoDate { days: self.days }
43 }
44
45 fn from_proto(proto: ProtoDate) -> Result<Self, TryFromProtoError> {
46 Ok(Date { days: proto.days })
47 }
48}
49
50impl std::str::FromStr for Date {
51 type Err = anyhow::Error;
52
53 fn from_str(s: &str) -> Result<Self, Self::Err> {
54 crate::strconv::parse_date(s).map_err(|e| anyhow!(e))
55 }
56}
57
58static PG_EPOCH: LazyLock<NaiveDate> =
59 LazyLock::new(|| NaiveDate::from_ymd_opt(2000, 1, 1).unwrap());
60
61impl Date {
62 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;
71
72 pub fn from_pg_epoch(days: i32) -> Result<Date, DateError> {
75 if days < Self::LOW_DAYS || days > Self::HIGH_DAYS {
76 Err(DateError::OutOfRange)
77 } else {
78 Ok(Date { days })
79 }
80 }
81
82 pub fn from_unix_epoch(unix_days: i32) -> Result<Date, DateError> {
84 let pg_days = unix_days.saturating_sub(Self::UNIX_EPOCH_TO_PG_EPOCH);
85 if pg_days == i32::MIN {
86 return Err(DateError::OutOfRange);
87 }
88 Self::from_pg_epoch(pg_days)
89 }
90
91 pub fn pg_epoch_days(&self) -> i32 {
93 self.days
94 }
95
96 pub fn is_finite(&self) -> bool {
102 self.days != i32::MAX && self.days != i32::MIN
103 }
104
105 pub fn unix_epoch_days(&self) -> i32 {
107 assert!(self.is_finite());
108 self.days + Self::UNIX_EPOCH_TO_PG_EPOCH
111 }
112
113 pub fn checked_add(self, days: i32) -> Result<Date, DateError> {
115 let days = if let Some(days) = self.days.checked_add(days) {
116 days
117 } else {
118 return Err(DateError::OutOfRange);
119 };
120 Self::from_pg_epoch(days)
121 }
122}
123
124impl Sub for Date {
125 type Output = i32;
126
127 fn sub(self, rhs: Self) -> Self::Output {
128 assert!(self.is_finite());
129 self.days - rhs.days
130 }
131}
132
133impl From<Date> for NaiveDate {
134 fn from(date: Date) -> Self {
135 Self::from(&date)
136 }
137}
138
139impl From<&Date> for NaiveDate {
140 fn from(date: &Date) -> Self {
141 let days = date
142 .pg_epoch_days()
143 .checked_add(Date::CE_EPOCH_TO_PG_EPOCH)
144 .expect("out of range date are prevented");
145 NaiveDate::from_num_days_from_ce_opt(days).unwrap()
146 }
147}
148
149impl TryFrom<NaiveDate> for Date {
150 type Error = DateError;
151
152 fn try_from(value: NaiveDate) -> Result<Self, Self::Error> {
153 let d = value.signed_duration_since(*PG_EPOCH);
154 let days: i32 = d.num_days().try_into().map_err(|_| DateError::OutOfRange)?;
155 Self::from_pg_epoch(days)
156 }
157}
158
159impl fmt::Display for Date {
161 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
162 let d: NaiveDate = (*self).into();
163 d.format("%Y-%m-%d").fmt(f)
164 }
165}
166
167#[cfg(test)]
168mod test {
169 use super::*;
170
171 #[mz_ore::test]
172 fn test_date() {
173 let pgepoch = Date::from_pg_epoch(0).unwrap();
174 let unixepoch = Date::from_unix_epoch(0).unwrap();
175 assert_eq!(pgepoch.pg_epoch_days(), 0);
176 assert_eq!(pgepoch.unix_epoch_days(), 10957);
177 assert_eq!(unixepoch.pg_epoch_days(), -10957);
178 assert_eq!(unixepoch.unix_epoch_days(), 0);
179 assert_eq!(
180 NaiveDate::from(pgepoch),
181 NaiveDate::from_ymd_opt(2000, 1, 1).unwrap()
182 );
183 assert_eq!(
184 pgepoch,
185 Date::try_from(NaiveDate::from_ymd_opt(2000, 1, 1).unwrap()).unwrap()
186 );
187 assert_eq!(
188 unixepoch,
189 Date::try_from(NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).unwrap()
190 );
191 assert_eq!(
192 unixepoch,
193 Date::try_from(NaiveDate::from_ymd_opt(1970, 1, 1).unwrap()).unwrap()
194 );
195 assert!(pgepoch > unixepoch);
196 }
197}