1use std::str::FromStr;
2
3use chrono::{
4 DateTime, FixedOffset, MappedLocalTime, NaiveDate, NaiveDateTime, Offset, TimeZone, Utc,
5};
6
7use crate::utils::StringValueData;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub struct XsDate {
15 pub date: NaiveDate,
17 pub tz: Option<FixedOffset>,
19}
20
21impl XsDate {
22 pub fn new(date: NaiveDate) -> Self {
24 XsDate { date, tz: None }
25 }
26
27 pub fn from_date(year: i32, month: u32, day: u32) -> Option<Self> {
29 NaiveDate::from_ymd_opt(year, month, day).map(XsDate::new)
30 }
31
32 pub fn new_with_tz<O: Offset>(date: NaiveDate, tz: O) -> Self {
34 XsDate {
35 date,
36 tz: Some(tz.fix()),
37 }
38 }
39}
40
41impl From<NaiveDate> for XsDate {
42 fn from(value: NaiveDate) -> Self {
43 XsDate::new(value)
44 }
45}
46
47impl FromStr for XsDate {
48 type Err = chrono::ParseError;
49
50 fn from_str(s: &str) -> Result<Self, Self::Err> {
51 let sep_count = s.chars().filter(|&c| c == '-' || c == '+').count();
53 if sep_count > 2
54 && let Some(pos) = s.rfind(['+', '-'])
55 {
56 let (date_str, tz_str) = s.split_at(pos);
58 let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d")?;
59 let tz = tz_str.parse::<FixedOffset>()?;
60 Ok(XsDate { date, tz: Some(tz) })
61 } else if s.ends_with('Z') || s.ends_with('z') {
62 let date_str = &s[..s.len() - 1];
64 let date = NaiveDate::parse_from_str(date_str, "%Y-%m-%d")?;
65
66 Ok(XsDate {
67 date,
68 tz: Some(Utc.fix()),
69 })
70 } else {
71 let date = NaiveDate::parse_from_str(s, "%Y-%m-%d")?;
73 Ok(XsDate { date, tz: None })
74 }
75 }
76}
77
78impl StringValueData for XsDate {
79 type Error = chrono::ParseError;
80
81 fn parse_from_str(s: &str) -> Result<Self, Self::Error>
82 where
83 Self: Sized,
84 {
85 s.parse()
86 }
87
88 fn to_raw_value(&self) -> Box<str> {
89 match self.tz {
90 Some(tz) => format!("{}{}", self.date.format("%Y-%m-%d"), tz).into(),
91 None => self.date.format("%Y-%m-%d").to_string().into(),
92 }
93 }
94}
95
96#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
101pub struct XsDateTime {
102 pub naive_date_time: NaiveDateTime,
107 pub tz: Option<FixedOffset>,
110}
111
112impl XsDateTime {
113 pub fn new<T: TimeZone>(date_time: DateTime<T>) -> Self {
115 XsDateTime {
116 naive_date_time: date_time.naive_utc(),
117 tz: Some(date_time.offset().fix()),
118 }
119 }
120
121 pub fn new_without_tz(naive_date_time: NaiveDateTime) -> Self {
123 XsDateTime {
124 naive_date_time,
125 tz: None,
126 }
127 }
128
129 pub fn datetime_utc(&self) -> DateTime<Utc> {
133 match self.tz {
134 Some(tz) => {
135 DateTime::<FixedOffset>::from_naive_utc_and_offset(self.naive_date_time, tz)
136 .to_utc()
137 }
138 None => DateTime::<Utc>::from_naive_utc_and_offset(self.naive_date_time, Utc),
139 }
140 }
141
142 pub fn datetime_tz<Tz: TimeZone>(&self, tz: &Tz) -> MappedLocalTime<DateTime<Tz>> {
147 match self.tz {
148 Some(original_tz) => MappedLocalTime::Single(
149 DateTime::<FixedOffset>::from_naive_utc_and_offset(
150 self.naive_date_time,
151 original_tz,
152 )
153 .with_timezone(tz),
154 ),
155 None => tz.from_local_datetime(&self.naive_date_time),
156 }
157 }
158}
159
160impl<T: TimeZone> From<DateTime<T>> for XsDateTime {
161 fn from(value: DateTime<T>) -> Self {
162 XsDateTime::new(value)
163 }
164}
165
166impl FromStr for XsDateTime {
167 type Err = chrono::ParseError;
168
169 fn from_str(s: &str) -> Result<Self, Self::Err> {
170 if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
172 Ok(XsDateTime {
173 naive_date_time: dt.naive_utc(),
174 tz: Some(dt.offset().to_owned()),
175 })
176 } else {
177 let naive_dt = chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.f")?;
179 Ok(XsDateTime {
180 naive_date_time: naive_dt,
181 tz: None,
182 })
183 }
184 }
185}
186
187impl StringValueData for XsDateTime {
188 type Error = chrono::ParseError;
189
190 fn parse_from_str(s: &str) -> Result<Self, Self::Error>
191 where
192 Self: Sized,
193 {
194 s.parse()
195 }
196
197 fn to_raw_value(&self) -> Box<str> {
198 match self.tz {
199 Some(tz) => {
200 let dt_with_tz =
201 DateTime::<FixedOffset>::from_naive_utc_and_offset(self.naive_date_time, tz);
202 dt_with_tz.to_rfc3339().into()
203 }
204 None => self
205 .naive_date_time
206 .format("%Y-%m-%dT%H:%M:%S%.f")
207 .to_string()
208 .into(),
209 }
210 }
211}
212
213#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
215pub enum XsDateOrDateTime {
216 Date(XsDate),
218 DateTime(XsDateTime),
220}
221
222impl XsDateOrDateTime {
223 pub fn date<Tz: TimeZone>(&self, tz: &Tz) -> MappedLocalTime<NaiveDate> {
228 match self {
229 XsDateOrDateTime::Date(d) => MappedLocalTime::Single(d.date),
231 XsDateOrDateTime::DateTime(dt) => {
233 let dt = dt.datetime_tz(tz);
234 match dt {
235 MappedLocalTime::Single(dt) => MappedLocalTime::Single(dt.date_naive()),
236 MappedLocalTime::None => MappedLocalTime::None,
237 MappedLocalTime::Ambiguous(first, second) => {
238 if first.date_naive() == second.date_naive() {
241 MappedLocalTime::Single(first.date_naive())
242 } else {
243 MappedLocalTime::Ambiguous(first.date_naive(), second.date_naive())
244 }
245 }
246 }
247 }
248 }
249 }
250}
251
252impl From<XsDate> for XsDateOrDateTime {
253 fn from(value: XsDate) -> Self {
254 XsDateOrDateTime::Date(value)
255 }
256}
257
258impl From<XsDateTime> for XsDateOrDateTime {
259 fn from(value: XsDateTime) -> Self {
260 XsDateOrDateTime::DateTime(value)
261 }
262}
263
264impl From<NaiveDate> for XsDateOrDateTime {
265 fn from(value: NaiveDate) -> Self {
266 XsDateOrDateTime::Date(XsDate::new(value))
267 }
268}
269
270impl<T> From<DateTime<T>> for XsDateOrDateTime
271where
272 T: TimeZone,
273{
274 fn from(value: DateTime<T>) -> Self {
275 XsDateOrDateTime::DateTime(XsDateTime::new(value))
276 }
277}
278
279impl FromStr for XsDateOrDateTime {
280 type Err = chrono::ParseError;
281
282 fn from_str(s: &str) -> Result<Self, Self::Err> {
283 if s.contains('T') {
284 let date_time = s.parse::<XsDateTime>()?;
285 Ok(XsDateOrDateTime::DateTime(date_time))
286 } else {
287 let date = s.parse::<XsDate>()?;
288 Ok(XsDateOrDateTime::Date(date))
289 }
290 }
291}
292
293impl StringValueData for XsDateOrDateTime {
294 type Error = chrono::ParseError;
295
296 fn parse_from_str(s: &str) -> Result<Self, Self::Error>
297 where
298 Self: Sized,
299 {
300 s.parse()
301 }
302
303 fn to_raw_value(&self) -> Box<str> {
304 match self {
305 XsDateOrDateTime::Date(d) => d.to_raw_value(),
306 XsDateOrDateTime::DateTime(dt) => dt.to_raw_value(),
307 }
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use chrono::{Datelike as _, Timelike as _};
314
315 use super::*;
316
317 #[test]
318 fn test_xs_date_parse() {
319 let d1: XsDate = "2025-10-05".parse().unwrap();
320 assert_eq!(d1.date, NaiveDate::from_ymd_opt(2025, 10, 5).unwrap());
321 assert!(d1.tz.is_none());
322
323 let d2: XsDate = "2025-10-05+02:00".parse().unwrap();
324 assert_eq!(d2.date, NaiveDate::from_ymd_opt(2025, 10, 5).unwrap());
325 assert_eq!(d2.tz.unwrap(), FixedOffset::east_opt(2 * 3600).unwrap());
326
327 let d3: XsDate = "2025-10-05Z".parse().unwrap();
328 assert_eq!(d3.date, NaiveDate::from_ymd_opt(2025, 10, 5).unwrap());
329 assert_eq!(d3.tz.unwrap(), Utc.fix());
330
331 let d4: XsDate = "2025-10-05-05:00".parse().unwrap();
332 assert_eq!(d4.date, NaiveDate::from_ymd_opt(2025, 10, 5).unwrap());
333 assert_eq!(d4.tz.unwrap(), FixedOffset::west_opt(5 * 3600).unwrap());
334 }
335
336 #[test]
337 fn test_xs_date_time_parse() {
338 let dt1: XsDateTime = "2025-10-05T14:30:00".parse().unwrap();
339 assert_eq!(
340 dt1.naive_date_time,
341 NaiveDate::from_ymd_opt(2025, 10, 5)
342 .unwrap()
343 .and_hms_opt(14, 30, 0)
344 .unwrap()
345 );
346 assert!(dt1.tz.is_none());
347
348 let dt2: XsDateTime = "2025-10-05T14:30:00+02:00".parse().unwrap();
349 assert_eq!(
350 dt2.naive_date_time,
351 NaiveDate::from_ymd_opt(2025, 10, 5)
352 .unwrap()
353 .and_hms_opt(12, 30, 0)
354 .unwrap()
355 );
356 assert_eq!(dt2.tz.unwrap(), FixedOffset::east_opt(2 * 3600).unwrap());
357
358 let dt3: XsDateTime = "2025-10-05T14:30:00Z".parse().unwrap();
359 assert_eq!(
360 dt3.naive_date_time,
361 NaiveDate::from_ymd_opt(2025, 10, 5)
362 .unwrap()
363 .and_hms_opt(14, 30, 0)
364 .unwrap()
365 );
366 assert_eq!(dt3.tz.unwrap(), Utc.fix());
367
368 let dt4: XsDateTime = "2025-10-05T14:30:00.123456".parse().unwrap();
369 assert_eq!(
370 dt4.naive_date_time,
371 NaiveDate::from_ymd_opt(2025, 10, 5)
372 .unwrap()
373 .and_hms_micro_opt(14, 30, 0, 123456)
374 .unwrap()
375 );
376 assert!(dt4.tz.is_none());
377
378 let dt5: XsDateTime = "2025-10-05T14:30:00.123456-02:00".parse().unwrap();
379 assert_eq!(
380 dt5.naive_date_time,
381 NaiveDate::from_ymd_opt(2025, 10, 5)
382 .unwrap()
383 .and_hms_micro_opt(16, 30, 0, 123456)
384 .unwrap()
385 );
386 assert_eq!(dt5.tz.unwrap(), FixedOffset::west_opt(2 * 3600).unwrap());
387 }
388
389 #[test]
390 fn test_xs_date_time_to_datetime_utc() {
391 let dt1: XsDateTime = "2025-10-05T14:30:00+02:00".parse().unwrap();
392 let utc_dt1 = dt1.datetime_utc();
393 assert_eq!(utc_dt1.year(), 2025);
394 assert_eq!(utc_dt1.month(), 10);
395 assert_eq!(utc_dt1.day(), 5);
396 assert_eq!(utc_dt1.hour(), 12);
397 assert_eq!(utc_dt1.minute(), 30);
398
399 let dt2: XsDateTime = "2025-10-05T14:30:00".parse().unwrap();
400 let utc_dt2 = dt2.datetime_utc();
401 assert_eq!(utc_dt2.year(), 2025);
402 assert_eq!(utc_dt2.month(), 10);
403 assert_eq!(utc_dt2.day(), 5);
404 assert_eq!(utc_dt2.hour(), 14);
405 assert_eq!(utc_dt2.minute(), 30);
406 }
407
408 #[test]
409 fn test_xs_date_time_to_datetime_tz() {
410 let dt1: XsDateTime = "2025-10-05T14:30:00+02:00".parse().unwrap();
411 let tz = FixedOffset::east_opt(3600).unwrap();
412 let dt1_in_tz = dt1.datetime_tz(&tz).single().unwrap();
413 assert_eq!(dt1_in_tz.year(), 2025);
414 assert_eq!(dt1_in_tz.month(), 10);
415 assert_eq!(dt1_in_tz.day(), 5);
416 assert_eq!(dt1_in_tz.hour(), 13);
417 assert_eq!(dt1_in_tz.minute(), 30);
418
419 let dt2: XsDateTime = "2025-10-05T14:30:00".parse().unwrap();
420 let dt2_in_tz = dt2.datetime_tz(&tz).single().unwrap();
421 assert_eq!(dt2_in_tz.year(), 2025);
422 assert_eq!(dt2_in_tz.month(), 10);
423 assert_eq!(dt2_in_tz.day(), 5);
424 assert_eq!(dt2_in_tz.hour(), 14);
425 assert_eq!(dt2_in_tz.minute(), 30);
426 }
427
428 #[test]
429 fn test_xs_date_or_date_time_parse() {
430 let d: XsDateOrDateTime = "2025-10-05".parse().unwrap();
431 match d {
432 XsDateOrDateTime::Date(date) => {
433 assert_eq!(date.date, NaiveDate::from_ymd_opt(2025, 10, 5).unwrap());
434 assert!(date.tz.is_none());
435 }
436 _ => panic!("Expected XsDate variant"),
437 }
438
439 let dt: XsDateOrDateTime = "2025-10-05T14:30:00+02:00".parse().unwrap();
440 match dt {
441 XsDateOrDateTime::DateTime(date_time) => {
442 assert_eq!(
443 date_time.naive_date_time,
444 NaiveDate::from_ymd_opt(2025, 10, 5)
445 .unwrap()
446 .and_hms_opt(12, 30, 0)
447 .unwrap()
448 );
449 assert_eq!(
450 date_time.tz.unwrap(),
451 FixedOffset::east_opt(2 * 3600).unwrap()
452 );
453 }
454 _ => panic!("Expected XsDateTime variant"),
455 }
456 }
457}