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 Ok(Date { days: proto.days })
58 }
59}
60
61impl std::str::FromStr for Date {
62 type Err = anyhow::Error;
63
64 fn from_str(s: &str) -> Result<Self, Self::Err> {
65 crate::strconv::parse_date(s).map_err(|e| anyhow!(e))
66 }
67}
68
69static PG_EPOCH: LazyLock<NaiveDate> =
70 LazyLock::new(|| NaiveDate::from_ymd_opt(2000, 1, 1).unwrap());
71
72impl Date {
73 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;
82
83 pub fn from_pg_epoch(days: i32) -> Result<Date, DateError> {
86 if days < Self::LOW_DAYS || days > Self::HIGH_DAYS {
87 Err(DateError::OutOfRange)
88 } else {
89 Ok(Date { days })
90 }
91 }
92
93 pub fn from_unix_epoch(unix_days: i32) -> Result<Date, DateError> {
95 let pg_days = unix_days.saturating_sub(Self::UNIX_EPOCH_TO_PG_EPOCH);
96 if pg_days == i32::MIN {
97 return Err(DateError::OutOfRange);
98 }
99 Self::from_pg_epoch(pg_days)
100 }
101
102 pub fn pg_epoch_days(&self) -> i32 {
104 self.days
105 }
106
107 pub fn is_finite(&self) -> bool {
113 self.days != i32::MAX && self.days != i32::MIN
114 }
115
116 pub fn unix_epoch_days(&self) -> i32 {
118 assert!(self.is_finite());
119 self.days + Self::UNIX_EPOCH_TO_PG_EPOCH
122 }
123
124 pub fn checked_add(self, days: i32) -> Result<Date, DateError> {
126 let days = if let Some(days) = self.days.checked_add(days) {
127 days
128 } else {
129 return Err(DateError::OutOfRange);
130 };
131 Self::from_pg_epoch(days)
132 }
133}
134
135impl Sub for Date {
136 type Output = i32;
137
138 fn sub(self, rhs: Self) -> Self::Output {
139 assert!(self.is_finite());
140 self.days - rhs.days
141 }
142}
143
144impl From<Date> for NaiveDate {
145 fn from(date: Date) -> Self {
146 Self::from(&date)
147 }
148}
149
150impl From<&Date> for NaiveDate {
151 fn from(date: &Date) -> Self {
152 let days = date
153 .pg_epoch_days()
154 .checked_add(Date::CE_EPOCH_TO_PG_EPOCH)
155 .expect("out of range date are prevented");
156 NaiveDate::from_num_days_from_ce_opt(days).unwrap()
157 }
158}
159
160impl TryFrom<NaiveDate> for Date {
161 type Error = DateError;
162
163 fn try_from(value: NaiveDate) -> Result<Self, Self::Error> {
164 let d = value.signed_duration_since(*PG_EPOCH);
165 let days: i32 = d.num_days().try_into().map_err(|_| DateError::OutOfRange)?;
166 Self::from_pg_epoch(days)
167 }
168}
169
170impl fmt::Display for Date {
172 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
173 let d: NaiveDate = (*self).into();
174 d.format("%Y-%m-%d").fmt(f)
175 }
176}
177
178#[cfg(test)]
179mod test {
180 use super::*;
181
182 #[mz_ore::test]
183 fn test_date() {
184 let pgepoch = Date::from_pg_epoch(0).unwrap();
185 let unixepoch = Date::from_unix_epoch(0).unwrap();
186 assert_eq!(pgepoch.pg_epoch_days(), 0);
187 assert_eq!(pgepoch.unix_epoch_days(), 10957);
188 assert_eq!(unixepoch.pg_epoch_days(), -10957);
189 assert_eq!(unixepoch.unix_epoch_days(), 0);
190 assert_eq!(
191 NaiveDate::from(pgepoch),
192 NaiveDate::from_ymd_opt(2000, 1, 1).unwrap()
193 );
194 assert_eq!(
195 pgepoch,
196 Date::try_from(NaiveDate::from_ymd_opt(2000, 1, 1).unwrap()).unwrap()
197 );
198 assert_eq!(
199 unixepoch,
200 Date::try_from(NaiveDate::from_ymd_opt(1970, 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!(pgepoch > unixepoch);
207 }
208}