1#![forbid(unsafe_code)]
20
21use crate::civil::{civil_from_days, days_from_civil};
22use crate::tzif::Observation;
23
24#[derive(Debug, Clone, PartialEq, Eq)]
26enum Rule {
27 Julian1(i64),
29 ZeroBased(i64),
31 MonthWeekDay { m: i64, w: i64, d: i64 },
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct TzString {
39 pub std_abbr: String,
40 pub std_utoff: i32,
41 pub dst: Option<Dst>,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct Dst {
46 pub dst_abbr: String,
47 pub dst_utoff: i32,
48 start: Rule,
49 start_time: i64,
50 end: Rule,
51 end_time: i64,
52}
53
54pub fn parse(s: &str) -> Option<TzString> {
57 let b = s.as_bytes();
58 let mut i = 0usize;
59 let (std_abbr, ni) = parse_name(b, i)?;
60 i = ni;
61 let (std_posix, ni) = parse_offset(b, i)?; i = ni;
63 let std_utoff = -std_posix;
64 if i >= b.len() {
65 return Some(TzString {
66 std_abbr,
67 std_utoff,
68 dst: None,
69 });
70 }
71 let (dst_abbr, ni) = parse_name(b, i)?;
73 i = ni;
74 let (dst_utoff, ni) = if i < b.len() && b[i] != b',' {
76 let (p, ni) = parse_offset(b, i)?;
77 (-p, ni)
78 } else {
79 (std_utoff + 3600, i)
80 };
81 i = ni;
82 if i >= b.len() || b[i] != b',' {
84 return None; }
86 i += 1;
87 let (start, ni) = parse_rule(b, i)?;
88 i = ni;
89 let (start_time, ni) = parse_opt_time(b, i);
90 i = ni;
91 if i >= b.len() || b[i] != b',' {
92 return None;
93 }
94 i += 1;
95 let (end, ni) = parse_rule(b, i)?;
96 i = ni;
97 let (end_time, _ni) = parse_opt_time(b, i);
98 Some(TzString {
99 std_abbr,
100 std_utoff,
101 dst: Some(Dst {
102 dst_abbr,
103 dst_utoff,
104 start,
105 start_time,
106 end,
107 end_time,
108 }),
109 })
110}
111
112fn parse_name(b: &[u8], mut i: usize) -> Option<(String, usize)> {
113 if i >= b.len() {
114 return None;
115 }
116 if b[i] == b'<' {
117 i += 1;
118 let start = i;
119 while i < b.len() && b[i] != b'>' {
120 i += 1;
121 }
122 if i >= b.len() {
123 return None;
124 }
125 let name = std::str::from_utf8(&b[start..i]).ok()?.to_string();
126 Some((name, i + 1))
127 } else {
128 let start = i;
129 while i < b.len() && b[i].is_ascii_alphabetic() {
130 i += 1;
131 }
132 if i - start < 3 {
133 return None;
134 }
135 Some((std::str::from_utf8(&b[start..i]).ok()?.to_string(), i))
136 }
137}
138
139fn parse_offset(b: &[u8], mut i: usize) -> Option<(i32, usize)> {
141 let mut sign = 1i32;
142 if i < b.len() && (b[i] == b'+' || b[i] == b'-') {
143 if b[i] == b'-' {
144 sign = -1;
145 }
146 i += 1;
147 }
148 let (hh, ni) = parse_uint(b, i)?;
149 i = ni;
150 let mut secs = hh * 3600;
151 if i < b.len() && b[i] == b':' {
152 let (mm, ni) = parse_uint(b, i + 1)?;
153 i = ni;
154 secs += mm * 60;
155 if i < b.len() && b[i] == b':' {
156 let (ss, ni) = parse_uint(b, i + 1)?;
157 i = ni;
158 secs += ss;
159 }
160 }
161 Some((sign * secs as i32, i))
162}
163
164fn parse_opt_time(b: &[u8], i: usize) -> (i64, usize) {
166 if i < b.len() && b[i] == b'/' {
167 if let Some((t, ni)) = parse_signed_time(b, i + 1) {
168 return (t, ni);
169 }
170 }
171 (7200, i)
172}
173
174fn parse_signed_time(b: &[u8], mut i: usize) -> Option<(i64, usize)> {
175 let mut sign = 1i64;
176 if i < b.len() && (b[i] == b'+' || b[i] == b'-') {
177 if b[i] == b'-' {
178 sign = -1;
179 }
180 i += 1;
181 }
182 let (hh, ni) = parse_uint(b, i)?;
183 i = ni;
184 let mut secs = (hh as i64) * 3600;
185 if i < b.len() && b[i] == b':' {
186 let (mm, ni) = parse_uint(b, i + 1)?;
187 i = ni;
188 secs += (mm as i64) * 60;
189 if i < b.len() && b[i] == b':' {
190 let (ss, ni) = parse_uint(b, i + 1)?;
191 i = ni;
192 secs += ss as i64;
193 }
194 }
195 Some((sign * secs, i))
196}
197
198fn parse_rule(b: &[u8], i: usize) -> Option<(Rule, usize)> {
199 if i >= b.len() {
200 return None;
201 }
202 match b[i] {
203 b'J' => {
204 let (n, ni) = parse_uint(b, i + 1)?;
205 Some((Rule::Julian1(n as i64), ni))
206 }
207 b'M' => {
208 let (m, i1) = parse_uint(b, i + 1)?;
209 if b.get(i1) != Some(&b'.') {
210 return None;
211 }
212 let (w, i2) = parse_uint(b, i1 + 1)?;
213 if b.get(i2) != Some(&b'.') {
214 return None;
215 }
216 let (d, i3) = parse_uint(b, i2 + 1)?;
217 Some((
218 Rule::MonthWeekDay {
219 m: m as i64,
220 w: w as i64,
221 d: d as i64,
222 },
223 i3,
224 ))
225 }
226 _ => {
227 let (n, ni) = parse_uint(b, i)?;
228 Some((Rule::ZeroBased(n as i64), ni))
229 }
230 }
231}
232
233fn parse_uint(b: &[u8], mut i: usize) -> Option<(u64, usize)> {
234 let start = i;
235 let mut v = 0u64;
236 while i < b.len() && b[i].is_ascii_digit() {
237 v = v * 10 + (b[i] - b'0') as u64;
238 i += 1;
239 }
240 if i == start {
241 None
242 } else {
243 Some((v, i))
244 }
245}
246
247fn weekday(days: i64) -> i64 {
249 (days.rem_euclid(7) + 4).rem_euclid(7)
250}
251
252fn days_in_month(y: i64, m: i64) -> i64 {
253 let leap = (y % 4 == 0 && y % 100 != 0) || y % 400 == 0;
254 match m {
255 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
256 4 | 6 | 9 | 11 => 30,
257 2 => {
258 if leap {
259 29
260 } else {
261 28
262 }
263 }
264 _ => 30,
265 }
266}
267
268fn julian1_to_md(mut n: i64) -> (i64, i64) {
270 const ML: [i64; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
271 let mut m = 1;
272 while m <= 12 && n > ML[(m - 1) as usize] {
273 n -= ML[(m - 1) as usize];
274 m += 1;
275 }
276 (m, n)
277}
278
279fn rule_to_unix(r: &Rule, time: i64, year: i64, utoff_before: i32) -> i64 {
281 let (m, d) = match *r {
282 Rule::Julian1(n) => julian1_to_md(n),
283 Rule::ZeroBased(n) => {
284 let mut rem = n;
286 let mut mm = 1;
287 loop {
288 let dim = days_in_month(year, mm);
289 if rem < dim {
290 break;
291 }
292 rem -= dim;
293 mm += 1;
294 if mm > 12 {
295 mm = 12;
296 rem = days_in_month(year, 12) - 1;
297 break;
298 }
299 }
300 (mm, rem + 1)
301 }
302 Rule::MonthWeekDay { m, w, d } => {
303 let first = days_from_civil(year, m, 1);
304 let wd_first = weekday(first);
305 let mut dom = 1 + (d - wd_first).rem_euclid(7);
307 dom += (w - 1) * 7;
308 let dim = days_in_month(year, m);
309 while dom > dim {
310 dom -= 7; }
312 (m, dom)
313 }
314 };
315 let local = days_from_civil(year, m, d) * 86400 + time;
317 local - utoff_before as i64
318}
319
320impl TzString {
321 fn std_obs(&self) -> Observation {
322 Observation {
323 utoff: self.std_utoff,
324 is_dst: false,
325 abbr: self.std_abbr.clone(),
326 }
327 }
328 fn dst_obs(&self, d: &Dst) -> Observation {
329 Observation {
330 utoff: d.dst_utoff,
331 is_dst: true,
332 abbr: d.dst_abbr.clone(),
333 }
334 }
335
336 pub fn observe(&self, t: i64) -> Observation {
340 let Some(d) = &self.dst else {
341 return self.std_obs();
342 };
343 let (y, _, _) = civil_from_days((t + self.std_utoff as i64).div_euclid(86400));
345 let mut tr: Vec<(i64, bool)> = Vec::with_capacity(6);
347 for yr in (y - 1)..=(y + 1) {
348 tr.push((
349 rule_to_unix(&d.start, d.start_time, yr, self.std_utoff),
350 true,
351 ));
352 tr.push((rule_to_unix(&d.end, d.end_time, yr, d.dst_utoff), false));
353 }
354 tr.sort_by_key(|x| x.0);
355 let mut state_dst = None;
357 for (inst, becomes_dst) in &tr {
358 if *inst <= t {
359 state_dst = Some(*becomes_dst);
360 } else {
361 break;
362 }
363 }
364 let dst_now = match state_dst {
366 Some(s) => s,
367 None => !tr.first().map(|x| x.1).unwrap_or(false),
368 };
369 if dst_now {
370 self.dst_obs(d)
371 } else {
372 self.std_obs()
373 }
374 }
375}
376
377#[cfg(test)]
378mod tests {
379 use super::*;
380
381 #[test]
382 fn parses_est5edt() {
383 let z = parse("EST5EDT,M3.2.0,M11.1.0").unwrap();
384 assert_eq!(z.std_abbr, "EST");
385 assert_eq!(z.std_utoff, -5 * 3600);
386 let d = z.dst.as_ref().unwrap();
387 assert_eq!(d.dst_abbr, "EDT");
388 assert_eq!(d.dst_utoff, -4 * 3600); }
390
391 #[test]
392 fn est_winter_summer() {
393 let z = parse("EST5EDT,M3.2.0,M11.1.0").unwrap();
394 let jan = crate::civil::parse_iso_utc("2027-01-15T12:00:00Z").unwrap();
396 let jul = crate::civil::parse_iso_utc("2027-07-15T12:00:00Z").unwrap();
397 let a = z.observe(jan);
398 assert_eq!((a.utoff, a.is_dst, a.abbr.as_str()), (-18000, false, "EST"));
399 let b = z.observe(jul);
400 assert_eq!((b.utoff, b.is_dst, b.abbr.as_str()), (-14400, true, "EDT"));
401 }
402
403 #[test]
404 fn london_and_angle_names() {
405 let z = parse("GMT0BST,M3.5.0/1,M10.5.0").unwrap();
406 assert_eq!(z.std_utoff, 0);
407 assert_eq!(z.dst.as_ref().unwrap().dst_utoff, 3600);
408 let z2 = parse("<-03>3<-02>,M3.5.0/-2,M10.5.0/-1").unwrap();
410 assert_eq!(z2.std_abbr, "-03");
411 assert_eq!(z2.std_utoff, -3 * 3600);
412 assert_eq!(z2.dst.as_ref().unwrap().dst_utoff, -2 * 3600);
413 }
414
415 #[test]
416 fn fixed_zone_no_dst() {
417 let z = parse("UTC0").unwrap();
418 assert!(z.dst.is_none());
419 let o = z.observe(0);
420 assert_eq!((o.utoff, o.is_dst), (0, false));
421 }
422
423 #[test]
424 fn southern_hemisphere_wraps_year() {
425 let z = parse("AEST-10AEDT,M10.1.0,M4.1.0/3").unwrap();
427 let jan = crate::civil::parse_iso_utc("2030-01-15T00:00:00Z").unwrap(); let jul = crate::civil::parse_iso_utc("2030-07-15T00:00:00Z").unwrap(); assert!(z.observe(jan).is_dst);
430 assert!(!z.observe(jul).is_dst);
431 }
432}