use crate::datetime::DateTime;
#[cfg(feature = "iana-tz")]
pub use iana::{load_zone, zone_names, IanaZone};
#[cfg(feature = "iana-tz")]
mod iana {
use crate::datetime::DateTime;
const UNIX_EPOCH_JDN: i64 = 2_440_588;
fn to_unix(dt: &DateTime) -> i64 {
let jdn = crate::calendar::gregorian_to_jdn(dt.year as i64, dt.month as i64, dt.day as i64);
(jdn - UNIX_EPOCH_JDN) * 86_400
+ dt.hour as i64 * 3600
+ dt.minute as i64 * 60
+ dt.second as i64
}
fn from_unix(secs: i64) -> DateTime {
let (days, sod) = (secs.div_euclid(86_400), secs.rem_euclid(86_400));
let (y, m, d) = crate::calendar::jdn_to_gregorian(UNIX_EPOCH_JDN + days);
DateTime {
year: y as i32,
month: m as u8,
day: d as u8,
hour: (sod / 3600) as u8,
minute: (sod % 3600 / 60) as u8,
second: (sod % 60) as u8,
}
}
pub struct IanaZone(timezone_data::Zone<'static>);
#[must_use]
pub fn load_zone(name: &str) -> Option<IanaZone> {
timezone_data::load(name).ok().map(IanaZone)
}
impl IanaZone {
#[must_use]
pub fn offset_at(&self, unix: i64) -> i32 {
self.0.lookup(unix).offset
}
#[must_use]
pub fn abbrev_at(&self, unix: i64) -> &'static str {
self.0.lookup(unix).abbrev
}
#[must_use]
pub fn is_dst_at(&self, unix: i64) -> bool {
self.0.lookup(unix).is_dst
}
#[must_use]
pub fn to_local(&self, unix: i64) -> DateTime {
from_unix(unix + self.0.lookup(unix).offset as i64)
}
#[must_use]
pub fn offset_for_local(&self, dt: &DateTime) -> i32 {
let approx = to_unix(dt);
let off = self.0.lookup(approx).offset as i64;
self.0.lookup(approx - off).offset
}
}
pub fn zone_names() -> impl Iterator<Item = &'static str> {
timezone_data::names()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Rule {
month: u8, week: u8, dow: u8, time: i32, }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PosixTz {
std_offset: i32,
dst: Option<(i32, Rule, Rule)>,
}
fn parse_offset(s: &str) -> Option<(i32, usize)> {
let bytes = s.as_bytes();
let mut i = 0;
let neg = match bytes.first() {
Some(b'-') => {
i += 1;
true
}
Some(b'+') => {
i += 1;
false
}
_ => false,
};
let start = i;
let mut parts = [0i32; 3];
let mut p = 0;
while p < 3 {
let d0 = i;
while i < bytes.len() && bytes[i].is_ascii_digit() {
i += 1;
}
if i == d0 {
return None;
}
parts[p] = s[d0..i].parse().ok()?;
p += 1;
if i < bytes.len() && bytes[i] == b':' {
i += 1;
} else {
break;
}
}
if i == start {
return None;
}
let secs = parts[0]
.checked_mul(3600)?
.checked_add(parts[1].checked_mul(60)?)?
.checked_add(parts[2])?;
Some((if neg { secs } else { -secs }, i))
}
fn parse_rule(s: &str) -> Option<Rule> {
let s = s.strip_prefix('M')?;
let (spec, time) = match s.split_once('/') {
Some((a, b)) => (a, parse_offset(b)?.0.checked_neg()?),
None => (s, 2 * 3600),
};
let mut it = spec.split('.');
let month: u8 = it.next()?.parse().ok()?;
let week: u8 = it.next()?.parse().ok()?;
let dow: u8 = it.next()?.parse().ok()?;
if it.next().is_some() {
return None;
}
Some(Rule {
month,
week,
dow,
time,
})
}
impl PosixTz {
#[must_use]
pub fn parse(tz: &str) -> Option<PosixTz> {
let after_std_name = skip_name(tz)?;
let (std_offset, n) = parse_offset(&tz[after_std_name..])?;
let mut rest = &tz[after_std_name + n..];
if rest.is_empty() {
return Some(PosixTz {
std_offset,
dst: None,
});
}
let after_dst_name = skip_name(rest)?;
let dst_off_str = &rest[after_dst_name..];
let (dst_offset, used) = if dst_off_str.starts_with(',') {
(std_offset.checked_add(3600)?, 0)
} else {
parse_offset(dst_off_str)?
};
rest = &dst_off_str[used..];
let rules = rest.strip_prefix(',')?;
let (start, end) = rules.split_once(',')?;
Some(PosixTz {
std_offset,
dst: Some((dst_offset, parse_rule(start)?, parse_rule(end)?)),
})
}
#[must_use]
pub fn offset_seconds(&self, dt: &DateTime) -> i32 {
let Some((dst_offset, start, end)) = self.dst else {
return self.std_offset;
};
let now = local_seconds(dt);
let s = rule_seconds(start, dt.year as i64);
let e = rule_seconds(end, dt.year as i64);
let in_dst = if s < e {
now >= s && now < e } else {
now >= s || now < e };
if in_dst {
dst_offset
} else {
self.std_offset
}
}
#[must_use]
pub fn is_dst(&self, dt: &DateTime) -> bool {
self.dst.is_some_and(|(d, ..)| self.offset_seconds(dt) == d)
}
}
fn skip_name(s: &str) -> Option<usize> {
let b = s.as_bytes();
if b.first() == Some(&b'<') {
return s.find('>').map(|i| i + 1);
}
let mut i = 0;
while i < b.len() && b[i].is_ascii_alphabetic() {
i += 1;
}
if i == 0 {
None
} else {
Some(i)
}
}
fn local_seconds(dt: &DateTime) -> i64 {
let jan1 = crate::calendar::gregorian_to_jdn(dt.year as i64, 1, 1);
let jdn = crate::calendar::gregorian_to_jdn(dt.year as i64, dt.month as i64, dt.day as i64);
(jdn - jan1) * 86_400 + dt.hour as i64 * 3600 + dt.minute as i64 * 60 + dt.second as i64
}
fn rule_seconds(rule: Rule, year: i64) -> i64 {
let jan1 = crate::calendar::gregorian_to_jdn(year, 1, 1);
let first = crate::calendar::gregorian_to_jdn(year, rule.month as i64, 1);
let first_dow = (first.rem_euclid(7) + 1) % 7; let mut day = 1 + (rule.dow as i64 - first_dow).rem_euclid(7) + (rule.week as i64 - 1) * 7;
let dim = days_in_month(year, rule.month as i64);
while day > dim {
day -= 7;
}
let jdn = crate::calendar::gregorian_to_jdn(year, rule.month as i64, day);
(jdn - jan1) * 86_400 + rule.time as i64
}
fn days_in_month(year: i64, month: i64) -> i64 {
let next = crate::calendar::gregorian_to_jdn(
if month == 12 { year + 1 } else { year },
if month == 12 { 1 } else { month + 1 },
1,
);
next - crate::calendar::gregorian_to_jdn(year, month, 1)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn offset_overflow_returns_none() {
assert!(parse_offset("600000").is_none());
assert!(PosixTz::parse("X600000").is_none());
}
#[test]
fn default_dst_offset_overflow_returns_none() {
assert!(PosixTz::parse("STD-596523DST,M3.2.0,M11.1.0").is_none());
}
}