1use std::str::FromStr;
2
3use chrono::{DateTime, Duration, NaiveDate, NaiveDateTime, TimeZone as _, Utc};
4
5use crate::{Property, ValueType};
6
7const NAIVE_DATE_TIME_FORMAT: &str = "%Y%m%dT%H%M%S";
8const UTC_DATE_TIME_FORMAT: &str = "%Y%m%dT%H%M%SZ";
9const NAIVE_DATE_FORMAT: &str = "%Y%m%d";
10
11pub(crate) fn parse_utc_date_time(s: &str) -> Option<DateTime<Utc>> {
13 Utc.datetime_from_str(s, UTC_DATE_TIME_FORMAT).ok()
14}
15
16pub(crate) fn parse_naive_date_time(s: &str) -> Option<NaiveDateTime> {
17 NaiveDateTime::parse_from_str(s, NAIVE_DATE_TIME_FORMAT).ok()
18}
19
20pub(crate) fn format_utc_date_time(utc_dt: DateTime<Utc>) -> String {
21 utc_dt.format(UTC_DATE_TIME_FORMAT).to_string()
22}
23
24pub(crate) fn parse_duration(s: &str) -> Option<Duration> {
25 iso8601::duration(s)
26 .ok()
27 .and_then(|iso| Duration::from_std(iso.into()).ok())
28}
29
30pub(crate) fn naive_date_to_property(date: NaiveDate, key: &str) -> Property {
31 Property::new(key, date.format(NAIVE_DATE_FORMAT).to_string())
32 .append_parameter(ValueType::Date)
33 .done()
34}
35
36#[derive(Clone, Debug, Eq, PartialEq)]
46pub enum CalendarDateTime {
47 Floating(NaiveDateTime),
54
55 Utc(DateTime<Utc>),
60
61 WithTimezone {
63 date_time: NaiveDateTime,
65 tzid: String,
67 },
68}
69
70impl CalendarDateTime {
71 #[cfg(test)]
73 pub(crate) fn now() -> Self {
74 NaiveDate::from_ymd_opt(2015, 10, 26)
75 .unwrap()
76 .and_hms_opt(1, 22, 00)
77 .unwrap()
78 .into()
79 }
80
81 pub(crate) fn from_property(property: &Property) -> Option<Self> {
82 let value = property.value();
83 if let Some(tzid) = property.params().get("TZID") {
84 Some(Self::WithTimezone {
85 date_time: NaiveDateTime::parse_from_str(value, NAIVE_DATE_TIME_FORMAT).ok()?,
86 tzid: tzid.value().to_owned(),
87 })
88 } else if let Ok(naive_date_time) =
89 NaiveDateTime::parse_from_str(value, NAIVE_DATE_TIME_FORMAT)
90 {
91 Some(naive_date_time.into())
92 } else {
93 Self::from_str(value).ok()
94 }
95 }
96
97 pub(crate) fn to_property(&self, key: &str) -> Property {
98 match self {
99 CalendarDateTime::Floating(naive_dt) => {
100 Property::new(key, naive_dt.format(NAIVE_DATE_TIME_FORMAT).to_string())
101 }
102 CalendarDateTime::Utc(utc_dt) => Property::new(key, format_utc_date_time(*utc_dt)),
103 CalendarDateTime::WithTimezone { date_time, tzid } => {
104 Property::new(key, date_time.format(NAIVE_DATE_TIME_FORMAT).to_string())
105 .add_parameter("TZID", tzid)
106 .done()
107 }
108 }
109 }
110
111 pub(crate) fn from_utc_string(s: &str) -> Option<Self> {
112 parse_utc_date_time(s).map(CalendarDateTime::Utc)
113 }
114
115 pub(crate) fn from_naive_string(s: &str) -> Option<Self> {
116 parse_naive_date_time(s).map(CalendarDateTime::Floating)
117 }
118
119 #[cfg(feature = "chrono-tz")]
121 pub fn try_into_utc(&self) -> Option<DateTime<Utc>> {
122 match self {
123 CalendarDateTime::Floating(_) => None, CalendarDateTime::Utc(inner) => Some(*inner),
125 CalendarDateTime::WithTimezone { date_time, tzid } => tzid
126 .parse::<chrono_tz::Tz>()
127 .ok()
128 .and_then(|tz| tz.from_local_datetime(date_time).single())
129 .map(|tz| tz.with_timezone(&Utc)),
130 }
131 }
132
133 #[cfg(feature = "chrono-tz")]
135 #[allow(dead_code)]
136 pub(crate) fn with_timezone(dt: NaiveDateTime, tz_id: chrono_tz::Tz) -> Self {
137 Self::WithTimezone {
138 date_time: dt,
139 tzid: tz_id.name().to_owned(),
140 }
141 }
142
143 #[cfg(feature = "chrono-tz")]
145 pub fn from_ymd_hm_tzid(
146 year: i32,
147 month: u32,
148 day: u32,
149 hour: u32,
150 min: u32,
151 tz_id: chrono_tz::Tz,
152 ) -> Option<Self> {
153 NaiveDate::from_ymd_opt(year, month, day)
154 .and_then(|date| date.and_hms_opt(hour, min, 0))
155 .zip(Some(tz_id))
156 .map(|(dt, tz)| Self::with_timezone(dt, tz))
157 }
158
159 #[cfg(feature = "chrono-tz")]
161 pub fn from_date_time<TZ: chrono::TimeZone<Offset = O>, O: chrono_tz::OffsetName>(
162 dt: DateTime<TZ>,
163 ) -> Self {
164 Self::WithTimezone {
165 date_time: dt.naive_local(),
166 tzid: dt.offset().tz_id().to_owned(),
167 }
168 }
169}
170
171#[cfg(feature = "chrono-tz")]
173pub fn ymd_hm_tzid(
174 year: i32,
175 month: u32,
176 day: u32,
177 hour: u32,
178 min: u32,
179 tz_id: chrono_tz::Tz,
180) -> Option<CalendarDateTime> {
181 CalendarDateTime::from_ymd_hm_tzid(year, month, day, hour, min, tz_id)
182}
183
184impl From<DateTime<Utc>> for CalendarDateTime {
186 fn from(dt: DateTime<Utc>) -> Self {
187 Self::Utc(dt)
188 }
189}
190
191impl From<NaiveDateTime> for CalendarDateTime {
204 fn from(dt: NaiveDateTime) -> Self {
205 Self::Floating(dt)
206 }
207}
208
209#[cfg(feature = "chrono-tz")]
210impl From<(NaiveDateTime, chrono_tz::Tz)> for CalendarDateTime {
211 fn from((date_time, tzid): (NaiveDateTime, chrono_tz::Tz)) -> Self {
212 Self::WithTimezone {
213 date_time,
214 tzid: tzid.name().into(),
215 }
216 }
217}
218
219#[cfg(feature = "chrono-tz")]
220impl TryFrom<(NaiveDateTime, &str)> for CalendarDateTime {
221 type Error = String;
222
223 fn try_from((dt, maybe_tz): (NaiveDateTime, &str)) -> Result<Self, Self::Error> {
224 let tzid: chrono_tz::Tz = maybe_tz
225 .parse()
226 .map_err(|e: chrono_tz::ParseError| e.to_string())?;
227 Ok(CalendarDateTime::from((dt, tzid)))
228 }
229}
230
231impl FromStr for CalendarDateTime {
232 type Err = ();
233
234 fn from_str(s: &str) -> Result<Self, Self::Err> {
235 CalendarDateTime::from_utc_string(s)
236 .or_else(|| CalendarDateTime::from_naive_string(s))
237 .ok_or(())
238 }
239}
240
241#[derive(Clone, Debug, Eq, PartialEq)]
243pub enum DatePerhapsTime {
244 DateTime(CalendarDateTime),
246 Date(NaiveDate),
248}
249
250impl DatePerhapsTime {
251 pub fn from_property(property: &Property) -> Option<Self> {
253 if property.value_type() == Some(ValueType::Date) {
254 Some(
255 NaiveDate::parse_from_str(property.value(), NAIVE_DATE_FORMAT)
256 .ok()?
257 .into(),
258 )
259 } else {
260 Some(CalendarDateTime::from_property(property)?.into())
261 }
262 }
263
264 pub fn to_property(&self, key: &str) -> Property {
266 match self {
267 Self::DateTime(date_time) => date_time.to_property(key),
268 Self::Date(date) => naive_date_to_property(*date, key),
269 }
270 }
271
272 pub fn date_naive(&self) -> NaiveDate {
274 use crate::DatePerhapsTime::*;
275 match self {
276 Date(date) => date.to_owned(),
277 DateTime(CalendarDateTime::Floating(date_time)) => date_time.date(),
278 DateTime(CalendarDateTime::Utc(date_time)) => date_time.date_naive(),
279 DateTime(CalendarDateTime::WithTimezone { date_time, tzid: _ }) => date_time.date(),
280 }
281 }
282}
283
284#[cfg(feature = "chrono-tz")]
286#[allow(dead_code)]
287pub fn with_timezone<T: chrono::TimeZone + chrono_tz::OffsetName>(
288 dt: DateTime<T>,
289) -> DatePerhapsTime {
290 CalendarDateTime::WithTimezone {
291 date_time: dt.naive_local(),
292 tzid: dt.timezone().tz_id().to_owned(),
293 }
294 .into()
295}
296
297impl From<DatePerhapsTime> for NaiveDate {
298 fn from(dt: DatePerhapsTime) -> Self {
299 match dt {
300 DatePerhapsTime::Date(date) => date,
301 DatePerhapsTime::DateTime(CalendarDateTime::Floating(date_time)) => date_time.date(),
302 DatePerhapsTime::DateTime(CalendarDateTime::Utc(date_time)) => date_time.date_naive(),
303 DatePerhapsTime::DateTime(CalendarDateTime::WithTimezone { date_time, tzid: _ }) => {
304 date_time.date()
305 }
306 }
307 }
308}
309
310impl From<CalendarDateTime> for DatePerhapsTime {
311 fn from(dt: CalendarDateTime) -> Self {
312 Self::DateTime(dt)
313 }
314}
315
316impl From<DateTime<Utc>> for DatePerhapsTime {
317 fn from(dt: DateTime<Utc>) -> Self {
318 Self::DateTime(CalendarDateTime::Utc(dt))
319 }
320}
321
322#[allow(deprecated)]
333impl From<chrono::Date<Utc>> for DatePerhapsTime {
334 fn from(dt: chrono::Date<Utc>) -> Self {
335 Self::Date(dt.naive_utc())
336 }
337}
338
339impl From<NaiveDateTime> for DatePerhapsTime {
340 fn from(dt: NaiveDateTime) -> Self {
341 Self::DateTime(dt.into())
342 }
343}
344
345#[cfg(feature = "chrono-tz")]
346impl TryFrom<(NaiveDateTime, &str)> for DatePerhapsTime {
347 type Error = String;
348
349 fn try_from(value: (NaiveDateTime, &str)) -> Result<Self, Self::Error> {
350 Ok(Self::DateTime(value.try_into()?))
351 }
352}
353#[cfg(feature = "chrono-tz")]
354impl From<(NaiveDateTime, chrono_tz::Tz)> for DatePerhapsTime {
355 fn from(both: (NaiveDateTime, chrono_tz::Tz)) -> Self {
356 Self::DateTime(both.into())
357 }
358}
359
360impl From<NaiveDate> for DatePerhapsTime {
361 fn from(date: NaiveDate) -> Self {
362 Self::Date(date)
363 }
364}
365
366#[cfg(feature = "time")]
367impl From<time::Date> for DatePerhapsTime {
368 fn from(date: time::Date) -> Self {
369 let (y, o) = date.to_ordinal_date();
370 Self::Date(NaiveDate::from_yo_opt(y, o as u32).expect("bug: invalid date"))
371 }
372}
373
374#[cfg(feature = "time")]
375impl From<time::OffsetDateTime> for DatePerhapsTime {
376 fn from(datetime: time::OffsetDateTime) -> Self {
377 datetime.to_utc().into()
379 }
380}
381
382#[cfg(feature = "time")]
383impl From<time::UtcDateTime> for DatePerhapsTime {
384 fn from(datetime: time::UtcDateTime) -> Self {
385 Self::DateTime(CalendarDateTime::Utc(
386 DateTime::from_timestamp(datetime.unix_timestamp(), datetime.nanosecond())
387 .expect("bug: invalid time"),
388 ))
389 }
390}
391
392#[cfg(feature = "time")]
393impl From<time::PrimitiveDateTime> for DatePerhapsTime {
394 fn from(datetime: time::PrimitiveDateTime) -> Self {
395 let utc = datetime.assume_utc();
396 Self::DateTime(CalendarDateTime::Floating(
397 NaiveDateTime::from_timestamp_opt(utc.unix_timestamp(), utc.nanosecond())
398 .expect("bug: invalid date"),
399 ))
400 }
401}
402
403#[cfg(feature = "parser")]
404impl TryFrom<&crate::parser::Property<'_>> for DatePerhapsTime {
405 type Error = &'static str;
406
407 fn try_from(value: &crate::parser::Property) -> Result<Self, Self::Error> {
408 let val = value.val.as_ref();
409
410 if let Ok(utc_dt) = Utc.datetime_from_str(val, "%Y%m%dT%H%M%SZ") {
413 return Ok(Self::DateTime(CalendarDateTime::Utc(utc_dt)));
414 };
415
416 if let Ok(naive_date) = NaiveDate::parse_from_str(val, "%Y%m%d") {
417 return Ok(Self::Date(naive_date));
418 };
419
420 if let Ok(naive_dt) = NaiveDateTime::parse_from_str(val, "%Y%m%dT%H%M%S") {
421 if let Some(tz_param) = value.params.iter().find(|p| p.key == "TZID") {
422 if let Some(tzid) = &tz_param.val {
423 return Ok(Self::DateTime(CalendarDateTime::WithTimezone {
424 date_time: naive_dt,
425 tzid: tzid.as_ref().to_string(),
426 }));
427 } else {
428 return Err("Found empty TZID param.");
429 }
430 } else {
431 return Ok(Self::DateTime(CalendarDateTime::Floating(naive_dt)));
432 };
433 };
434
435 Err("Value does not look like a known DATE-TIME")
436 }
437}
438
439#[cfg(all(test, feature = "parser"))]
440mod try_from_tests {
441 use super::*;
442
443 #[test]
444 fn try_from_utc_dt() {
445 let prop = crate::parser::Property {
446 name: "TRIGGER".into(),
447 val: "20220716T141500Z".into(),
448 params: vec![crate::parser::Parameter {
449 key: "VALUE".into(),
450 val: Some("DATE-TIME".into()),
451 }],
452 };
453
454 let result = DatePerhapsTime::try_from(&prop);
455 let expected = Utc.ymd(2022, 7, 16).and_hms(14, 15, 0);
456
457 assert_eq!(
458 result,
459 Ok(DatePerhapsTime::DateTime(CalendarDateTime::Utc(expected)))
460 );
461 }
462
463 #[test]
464 fn try_from_naive_date() {
465 let prop = crate::parser::Property {
466 name: "TRIGGER".into(),
467 val: "19970714".into(),
468 params: vec![crate::parser::Parameter {
469 key: "VALUE".into(),
470 val: Some("DATE-TIME".into()),
471 }],
472 };
473
474 let result = DatePerhapsTime::try_from(&prop);
475 let expected = NaiveDate::from_ymd(1997, 7, 14);
476
477 assert_eq!(result, Ok(DatePerhapsTime::Date(expected)));
478 }
479
480 #[test]
481 fn try_from_dt_with_tz() {
482 let prop = crate::parser::Property {
483 name: "TRIGGER".into(),
484 val: "20220716T141500".into(),
485 params: vec![
486 crate::parser::Parameter {
487 key: "VALUE".into(),
488 val: Some("DATE-TIME".into()),
489 },
490 crate::parser::Parameter {
491 key: "TZID".into(),
492 val: Some("MY-TZ-ID".into()),
493 },
494 ],
495 };
496
497 let result = DatePerhapsTime::try_from(&prop);
498 let expected = NaiveDate::from_ymd(2022, 7, 16).and_hms(14, 15, 0);
499
500 assert_eq!(
501 result,
502 Ok(DatePerhapsTime::DateTime(CalendarDateTime::WithTimezone {
503 date_time: expected,
504 tzid: "MY-TZ-ID".into(),
505 }))
506 );
507 }
508
509 #[test]
510 fn try_from_dt_with_empty_tz() {
511 let prop = crate::parser::Property {
512 name: "TRIGGER".into(),
513 val: "20220716T141500".into(),
514 params: vec![
515 crate::parser::Parameter {
516 key: "VALUE".into(),
517 val: Some("DATE-TIME".into()),
518 },
519 crate::parser::Parameter {
520 key: "TZID".into(),
521 val: None,
522 },
523 ],
524 };
525
526 let result = DatePerhapsTime::try_from(&prop);
527
528 assert_eq!(result, Err("Found empty TZID param."));
529 }
530
531 #[test]
532 fn try_from_floating_dt() {
533 let prop = crate::parser::Property {
534 name: "TRIGGER".into(),
535 val: "20220716T141500".into(),
536 params: vec![crate::parser::Parameter {
537 key: "VALUE".into(),
538 val: Some("DATE-TIME".into()),
539 }],
540 };
541
542 let result = DatePerhapsTime::try_from(&prop);
543 let expected = NaiveDate::from_ymd(2022, 7, 16).and_hms(14, 15, 0);
544
545 assert_eq!(
546 result,
547 Ok(DatePerhapsTime::DateTime(CalendarDateTime::Floating(
548 expected
549 )))
550 );
551 }
552
553 #[test]
554 fn try_from_non_dt_prop() {
555 let prop = crate::parser::Property {
556 name: "TZNAME".into(),
557 val: "CET".into(),
558 params: vec![],
559 };
560
561 let result = DatePerhapsTime::try_from(&prop);
562
563 assert_eq!(result, Err("Value does not look like a known DATE-TIME"));
564 }
565}