use jiff::Timestamp;
use jiff::civil::DateTime;
use jiff::tz::{AmbiguousOffset, TimeZone};
use crate::windows_zones::WINDOWS_ZONES;
#[derive(Debug, Clone)]
pub(crate) enum Zone {
Utc,
Fixed(i32),
Named(TimeZone),
}
const ABBREVIATIONS: &[(&str, i32)] = &[
("HST", -600),
("AKST", -540),
("AKDT", -480),
("PST", -480),
("PDT", -420),
("MST", -420),
("MDT", -360),
("EST", -300),
("EDT", -240),
("NST", -210),
("NDT", -150),
("WET", 0),
("WEST", 60),
("CET", 60),
("CEST", 120),
("EET", 120),
("EEST", 180),
("MSK", 180),
("HKT", 480),
("SGT", 480),
("AWST", 480),
("JST", 540),
("KST", 540),
("ACST", 570),
("ACDT", 630),
("AEST", 600),
("AEDT", 660),
("NZST", 720),
("NZDT", 780),
];
const AMBIGUOUS: &[&str] = &[
"CST", "CDT", "IST", "BST", "AST", "ADT", "SST", "GST", "ECT",
];
pub(crate) fn zone(name: &str) -> Result<Zone, String> {
if matches!(name, "UTC" | "GMT" | "Z") {
return Ok(Zone::Utc);
}
if let Some((_, minutes)) = ABBREVIATIONS.iter().find(|(a, _)| *a == name) {
return Ok(Zone::Fixed(minutes * 60));
}
if AMBIGUOUS.contains(&name) {
return Err(format!(
"`{name}` names more than one time zone; use an offset such as -06:00 or an id such as America/Chicago"
));
}
let id = WINDOWS_ZONES
.iter()
.find(|(windows, _)| *windows == name)
.map_or(name, |(_, iana)| *iana);
TimeZone::get(id)
.map(Zone::Named)
.map_err(|_| format!("`{name}` is not a time zone this engine knows"))
}
pub(crate) fn read_zone(text: &str) -> Result<(Zone, usize), String> {
if let Some((windows, _)) = WINDOWS_ZONES.iter().find(|(w, _)| text.starts_with(w)) {
return Ok((zone(windows)?, windows.len()));
}
let length = text
.find(|c: char| !(c.is_ascii_alphanumeric() || matches!(c, '_' | '/' | '+' | '-')))
.unwrap_or(text.len());
if length == 0 {
return Err("a time zone name is missing".to_owned());
}
Ok((zone(&text[..length])?, length))
}
impl Zone {
pub(crate) fn offset_at_local(
&self,
year: i64,
month: u32,
day: u32,
hour: u32,
minute: u32,
second: u32,
) -> Result<i32, String> {
match self {
Zone::Utc => Ok(0),
Zone::Fixed(seconds) => Ok(*seconds),
Zone::Named(tz) => {
let out_of_range = || format!("{year:04}-{month:02}-{day:02} is out of range");
let local = DateTime::new(
i16::try_from(year).map_err(|_| out_of_range())?,
month as i8,
day as i8,
hour as i8,
minute as i8,
second as i8,
0,
)
.map_err(|e| e.to_string())?;
let offset = match tz.to_ambiguous_timestamp(local).offset() {
AmbiguousOffset::Unambiguous { offset } => offset,
AmbiguousOffset::Gap { before, .. } | AmbiguousOffset::Fold { before, .. } => {
before
}
};
Ok(offset.seconds())
}
}
}
pub(crate) fn offset_at(&self, seconds: i64) -> Result<i32, String> {
match self {
Zone::Utc => Ok(0),
Zone::Fixed(offset) => Ok(*offset),
Zone::Named(tz) => {
let instant = Timestamp::from_second(seconds).map_err(|e| e.to_string())?;
Ok(tz.to_offset(instant).seconds())
}
}
}
pub(crate) fn text(&self, offset: i32) -> String {
if matches!(self, Zone::Utc) {
return "Z".to_owned();
}
let sign = if offset < 0 { '-' } else { '+' };
let minutes = offset.unsigned_abs() / 60;
format!("{sign}{:02}:{:02}", minutes / 60, minutes % 60)
}
}