1use std::fmt::{Debug, Formatter};
4
5#[cfg(feature = "bounded-static")]
6use bounded_static::{IntoBoundedStatic, ToBoundedStatic};
7use chrono::{Datelike, FixedOffset};
8#[cfg(feature = "serde")]
9use serde::{Deserialize, Serialize};
10
11use crate::datetime::error::{DateTimeError, NaiveDateError};
12
13#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
14#[derive(Clone, Eq, PartialEq, Hash)]
15pub struct DateTime(chrono::DateTime<FixedOffset>);
16
17impl DateTime {
18 pub fn validate(value: &chrono::DateTime<FixedOffset>) -> Result<(), DateTimeError> {
19 if !(0..=9999).contains(&value.year()) {
21 return Err(DateTimeError::YearOutOfRange { got: value.year() });
22 }
23
24 if value.timestamp_subsec_nanos() != 0 {
25 return Err(DateTimeError::UnalignedNanoSeconds {
26 got: value.timestamp_subsec_nanos(),
27 });
28 }
29
30 if value.offset().local_minus_utc() % 60 != 0 {
31 return Err(DateTimeError::UnalignedOffset {
32 got: value.offset().local_minus_utc() % 60,
33 });
34 }
35
36 Ok(())
37 }
38
39 #[cfg(feature = "unvalidated")]
47 #[cfg_attr(docsrs, doc(cfg(feature = "unvalidated")))]
48 pub fn unvalidated(value: chrono::DateTime<FixedOffset>) -> Self {
49 Self(value)
50 }
51}
52
53impl TryFrom<chrono::DateTime<FixedOffset>> for DateTime {
54 type Error = DateTimeError;
55
56 fn try_from(value: chrono::DateTime<FixedOffset>) -> Result<Self, Self::Error> {
57 Self::validate(&value)?;
58
59 Ok(Self(value))
60 }
61}
62
63impl Debug for DateTime {
64 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
65 Debug::fmt(&self.0, f)
66 }
67}
68
69impl AsRef<chrono::DateTime<FixedOffset>> for DateTime {
70 fn as_ref(&self) -> &chrono::DateTime<FixedOffset> {
71 &self.0
72 }
73}
74
75#[cfg(feature = "bounded-static")]
76impl IntoBoundedStatic for DateTime {
77 type Static = Self;
78
79 fn into_static(self) -> Self::Static {
80 self
81 }
82}
83
84#[cfg(feature = "bounded-static")]
85impl ToBoundedStatic for DateTime {
86 type Static = Self;
87
88 fn to_static(&self) -> Self::Static {
89 self.clone()
90 }
91}
92
93#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
94#[derive(Clone, Eq, PartialEq, Hash)]
95pub struct NaiveDate(chrono::NaiveDate);
96
97impl NaiveDate {
98 pub fn validate(value: &chrono::NaiveDate) -> Result<(), NaiveDateError> {
99 if !(0..=9999).contains(&value.year()) {
101 return Err(NaiveDateError::YearOutOfRange { got: value.year() });
102 }
103
104 Ok(())
105 }
106
107 #[cfg(feature = "unvalidated")]
115 #[cfg_attr(docsrs, doc(cfg(feature = "unvalidated")))]
116 pub fn unvalidated(value: chrono::NaiveDate) -> Self {
117 Self(value)
118 }
119}
120
121impl TryFrom<chrono::NaiveDate> for NaiveDate {
122 type Error = NaiveDateError;
123
124 fn try_from(value: chrono::NaiveDate) -> Result<Self, Self::Error> {
125 Self::validate(&value)?;
126
127 Ok(Self(value))
128 }
129}
130
131impl Debug for NaiveDate {
132 fn fmt(&self, f: &mut Formatter) -> std::fmt::Result {
133 Debug::fmt(&self.0, f)
134 }
135}
136
137impl AsRef<chrono::NaiveDate> for NaiveDate {
138 fn as_ref(&self) -> &chrono::NaiveDate {
139 &self.0
140 }
141}
142
143#[cfg(feature = "bounded-static")]
144impl IntoBoundedStatic for NaiveDate {
145 type Static = Self;
146
147 fn into_static(self) -> Self::Static {
148 self
149 }
150}
151
152#[cfg(feature = "bounded-static")]
153impl ToBoundedStatic for NaiveDate {
154 type Static = Self;
155
156 fn to_static(&self) -> Self::Static {
157 self.clone()
158 }
159}
160
161pub mod error {
163 use thiserror::Error;
164
165 #[derive(Clone, Debug, Eq, Error, Hash, Ord, PartialEq, PartialOrd)]
166 pub enum DateTimeError {
167 #[error("expected `0 <= year <= 9999`, got {got}")]
168 YearOutOfRange { got: i32 },
169 #[error("expected `nanos == 0`, got {got}")]
170 UnalignedNanoSeconds { got: u32 },
171 #[error("expected `offset % 60 == 0`, got {got}")]
172 UnalignedOffset { got: i32 },
173 }
174
175 #[derive(Clone, Debug, Eq, Error, Hash, Ord, PartialEq, PartialOrd)]
176 pub enum NaiveDateError {
177 #[error("expected `0 <= year <= 9999`, got {got}")]
178 YearOutOfRange { got: i32 },
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use chrono::{TimeZone, Timelike};
185
186 use super::*;
187
188 #[test]
189 fn test_conversion_date_time_failing() {
190 let tests = [
191 (
192 DateTime::try_from(
193 chrono::FixedOffset::east_opt(3600)
194 .unwrap()
195 .from_local_datetime(&chrono::NaiveDateTime::new(
196 chrono::NaiveDate::from_ymd_opt(-1, 2, 1).unwrap(),
197 chrono::NaiveTime::from_hms_opt(12, 34, 56).unwrap(),
198 ))
199 .unwrap(),
200 ),
201 DateTimeError::YearOutOfRange { got: -1 },
202 ),
203 (
204 DateTime::try_from(
205 chrono::FixedOffset::east_opt(3600)
206 .unwrap()
207 .from_local_datetime(&chrono::NaiveDateTime::new(
208 chrono::NaiveDate::from_ymd_opt(10000, 2, 1).unwrap(),
209 chrono::NaiveTime::from_hms_opt(12, 34, 56).unwrap(),
210 ))
211 .unwrap(),
212 ),
213 DateTimeError::YearOutOfRange { got: 10000 },
214 ),
215 (
216 DateTime::try_from(
217 chrono::FixedOffset::east_opt(1)
218 .unwrap()
219 .from_local_datetime(&chrono::NaiveDateTime::new(
220 chrono::NaiveDate::from_ymd_opt(0, 2, 1).unwrap(),
221 chrono::NaiveTime::from_hms_opt(12, 34, 56).unwrap(),
222 ))
223 .unwrap(),
224 ),
225 DateTimeError::UnalignedOffset { got: 1 },
226 ),
227 (
228 DateTime::try_from(
229 chrono::FixedOffset::east_opt(59)
230 .unwrap()
231 .from_local_datetime(&chrono::NaiveDateTime::new(
232 chrono::NaiveDate::from_ymd_opt(9999, 2, 1).unwrap(),
233 chrono::NaiveTime::from_hms_opt(12, 34, 56).unwrap(),
234 ))
235 .unwrap(),
236 ),
237 DateTimeError::UnalignedOffset { got: 59 },
238 ),
239 (
240 DateTime::try_from(
241 chrono::FixedOffset::east_opt(60)
242 .unwrap()
243 .from_local_datetime(&chrono::NaiveDateTime::new(
244 chrono::NaiveDate::from_ymd_opt(0, 2, 1).unwrap(),
245 chrono::NaiveTime::from_hms_opt(12, 34, 56).unwrap(),
246 ))
247 .unwrap()
248 .with_nanosecond(1)
249 .unwrap(),
250 ),
251 DateTimeError::UnalignedNanoSeconds { got: 1 },
252 ),
253 ];
254
255 for (got, expected) in tests {
256 println!("{}", got.clone().unwrap_err());
257 println!("{:?}", got.clone().unwrap_err());
258 assert_eq!(expected, got.unwrap_err());
259 }
260 }
261}