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) -> String {
89 match self.tz {
90 Some(tz) => format!("{}{}", self.date.format("%Y-%m-%d"), tz),
91 None => self.date.format("%Y-%m-%d").to_string(),
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) -> String {
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()
203 }
204 None => self
205 .naive_date_time
206 .format("%Y-%m-%dT%H:%M:%S%.f")
207 .to_string(),
208 }
209 }
210}
211
212#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
214pub enum XsDateOrDateTime {
215 Date(XsDate),
217 DateTime(XsDateTime),
219}
220
221impl XsDateOrDateTime {
222 pub fn date<Tz: TimeZone>(&self, tz: &Tz) -> MappedLocalTime<NaiveDate> {
227 match self {
228 XsDateOrDateTime::Date(d) => MappedLocalTime::Single(d.date),
230 XsDateOrDateTime::DateTime(dt) => {
232 let dt = dt.datetime_tz(tz);
233 match dt {
234 MappedLocalTime::Single(dt) => MappedLocalTime::Single(dt.date_naive()),
235 MappedLocalTime::None => MappedLocalTime::None,
236 MappedLocalTime::Ambiguous(first, second) => {
237 if first.date_naive() == second.date_naive() {
240 MappedLocalTime::Single(first.date_naive())
241 } else {
242 MappedLocalTime::Ambiguous(first.date_naive(), second.date_naive())
243 }
244 }
245 }
246 }
247 }
248 }
249}
250
251impl From<XsDate> for XsDateOrDateTime {
252 fn from(value: XsDate) -> Self {
253 XsDateOrDateTime::Date(value)
254 }
255}
256
257impl From<XsDateTime> for XsDateOrDateTime {
258 fn from(value: XsDateTime) -> Self {
259 XsDateOrDateTime::DateTime(value)
260 }
261}
262
263impl From<NaiveDate> for XsDateOrDateTime {
264 fn from(value: NaiveDate) -> Self {
265 XsDateOrDateTime::Date(XsDate::new(value))
266 }
267}
268
269impl<T> From<DateTime<T>> for XsDateOrDateTime
270where
271 T: TimeZone,
272{
273 fn from(value: DateTime<T>) -> Self {
274 XsDateOrDateTime::DateTime(XsDateTime::new(value))
275 }
276}
277
278impl FromStr for XsDateOrDateTime {
279 type Err = chrono::ParseError;
280
281 fn from_str(s: &str) -> Result<Self, Self::Err> {
282 if s.contains('T') {
283 let date_time = s.parse::<XsDateTime>()?;
284 Ok(XsDateOrDateTime::DateTime(date_time))
285 } else {
286 let date = s.parse::<XsDate>()?;
287 Ok(XsDateOrDateTime::Date(date))
288 }
289 }
290}
291
292impl StringValueData for XsDateOrDateTime {
293 type Error = chrono::ParseError;
294
295 fn parse_from_str(s: &str) -> Result<Self, Self::Error>
296 where
297 Self: Sized,
298 {
299 s.parse()
300 }
301
302 fn to_raw_value(&self) -> String {
303 match self {
304 XsDateOrDateTime::Date(d) => d.to_raw_value(),
305 XsDateOrDateTime::DateTime(dt) => dt.to_raw_value(),
306 }
307 }
308}
309
310#[cfg(test)]
311mod tests {
312 use chrono::{Datelike as _, Timelike as _};
313
314 use super::*;
315
316 #[test]
317 fn test_xs_date_parse() {
318 let d1: XsDate = "2025-10-05".parse().unwrap();
319 assert_eq!(d1.date, NaiveDate::from_ymd_opt(2025, 10, 5).unwrap());
320 assert!(d1.tz.is_none());
321
322 let d2: XsDate = "2025-10-05+02:00".parse().unwrap();
323 assert_eq!(d2.date, NaiveDate::from_ymd_opt(2025, 10, 5).unwrap());
324 assert_eq!(d2.tz.unwrap(), FixedOffset::east_opt(2 * 3600).unwrap());
325
326 let d3: XsDate = "2025-10-05Z".parse().unwrap();
327 assert_eq!(d3.date, NaiveDate::from_ymd_opt(2025, 10, 5).unwrap());
328 assert_eq!(d3.tz.unwrap(), Utc.fix());
329
330 let d4: XsDate = "2025-10-05-05:00".parse().unwrap();
331 assert_eq!(d4.date, NaiveDate::from_ymd_opt(2025, 10, 5).unwrap());
332 assert_eq!(d4.tz.unwrap(), FixedOffset::west_opt(5 * 3600).unwrap());
333 }
334
335 #[test]
336 fn test_xs_date_time_parse() {
337 let dt1: XsDateTime = "2025-10-05T14:30:00".parse().unwrap();
338 assert_eq!(
339 dt1.naive_date_time,
340 NaiveDate::from_ymd_opt(2025, 10, 5)
341 .unwrap()
342 .and_hms_opt(14, 30, 0)
343 .unwrap()
344 );
345 assert!(dt1.tz.is_none());
346
347 let dt2: XsDateTime = "2025-10-05T14:30:00+02:00".parse().unwrap();
348 assert_eq!(
349 dt2.naive_date_time,
350 NaiveDate::from_ymd_opt(2025, 10, 5)
351 .unwrap()
352 .and_hms_opt(12, 30, 0)
353 .unwrap()
354 );
355 assert_eq!(dt2.tz.unwrap(), FixedOffset::east_opt(2 * 3600).unwrap());
356
357 let dt3: XsDateTime = "2025-10-05T14:30:00Z".parse().unwrap();
358 assert_eq!(
359 dt3.naive_date_time,
360 NaiveDate::from_ymd_opt(2025, 10, 5)
361 .unwrap()
362 .and_hms_opt(14, 30, 0)
363 .unwrap()
364 );
365 assert_eq!(dt3.tz.unwrap(), Utc.fix());
366
367 let dt4: XsDateTime = "2025-10-05T14:30:00.123456".parse().unwrap();
368 assert_eq!(
369 dt4.naive_date_time,
370 NaiveDate::from_ymd_opt(2025, 10, 5)
371 .unwrap()
372 .and_hms_micro_opt(14, 30, 0, 123456)
373 .unwrap()
374 );
375 assert!(dt4.tz.is_none());
376
377 let dt5: XsDateTime = "2025-10-05T14:30:00.123456-02:00".parse().unwrap();
378 assert_eq!(
379 dt5.naive_date_time,
380 NaiveDate::from_ymd_opt(2025, 10, 5)
381 .unwrap()
382 .and_hms_micro_opt(16, 30, 0, 123456)
383 .unwrap()
384 );
385 assert_eq!(dt5.tz.unwrap(), FixedOffset::west_opt(2 * 3600).unwrap());
386 }
387
388 #[test]
389 fn test_xs_date_time_to_datetime_utc() {
390 let dt1: XsDateTime = "2025-10-05T14:30:00+02:00".parse().unwrap();
391 let utc_dt1 = dt1.datetime_utc();
392 assert_eq!(utc_dt1.year(), 2025);
393 assert_eq!(utc_dt1.month(), 10);
394 assert_eq!(utc_dt1.day(), 5);
395 assert_eq!(utc_dt1.hour(), 12);
396 assert_eq!(utc_dt1.minute(), 30);
397
398 let dt2: XsDateTime = "2025-10-05T14:30:00".parse().unwrap();
399 let utc_dt2 = dt2.datetime_utc();
400 assert_eq!(utc_dt2.year(), 2025);
401 assert_eq!(utc_dt2.month(), 10);
402 assert_eq!(utc_dt2.day(), 5);
403 assert_eq!(utc_dt2.hour(), 14);
404 assert_eq!(utc_dt2.minute(), 30);
405 }
406
407 #[test]
408 fn test_xs_date_time_to_datetime_tz() {
409 let dt1: XsDateTime = "2025-10-05T14:30:00+02:00".parse().unwrap();
410 let tz = FixedOffset::east_opt(3600).unwrap();
411 let dt1_in_tz = dt1.datetime_tz(&tz).single().unwrap();
412 assert_eq!(dt1_in_tz.year(), 2025);
413 assert_eq!(dt1_in_tz.month(), 10);
414 assert_eq!(dt1_in_tz.day(), 5);
415 assert_eq!(dt1_in_tz.hour(), 13);
416 assert_eq!(dt1_in_tz.minute(), 30);
417
418 let dt2: XsDateTime = "2025-10-05T14:30:00".parse().unwrap();
419 let dt2_in_tz = dt2.datetime_tz(&tz).single().unwrap();
420 assert_eq!(dt2_in_tz.year(), 2025);
421 assert_eq!(dt2_in_tz.month(), 10);
422 assert_eq!(dt2_in_tz.day(), 5);
423 assert_eq!(dt2_in_tz.hour(), 14);
424 assert_eq!(dt2_in_tz.minute(), 30);
425 }
426
427 #[test]
428 fn test_xs_date_or_date_time_parse() {
429 let d: XsDateOrDateTime = "2025-10-05".parse().unwrap();
430 match d {
431 XsDateOrDateTime::Date(date) => {
432 assert_eq!(date.date, NaiveDate::from_ymd_opt(2025, 10, 5).unwrap());
433 assert!(date.tz.is_none());
434 }
435 _ => panic!("Expected XsDate variant"),
436 }
437
438 let dt: XsDateOrDateTime = "2025-10-05T14:30:00+02:00".parse().unwrap();
439 match dt {
440 XsDateOrDateTime::DateTime(date_time) => {
441 assert_eq!(
442 date_time.naive_date_time,
443 NaiveDate::from_ymd_opt(2025, 10, 5)
444 .unwrap()
445 .and_hms_opt(12, 30, 0)
446 .unwrap()
447 );
448 assert_eq!(
449 date_time.tz.unwrap(),
450 FixedOffset::east_opt(2 * 3600).unwrap()
451 );
452 }
453 _ => panic!("Expected XsDateTime variant"),
454 }
455 }
456}