1use thiserror::Error;
2
3#[derive(Debug, Clone, PartialEq, Eq, Hash)]
9pub struct Tz(String);
10
11#[derive(Debug, Error, PartialEq, Eq)]
12pub enum TzParseError {
13 #[error("timezone name must not be empty")]
14 Empty,
15 #[error("unknown IANA timezone: {0}")]
16 Unknown(String),
17}
18
19impl Tz {
20 pub fn parse(s: &str) -> Result<Self, TzParseError> {
22 if s.is_empty() {
23 return Err(TzParseError::Empty);
24 }
25 if !Self::is_valid(s) {
26 return Err(TzParseError::Unknown(s.to_owned()));
27 }
28 Ok(Self(s.to_owned()))
29 }
30
31 pub fn as_iana(&self) -> &str {
32 &self.0
33 }
34
35 pub fn utc() -> Self {
36 Self("UTC".to_owned())
38 }
39
40 pub fn seoul() -> Self {
41 Self("Asia/Seoul".to_owned())
42 }
43
44 #[cfg(feature = "native")]
45 fn is_valid(s: &str) -> bool {
46 time_tz::timezones::get_by_name(s).is_some()
47 }
48
49 #[cfg(all(feature = "mock", not(feature = "native")))]
51 fn is_valid(_s: &str) -> bool {
52 true
53 }
54
55 #[cfg(not(any(feature = "native", feature = "mock")))]
57 fn is_valid(_s: &str) -> bool {
58 false
59 }
60}
61
62impl std::fmt::Display for Tz {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 f.write_str(&self.0)
65 }
66}
67
68#[cfg(test)]
69mod tests {
70 use super::*;
71
72 #[test]
73 fn parse_valid_roundtrips() {
74 let tz = Tz::parse("Asia/Seoul").expect("Asia/Seoul should be valid");
75 assert_eq!(tz.as_iana(), "Asia/Seoul");
76 }
77
78 #[test]
79 fn parse_empty_is_error() {
80 assert_eq!(Tz::parse(""), Err(TzParseError::Empty));
81 }
82
83 #[test]
84 #[cfg(feature = "native")]
85 fn parse_unknown_is_error_on_native() {
86 assert!(matches!(
87 Tz::parse("Mars/Olympus"),
88 Err(TzParseError::Unknown(_))
89 ));
90 }
91
92 #[test]
93 fn seoul_roundtrips() {
94 assert_eq!(Tz::seoul().as_iana(), "Asia/Seoul");
95 }
96
97 #[test]
98 fn utc_roundtrips() {
99 assert_eq!(Tz::utc().as_iana(), "UTC");
100 }
101
102 #[test]
103 fn display_matches_iana() {
104 let tz = Tz::parse("Asia/Seoul").expect("valid");
105 assert_eq!(tz.to_string(), "Asia/Seoul");
106 }
107
108 #[cfg(feature = "native")]
109 mod proptest_tz {
110 use super::*;
111 use proptest::prelude::*;
112
113 proptest! {
114 #[test]
115 fn parse_never_panics(s in ".*") {
116 let _ = Tz::parse(&s);
117 }
118 }
119 }
120}