use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Tz(String);
#[derive(Debug, Error, PartialEq, Eq)]
pub enum TzParseError {
#[error("timezone name must not be empty")]
Empty,
#[error("unknown IANA timezone: {0}")]
Unknown(String),
}
impl Tz {
pub fn parse(s: &str) -> Result<Self, TzParseError> {
if s.is_empty() {
return Err(TzParseError::Empty);
}
if !Self::is_valid(s) {
return Err(TzParseError::Unknown(s.to_owned()));
}
Ok(Self(s.to_owned()))
}
pub fn as_iana(&self) -> &str {
&self.0
}
pub fn utc() -> Self {
Self("UTC".to_owned())
}
pub fn seoul() -> Self {
Self("Asia/Seoul".to_owned())
}
#[cfg(feature = "native")]
fn is_valid(s: &str) -> bool {
time_tz::timezones::get_by_name(s).is_some()
}
#[cfg(all(feature = "mock", not(feature = "native")))]
fn is_valid(_s: &str) -> bool {
true
}
#[cfg(not(any(feature = "native", feature = "mock")))]
fn is_valid(_s: &str) -> bool {
false
}
}
impl std::fmt::Display for Tz {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.0)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_valid_roundtrips() {
let tz = Tz::parse("Asia/Seoul").expect("Asia/Seoul should be valid");
assert_eq!(tz.as_iana(), "Asia/Seoul");
}
#[test]
fn parse_empty_is_error() {
assert_eq!(Tz::parse(""), Err(TzParseError::Empty));
}
#[test]
#[cfg(feature = "native")]
fn parse_unknown_is_error_on_native() {
assert!(matches!(
Tz::parse("Mars/Olympus"),
Err(TzParseError::Unknown(_))
));
}
#[test]
fn seoul_roundtrips() {
assert_eq!(Tz::seoul().as_iana(), "Asia/Seoul");
}
#[test]
fn utc_roundtrips() {
assert_eq!(Tz::utc().as_iana(), "UTC");
}
#[test]
fn display_matches_iana() {
let tz = Tz::parse("Asia/Seoul").expect("valid");
assert_eq!(tz.to_string(), "Asia/Seoul");
}
#[cfg(feature = "native")]
mod proptest_tz {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn parse_never_panics(s in ".*") {
let _ = Tz::parse(&s);
}
}
}
}