use jiff::tz::{Offset, TimeZone};
use winnow::{
combinator::{alt, delimited, opt, preceded, repeat},
stream::AsChar,
token::{one_of, take_while},
ModalResult, Parser,
};
use super::primitive::{dec_uint, plus_or_minus};
pub(super) fn parse(input: &mut &str) -> ModalResult<TimeZone> {
delimited("TZ=\"", preceded(opt(':'), alt((posix, iana))), '"').parse_next(input)
}
fn posix(input: &mut &str) -> ModalResult<TimeZone> {
(take_while(3.., AsChar::is_alpha), posix_offset)
.verify_map(|(_, offset)| Offset::from_seconds(offset).ok().map(|o| o.to_time_zone()))
.parse_next(input)
}
fn iana(input: &mut &str) -> ModalResult<TimeZone> {
repeat(
0..,
alt((
preceded('\\', one_of(['\\', '"'])).map(|c: char| c.to_string()),
take_while(1, |c| c != '"' && c != '\\').map(str::to_string),
)),
)
.map(|parts: Vec<String>| parts.concat())
.map(|s| TimeZone::get(&s).unwrap_or(TimeZone::UTC))
.parse_next(input)
}
fn posix_offset(input: &mut &str) -> ModalResult<i32> {
let uint = dec_uint::<u32, _>;
(
opt(plus_or_minus),
alt((
(uint, preceded(':', uint), preceded(':', uint)).map(|(h, m, s)| (h, m, s)),
(uint, preceded(':', uint)).map(|(h, m)| (h, m, 0)),
uint.map(|h| (h, 0, 0)),
)),
)
.map(|(sign, (h, m, s))| {
let sign = if sign == Some('-') { 1 } else { -1 };
let h = h.min(24) as i32;
let m = m.min(59) as i32;
let s = s.min(59) as i32;
sign * (h * 3600 + m * 60 + s)
})
.parse_next(input)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tz_rule() {
for (input, expected) in [
(r#"TZ="""#, "UTC"),
(r#"TZ=":""#, "UTC"),
(r#"TZ=" ""#, "UTC"),
(r#"TZ=": ""#, "UTC"),
] {
let mut s = input;
assert_eq!(
parse(&mut s).unwrap().iana_name(),
Some(expected),
"{input}"
);
}
for (input, expected) in [
(r#"TZ="Etc/Zulu""#, "Etc/Zulu"),
(r#"TZ=":Etc/Zulu""#, "Etc/Zulu"),
(r#"TZ="America/New_York""#, "America/New_York"),
(r#"TZ=":America/New_York""#, "America/New_York"),
(r#"TZ="Asia/Tokyo""#, "Asia/Tokyo"),
(r#"TZ=":Asia/Tokyo""#, "Asia/Tokyo"),
(r#"TZ="Unknown/Timezone""#, "UTC"),
(r#"TZ=":Unknown/Timezone""#, "UTC"),
] {
let mut s = input;
assert_eq!(
parse(&mut s).unwrap().iana_name(),
Some(expected),
"{input}"
);
}
for (input, expected) in [
(r#"TZ="UTC0""#, 0),
(r#"TZ=":UTC0""#, 0),
(r#"TZ="UTC+5""#, -5 * 3600),
(r#"TZ=":UTC+5""#, -5 * 3600),
(r#"TZ="UTC-5""#, 5 * 3600),
(r#"TZ=":UTC-5""#, 5 * 3600),
(r#"TZ="UTC+5:20""#, -(5 * 3600 + 20 * 60)),
(r#"TZ=":UTC+5:20""#, -(5 * 3600 + 20 * 60)),
(r#"TZ="UTC-5:20""#, 5 * 3600 + 20 * 60),
(r#"TZ=":UTC-5:20""#, 5 * 3600 + 20 * 60),
(r#"TZ="UTC+5:20:15""#, -(5 * 3600 + 20 * 60 + 15)),
(r#"TZ=":UTC+5:20:15""#, -(5 * 3600 + 20 * 60 + 15)),
(r#"TZ="UTC-5:20:15""#, 5 * 3600 + 20 * 60 + 15),
(r#"TZ=":UTC-5:20:15""#, 5 * 3600 + 20 * 60 + 15),
] {
let mut s = input;
assert_eq!(
parse(&mut s).unwrap().to_fixed_offset().unwrap().seconds(),
expected,
"{input}"
);
}
for input in [
r#"UTC"#, r#"tz="UTC""#, r#"TZ=UTC"#, ] {
let mut s = input;
assert!(parse(&mut s).is_err(), "{input}");
}
}
#[test]
fn parse_iana() {
for (input, expected) in [
("UTC", "UTC"), ("Etc/Zulu", "Etc/Zulu"), ("America/New_York", "America/New_York"), ("Asia/Tokyo", "Asia/Tokyo"), ("Unknown/Timezone", "UTC"), ] {
let mut s = input;
assert_eq!(iana(&mut s).unwrap().iana_name(), Some(expected), "{input}");
}
}
#[test]
fn parse_posix() {
let to_seconds = |input: &str| {
let mut s = input;
posix(&mut s).unwrap().to_fixed_offset().unwrap().seconds()
};
for (input, expected) in [
("UTC0", 0),
("UTC+0", 0),
("UTC-0", 0),
("UTC000", 0),
("UTC+5", -5 * 3600),
("UTC-5", 5 * 3600),
("ABC0", 0),
("ABC+5", -5 * 3600),
("ABC-5", 5 * 3600),
] {
assert_eq!(to_seconds(input), expected, "{input}");
}
for (input, expected) in [
("UTC0:0", 0),
("UTC+0:0", 0),
("UTC-0:0", 0),
("UTC00:00", 0),
("UTC+5:20", -(5 * 3600 + 20 * 60)),
("UTC-5:20", 5 * 3600 + 20 * 60),
("ABC0:0", 0),
("ABC+5:20", -(5 * 3600 + 20 * 60)),
("ABC-5:20", 5 * 3600 + 20 * 60),
] {
assert_eq!(to_seconds(input), expected, "{input}");
}
for (input, expected) in [
("UTC0:0:0", 0),
("UTC+0:0:0", 0),
("UTC-0:0:0", 0),
("UTC00:00:00", 0),
("UTC+5:20:15", -(5 * 3600 + 20 * 60 + 15)),
("UTC-5:20:15", 5 * 3600 + 20 * 60 + 15),
("ABC0:0:0", 0),
("ABC+5:20:15", -(5 * 3600 + 20 * 60 + 15)),
("ABC-5:20:15", 5 * 3600 + 20 * 60 + 15),
] {
assert_eq!(to_seconds(input), expected, "{input}");
}
for input in [
"AB", "A1C", ] {
let mut s = input;
assert!(posix(&mut s).is_err(), "{input}");
}
}
#[test]
fn parse_posix_offset() {
for (input, expected) in [
("0", 0), ("00", 0), ("000", 0), ("+0", 0), ("-0", 0), ("5", -5 * 3600), ("-5", 5 * 3600), ("005", -5 * 3600), ("-05", 5 * 3600), ("25", -24 * 3600), ("-25", 24 * 3600), ] {
let mut s = input;
assert_eq!(posix_offset(&mut s).unwrap(), expected, "{input}");
}
for (input, expected) in [
("0:0", 0), ("00:00", 0), ("000:000", 0), ("+0:0", 0), ("-0:0", 0), ("5:20", -(5 * 3600 + 20 * 60)), ("-5:20", 5 * 3600 + 20 * 60), ("005:020", -(5 * 3600 + 20 * 60)), ("-05:20", 5 * 3600 + 20 * 60), ("25:20", -(24 * 3600 + 20 * 60)), ("-25:20", 24 * 3600 + 20 * 60), ("5:60", -(5 * 3600 + 59 * 60)), ("-5:60", 5 * 3600 + 59 * 60), ] {
let mut s = input;
assert_eq!(posix_offset(&mut s).unwrap(), expected, "{input}");
}
for (input, expected) in [
("0:0:0", 0), ("00:00:00", 0), ("000:000:000", 0), ("+0:0:0", 0), ("-0:0:0", 0), ("5:20:15", -(5 * 3600 + 20 * 60 + 15)), ("-5:20:15", 5 * 3600 + 20 * 60 + 15), ("005:020:015", -(5 * 3600 + 20 * 60 + 15)), ("-05:20:15", 5 * 3600 + 20 * 60 + 15), ("25:20:15", -(24 * 3600 + 20 * 60 + 15)), ("-25:20:15", 24 * 3600 + 20 * 60 + 15), ("5:60:15", -(5 * 3600 + 59 * 60 + 15)), ("-5:60:15", 5 * 3600 + 59 * 60 + 15), ("5:20:60", -(5 * 3600 + 20 * 60 + 59)), ("-5:20:60", 5 * 3600 + 20 * 60 + 59), ] {
let mut s = input;
assert_eq!(posix_offset(&mut s).unwrap(), expected, "{input}");
}
}
}