1use alloc::string::String;
15
16use crate::zones::{WINDOWS, ZONES};
17use crate::{days_in_month, weekday, Civil, Timestamp};
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22enum Rule {
23 M { month: u8, week: u8, wday: u8, time: i32 },
24 J { day: u16, time: i32 },
25 D { day: u16, time: i32 },
26}
27
28#[derive(Clone, Debug, PartialEq, Eq)]
29struct Dst {
30 abbr: String,
31 off: i32,
32 start: Rule,
33 end: Rule,
34}
35
36#[derive(Clone, Debug, PartialEq, Eq)]
38pub struct Zone {
39 name: String,
40 abbr: String,
41 off: i32,
42 dst: Option<Dst>,
43}
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
47pub enum LocalResult {
48 Single(Timestamp),
49 Ambiguous(Timestamp, Timestamp),
51 Gap { after: Timestamp },
53}
54
55impl LocalResult {
56 pub fn earliest(&self) -> Timestamp {
58 match *self {
59 LocalResult::Single(t) | LocalResult::Ambiguous(t, _) => t,
60 LocalResult::Gap { after } => after,
61 }
62 }
63}
64
65struct P<'a> {
68 s: &'a [u8],
69 i: usize,
70}
71
72impl P<'_> {
73 fn peek(&self) -> Option<u8> {
74 self.s.get(self.i).copied()
75 }
76 fn name(&mut self) -> Option<String> {
77 if self.peek() == Some(b'<') {
78 let start = self.i + 1;
79 while self.peek()? != b'>' {
80 self.i += 1;
81 }
82 let n = core::str::from_utf8(&self.s[start..self.i]).ok()?;
83 self.i += 1;
84 return Some(String::from(n));
85 }
86 let start = self.i;
87 while matches!(self.peek(), Some(c) if c.is_ascii_alphabetic()) {
88 self.i += 1;
89 }
90 if self.i - start < 3 {
91 return None;
92 }
93 Some(String::from(core::str::from_utf8(&self.s[start..self.i]).ok()?))
94 }
95 fn num(&mut self) -> Option<i32> {
96 let start = self.i;
97 let mut v = 0i32;
98 while let Some(c) = self.peek() {
99 if !c.is_ascii_digit() {
100 break;
101 }
102 v = v.checked_mul(10)?.checked_add((c - b'0') as i32)?;
103 self.i += 1;
104 }
105 if self.i == start { None } else { Some(v) }
106 }
107 fn time(&mut self) -> Option<i32> {
109 let neg = match self.peek() {
110 Some(b'-') => {
111 self.i += 1;
112 true
113 }
114 Some(b'+') => {
115 self.i += 1;
116 false
117 }
118 _ => false,
119 };
120 let h = self.num()?;
121 if h > 167 {
122 return None; }
124 let mut t = h * 3600;
125 if self.peek() == Some(b':') {
126 self.i += 1;
127 let m = self.num()?;
128 if m > 59 {
129 return None;
130 }
131 t += m * 60;
132 if self.peek() == Some(b':') {
133 self.i += 1;
134 let s = self.num()?;
135 if s > 59 {
136 return None;
137 }
138 t += s;
139 }
140 }
141 Some(if neg { -t } else { t })
142 }
143 fn rule(&mut self) -> Option<Rule> {
144 let r = match self.peek()? {
145 b'M' => {
146 self.i += 1;
147 let month = u8::try_from(self.num()?).ok()?;
148 if self.peek()? != b'.' {
149 return None;
150 }
151 self.i += 1;
152 let week = u8::try_from(self.num()?).ok()?;
153 if self.peek()? != b'.' {
154 return None;
155 }
156 self.i += 1;
157 let wday = u8::try_from(self.num()?).ok()?;
158 if !(1..=12).contains(&month) || !(1..=5).contains(&week) || wday > 6 {
159 return None;
160 }
161 Rule::M { month, week, wday, time: 7200 }
162 }
163 b'J' => {
164 self.i += 1;
165 let day = u16::try_from(self.num()?).ok().filter(|d| (1..=365).contains(d))?;
166 Rule::J { day, time: 7200 }
167 }
168 _ => Rule::D { day: u16::try_from(self.num()?).ok().filter(|&d| d <= 365)?, time: 7200 },
169 };
170 let time = if self.peek() == Some(b'/') {
171 self.i += 1;
172 self.time()?
173 } else {
174 7200
175 };
176 Some(match r {
177 Rule::M { month, week, wday, .. } => Rule::M { month, week, wday, time },
178 Rule::J { day, .. } => Rule::J { day, time },
179 Rule::D { day, .. } => Rule::D { day, time },
180 })
181 }
182}
183
184fn parse_posix(rule: &str) -> Option<(String, i32, Option<Dst>)> {
185 let mut p = P { s: rule.as_bytes(), i: 0 };
186 let abbr = p.name()?;
187 let off = -p.time()?;
189 if p.peek().is_none() {
190 return Some((abbr, off, None));
191 }
192 let dabbr = p.name()?;
193 let doff = match p.peek() {
194 Some(b',') | None => off + 3600,
195 _ => -p.time()?,
196 };
197 let (start, end) = if p.peek() == Some(b',') {
198 p.i += 1;
199 let s = p.rule()?;
200 if p.peek()? != b',' {
201 return None;
202 }
203 p.i += 1;
204 let e = p.rule()?;
205 (s, e)
206 } else {
207 (Rule::M { month: 3, week: 2, wday: 0, time: 7200 }, Rule::M { month: 11, week: 1, wday: 0, time: 7200 })
209 };
210 if p.peek().is_some() {
211 return None;
212 }
213 Some((abbr, off, Some(Dst { abbr: dabbr, off: doff, start, end })))
214}
215
216fn rule_local(y: i32, r: Rule) -> i64 {
218 let (date, time) = match r {
219 Rule::M { month, week, wday, time } => {
220 let first = (weekday(y, month, 1) + 1) % 7;
222 let mut day = 1 + (wday as i32 - first as i32 + 7) % 7 + 7 * (week as i32 - 1);
223 let dim = days_in_month(y, month) as i32;
224 while day > dim {
225 day -= 7;
226 }
227 (crate::days_from_civil(y, month, day as u8), time)
228 }
229 Rule::J { day, time } => {
230 let mut d = day as i64 - 1;
232 if crate::is_leap(y) && day >= 60 {
233 d += 1;
234 }
235 (crate::days_from_civil(y, 1, 1) + d, time)
236 }
237 Rule::D { day, time } => (crate::days_from_civil(y, 1, 1) + day as i64, time),
238 };
239 date * 86_400 + time as i64
240}
241
242impl Zone {
243 pub fn utc() -> Zone {
245 Zone { name: String::from("UTC"), abbr: String::from("UTC"), off: 0, dst: None }
246 }
247
248 pub fn fixed(name: &str, offset_secs: i32) -> Zone {
250 Zone { name: String::from(name), abbr: String::from(name), off: offset_secs, dst: None }
251 }
252
253 pub fn from_posix(name: &str, rule: &str) -> Option<Zone> {
255 let (abbr, off, dst) = parse_posix(rule)?;
256 Some(Zone { name: String::from(name), abbr, off, dst })
257 }
258
259 pub fn by_name(name: &str) -> Option<Zone> {
261 if matches!(name, "UTC" | "Z" | "Etc/UTC" | "Etc/Universal" | "Universal" | "Zulu" | "GMT" | "Etc/GMT") {
262 return Some(Zone { name: String::from(if name == "Z" { "UTC" } else { name }), ..Zone::utc() });
263 }
264 let i = ZONES.binary_search_by(|(n, _)| (*n).cmp(name)).ok()?;
265 Zone::from_posix(ZONES[i].0, ZONES[i].1)
266 }
267
268 pub fn from_windows(key: &str) -> Option<Zone> {
270 let i = WINDOWS.binary_search_by(|(k, _)| (*k).cmp(key)).ok()?;
271 Zone::by_name(WINDOWS[i].1)
272 }
273
274 pub fn windows_to_iana(key: &str) -> Option<&'static str> {
276 WINDOWS.binary_search_by(|(k, _)| (*k).cmp(key)).ok().map(|i| WINDOWS[i].1)
277 }
278
279 pub fn names() -> impl Iterator<Item = &'static str> {
281 ZONES.iter().map(|(n, _)| *n)
282 }
283
284 pub fn from_tzif(name: &str, data: &[u8]) -> Option<Zone> {
286 if !data.starts_with(b"TZif") || data.last() != Some(&b'\n') {
287 return None;
288 }
289 let body = &data[..data.len() - 1];
290 let i = body.iter().rposition(|&c| c == b'\n')?;
291 let rule = core::str::from_utf8(&body[i + 1..]).ok()?;
292 if rule.is_empty() {
293 return None;
294 }
295 Zone::from_posix(name, rule)
296 }
297
298 pub fn name(&self) -> &str {
299 &self.name
300 }
301
302 pub fn std_offset(&self) -> i32 {
304 self.off
305 }
306
307 fn dst_window(&self, y: i32) -> Option<(i64, i64, i32)> {
309 let d = self.dst.as_ref()?;
310 let start = rule_local(y, d.start) - self.off as i64;
312 let end = rule_local(y, d.end) - d.off as i64;
313 Some((start, end, d.off))
314 }
315
316 pub fn is_dst(&self, secs: i64) -> bool {
318 let Some(_) = self.dst else { return false };
319 let y = Civil::from_unix(secs + self.off as i64).year;
320 let Some((s, e, _)) = self.dst_window(y) else { return false };
321 if s < e { secs >= s && secs < e } else { secs < e || secs >= s }
322 }
323
324 pub fn offset_at(&self, secs: i64) -> i32 {
326 match &self.dst {
327 Some(d) if self.is_dst(secs) => d.off,
328 _ => self.off,
329 }
330 }
331
332 pub fn abbrev_at(&self, secs: i64) -> &str {
334 match &self.dst {
335 Some(d) if self.is_dst(secs) => &d.abbr,
336 _ => &self.abbr,
337 }
338 }
339
340 pub fn to_local(&self, ts: Timestamp) -> Civil {
342 let mut c = Civil::from_unix(ts.secs + self.offset_at(ts.secs) as i64);
343 c.nanos = ts.nanos;
344 c
345 }
346
347 pub fn from_local(&self, c: &Civil) -> LocalResult {
349 let l = c.to_unix();
350 let ns = c.nanos;
351 let Some(d) = &self.dst else {
352 return LocalResult::Single(Timestamp { secs: l - self.off as i64, nanos: ns });
353 };
354 let t1 = l - self.off as i64; let t2 = l - d.off as i64; let ok1 = self.offset_at(t1) == self.off;
357 let ok2 = self.offset_at(t2) == d.off;
358 let (a, b) = (t1.min(t2), t1.max(t2));
359 match (ok1, ok2) {
360 (true, true) if t1 != t2 => LocalResult::Ambiguous(Timestamp { secs: a, nanos: ns }, Timestamp { secs: b, nanos: ns }),
361 (true, _) => LocalResult::Single(Timestamp { secs: t1, nanos: ns }),
362 (_, true) => LocalResult::Single(Timestamp { secs: t2, nanos: ns }),
363 _ => {
364 let y = c.year;
366 let mut jump = b;
367 for yy in [y - 1, y, y + 1] {
368 if let Some((s, e, _)) = self.dst_window(yy) {
369 for t in [s, e] {
370 if t >= a && t <= b {
371 jump = t;
372 }
373 }
374 }
375 }
376 LocalResult::Gap { after: Timestamp { secs: jump, nanos: 0 } }
377 }
378 }
379 }
380}