mod inner;
mod local;
mod sys;
use crate::inner::TzInner;
pub use crate::local::Local;
use chrono::{FixedOffset, MappedLocalTime, NaiveDate, NaiveDateTime, NaiveTime, Offset, TimeZone};
use libc::time_t;
use std::{
ffi::NulError,
fmt::{self, Debug, Display, Formatter},
io,
sync::Arc,
};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum TzError {
#[error("Invalid string for timezone ID: {0}")]
InvalidId(#[from] NulError),
#[error("Error allocating timezone: {0}")]
Io(#[from] io::Error),
}
#[derive(Clone, Debug)]
pub struct TzOffset {
timezone: Arc<TzInner>,
time: time_t,
}
impl TzOffset {
pub fn name(&self) -> &str {
self.timezone.name_at_utc_timestamp(self.time)
}
}
impl Display for TzOffset {
fn fmt(&self, f: &mut Formatter) -> fmt::Result {
f.write_str(self.name())
}
}
impl Offset for TzOffset {
fn fix(&self) -> FixedOffset {
self.timezone.offset_at_utc_timestamp(self.time)
}
}
#[derive(Clone, Debug)]
pub struct Tz {
timezone: Arc<TzInner>,
}
impl Tz {
pub fn new(olson_id: &str) -> Result<Self, TzError> {
Ok(Self {
timezone: Arc::new(TzInner::new(olson_id)?),
})
}
pub fn local() -> Result<Self, TzError> {
Ok(Self {
timezone: Arc::new(TzInner::local()?),
})
}
}
impl TimeZone for Tz {
type Offset = TzOffset;
fn from_offset(offset: &TzOffset) -> Self {
Self {
timezone: offset.timezone.clone(),
}
}
fn offset_from_local_date(&self, local: &NaiveDate) -> MappedLocalTime<TzOffset> {
self.offset_from_local_datetime(&local.and_time(NaiveTime::MIN))
}
fn offset_from_local_datetime(&self, local: &NaiveDateTime) -> MappedLocalTime<TzOffset> {
self.timezone
.timestamp_from_local_datetime(local)
.map(|time| TzOffset {
timezone: self.timezone.clone(),
time,
})
}
fn offset_from_utc_date(&self, utc: &NaiveDate) -> TzOffset {
self.offset_from_utc_datetime(&utc.and_time(NaiveTime::MIN))
}
fn offset_from_utc_datetime(&self, utc: &NaiveDateTime) -> TzOffset {
TzOffset {
timezone: self.timezone.clone(),
time: utc.and_utc().timestamp() as time_t,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn london_names() {
let london = Tz::new("Europe/London").unwrap();
let winter = london.with_ymd_and_hms(2026, 3, 1, 0, 0, 0).unwrap();
assert_eq!(winter.offset().name(), "GMT");
let summer = london.with_ymd_and_hms(2026, 4, 1, 0, 0, 0).unwrap();
assert_eq!(summer.offset().name(), "BST");
}
#[test]
fn london_offsets() {
let london = Tz::new("Europe/London").unwrap();
let winter = london.with_ymd_and_hms(2026, 3, 1, 0, 0, 0).unwrap();
assert_eq!(winter.offset().fix(), FixedOffset::east_opt(0).unwrap());
let summer = london.with_ymd_and_hms(2026, 4, 1, 0, 0, 0).unwrap();
assert_eq!(summer.offset().fix(), FixedOffset::east_opt(3600).unwrap());
}
#[test]
fn dst_boundaries() {
let london = Tz::new("Europe/London").unwrap();
let start_local = NaiveDate::from_ymd_opt(2026, 3, 29)
.unwrap()
.and_hms_opt(1, 30, 0)
.unwrap();
assert_eq!(
london
.offset_from_local_datetime(&start_local)
.map(|offset| offset.fix()),
MappedLocalTime::None
);
let end_local = NaiveDate::from_ymd_opt(2026, 10, 25)
.unwrap()
.and_hms_opt(1, 30, 0)
.unwrap();
assert_eq!(
london
.offset_from_local_datetime(&end_local)
.map(|offset| offset.fix()),
MappedLocalTime::Ambiguous(
FixedOffset::east_opt(0).unwrap(),
FixedOffset::east_opt(3600).unwrap()
)
);
}
#[test]
fn offsets_from_utc() {
let london = Tz::new("Europe/London").unwrap();
let winter = NaiveDate::from_ymd_opt(2026, 3, 1)
.unwrap()
.and_hms_opt(0, 0, 0)
.unwrap();
assert_eq!(
london.offset_from_utc_datetime(&winter).fix(),
FixedOffset::east_opt(0).unwrap()
);
let summer = NaiveDate::from_ymd_opt(2026, 4, 1)
.unwrap()
.and_hms_opt(0, 0, 0)
.unwrap();
assert_eq!(
london.offset_from_utc_datetime(&summer).fix(),
FixedOffset::east_opt(3600).unwrap()
);
}
#[test]
fn local_same() {
let local = Local.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap();
let local_from_tz = Tz::local()
.unwrap()
.with_ymd_and_hms(2026, 1, 1, 0, 0, 0)
.unwrap();
assert_eq!(local, local_from_tz);
}
}