1use std::fmt::Debug;
2
3use chrono::format::{Parsed, StrftimeItems, parse};
4use chrono::{DateTime, FixedOffset, Local, Offset, ParseError, TimeZone as _, Utc};
5use chrono_tz::Tz;
6use serde::{Deserialize, Serialize};
7
8#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
15#[serde(try_from = "String", into = "String")]
16pub enum TimeZone {
17 #[default]
19 Local,
20
21 Named(Tz),
27}
28
29impl TimeZone {
31 pub fn datetime_from_str(&self, s: &str, format: &str) -> Result<DateTime<Utc>, ParseError> {
37 let mut parsed = Parsed::new();
38 parse(&mut parsed, s, StrftimeItems::new(format))?;
39
40 match self {
41 Self::Local => {
42 let local_datetime = parsed.to_datetime_with_timezone(&Local)?;
43 Ok(datetime_to_utc(&local_datetime))
44 }
45 Self::Named(tz) => {
46 let tz_datetime = parsed.to_datetime_with_timezone(tz)?;
47 Ok(datetime_to_utc(&tz_datetime))
48 }
49 }
50 }
51
52 #[must_use]
53 pub fn parse(s: &str) -> Option<Self> {
54 match s {
55 "" | "local" => Some(Self::Local),
56 _ => s.parse::<Tz>().ok().map(Self::Named),
57 }
58 }
59}
60
61pub(super) fn datetime_to_utc<TZ: chrono::TimeZone>(ts: &DateTime<TZ>) -> DateTime<Utc> {
63 Utc.timestamp_opt(ts.timestamp(), ts.timestamp_subsec_nanos())
64 .single()
65 .expect("invalid timestamp")
66}
67
68impl From<TimeZone> for String {
69 fn from(tz: TimeZone) -> Self {
70 match tz {
71 TimeZone::Local => "local".to_string(),
72 TimeZone::Named(tz) => tz.name().to_string(),
73 }
74 }
75}
76
77impl TryFrom<String> for TimeZone {
78 type Error = String;
79
80 fn try_from(value: String) -> Result<Self, Self::Error> {
81 match TimeZone::parse(&value) {
82 Some(tz) => Ok(tz),
83 None => Err("No such time zone".to_string()),
84 }
85 }
86}
87
88impl From<TimeZone> for FixedOffset {
89 fn from(tz: TimeZone) -> Self {
90 match tz {
91 TimeZone::Local => *Utc::now().with_timezone(&Local).offset(),
92 TimeZone::Named(tz) => Utc::now().with_timezone(&tz).offset().fix(),
93 }
94 }
95}