1mod inner;
16mod local;
17mod sys;
18
19use crate::inner::TzInner;
20pub use crate::local::Local;
21use chrono::{FixedOffset, MappedLocalTime, NaiveDate, NaiveDateTime, NaiveTime, Offset, TimeZone};
22use libc::time_t;
23use std::{
24 ffi::NulError,
25 fmt::{self, Debug, Display, Formatter},
26 io,
27 sync::Arc,
28};
29use thiserror::Error;
30
31#[derive(Debug, Error)]
33pub enum TzError {
34 #[error("Invalid string for timezone ID: {0}")]
36 InvalidId(#[from] NulError),
37 #[error("Error allocating timezone: {0}")]
39 Io(#[from] io::Error),
40}
41
42#[derive(Clone, Debug)]
44pub struct TzOffset {
45 timezone: Arc<TzInner>,
46 time: time_t,
47}
48
49impl TzOffset {
50 pub fn name(&self) -> &str {
52 self.timezone.name_at_utc_timestamp(self.time)
53 }
54}
55
56impl Display for TzOffset {
57 fn fmt(&self, f: &mut Formatter) -> fmt::Result {
58 f.write_str(self.name())
59 }
60}
61
62impl Offset for TzOffset {
63 fn fix(&self) -> FixedOffset {
64 self.timezone.offset_at_utc_timestamp(self.time)
65 }
66}
67
68#[derive(Clone, Debug)]
70pub struct Tz {
71 timezone: Arc<TzInner>,
72}
73
74impl Tz {
75 pub fn new(olson_id: &str) -> Result<Self, TzError> {
77 Ok(Self {
78 timezone: Arc::new(TzInner::new(olson_id)?),
79 })
80 }
81
82 pub fn local() -> Result<Self, TzError> {
87 Ok(Self {
88 timezone: Arc::new(TzInner::local()?),
89 })
90 }
91}
92
93impl TimeZone for Tz {
94 type Offset = TzOffset;
95
96 fn from_offset(offset: &TzOffset) -> Self {
97 Self {
98 timezone: offset.timezone.clone(),
99 }
100 }
101
102 fn offset_from_local_date(&self, local: &NaiveDate) -> MappedLocalTime<TzOffset> {
103 self.offset_from_local_datetime(&local.and_time(NaiveTime::MIN))
104 }
105
106 fn offset_from_local_datetime(&self, local: &NaiveDateTime) -> MappedLocalTime<TzOffset> {
107 self.timezone
108 .timestamp_from_local_datetime(local)
109 .map(|time| TzOffset {
110 timezone: self.timezone.clone(),
111 time,
112 })
113 }
114
115 fn offset_from_utc_date(&self, utc: &NaiveDate) -> TzOffset {
116 self.offset_from_utc_datetime(&utc.and_time(NaiveTime::MIN))
117 }
118
119 fn offset_from_utc_datetime(&self, utc: &NaiveDateTime) -> TzOffset {
120 TzOffset {
121 timezone: self.timezone.clone(),
122 time: utc.and_utc().timestamp() as time_t,
123 }
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[test]
132 fn london_names() {
133 let london = Tz::new("Europe/London").unwrap();
134
135 let winter = london.with_ymd_and_hms(2026, 3, 1, 0, 0, 0).unwrap();
136 assert_eq!(winter.offset().name(), "GMT");
137
138 let summer = london.with_ymd_and_hms(2026, 4, 1, 0, 0, 0).unwrap();
139 assert_eq!(summer.offset().name(), "BST");
140 }
141
142 #[test]
143 fn london_offsets() {
144 let london = Tz::new("Europe/London").unwrap();
145
146 let winter = london.with_ymd_and_hms(2026, 3, 1, 0, 0, 0).unwrap();
147 assert_eq!(winter.offset().fix(), FixedOffset::east_opt(0).unwrap());
148
149 let summer = london.with_ymd_and_hms(2026, 4, 1, 0, 0, 0).unwrap();
150 assert_eq!(summer.offset().fix(), FixedOffset::east_opt(3600).unwrap());
151 }
152
153 #[test]
154 fn dst_boundaries() {
155 let london = Tz::new("Europe/London").unwrap();
156
157 let start_local = NaiveDate::from_ymd_opt(2026, 3, 29)
159 .unwrap()
160 .and_hms_opt(1, 30, 0)
161 .unwrap();
162 assert_eq!(
163 london
164 .offset_from_local_datetime(&start_local)
165 .map(|offset| offset.fix()),
166 MappedLocalTime::None
167 );
168
169 let end_local = NaiveDate::from_ymd_opt(2026, 10, 25)
171 .unwrap()
172 .and_hms_opt(1, 30, 0)
173 .unwrap();
174 assert_eq!(
175 london
176 .offset_from_local_datetime(&end_local)
177 .map(|offset| offset.fix()),
178 MappedLocalTime::Ambiguous(
179 FixedOffset::east_opt(0).unwrap(),
180 FixedOffset::east_opt(3600).unwrap()
181 )
182 );
183 }
184
185 #[test]
186 fn offsets_from_utc() {
187 let london = Tz::new("Europe/London").unwrap();
188
189 let winter = NaiveDate::from_ymd_opt(2026, 3, 1)
190 .unwrap()
191 .and_hms_opt(0, 0, 0)
192 .unwrap();
193 assert_eq!(
194 london.offset_from_utc_datetime(&winter).fix(),
195 FixedOffset::east_opt(0).unwrap()
196 );
197
198 let summer = NaiveDate::from_ymd_opt(2026, 4, 1)
199 .unwrap()
200 .and_hms_opt(0, 0, 0)
201 .unwrap();
202 assert_eq!(
203 london.offset_from_utc_datetime(&summer).fix(),
204 FixedOffset::east_opt(3600).unwrap()
205 );
206 }
207
208 #[test]
209 fn local_same() {
210 let local = Local.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap();
211 let local_from_tz = Tz::local()
212 .unwrap()
213 .with_ymd_and_hms(2026, 1, 1, 0, 0, 0)
214 .unwrap();
215 assert_eq!(local, local_from_tz);
216 }
217}