1use core::fmt;
9
10use crate::error::Error;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum RuleKind {
15 Julian,
17 DayOfYear,
19 MonthWeekDay,
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub struct TransitionRule {
26 pub kind: RuleKind,
28 pub day: i32,
30 pub week: i32,
32 pub mon: i32,
34 pub time: i32,
36}
37
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub struct PosixTz<'a> {
41 pub std_abbrev: &'a str,
43 pub std_offset: i32,
45 pub dst_abbrev: &'a str,
47 pub dst_offset: i32,
49 pub start: TransitionRule,
51 pub end: TransitionRule,
53}
54
55impl<'a> PosixTz<'a> {
56 pub fn has_dst(&self) -> bool {
58 !self.dst_abbrev.is_empty()
59 }
60
61 pub fn lookup(&self, unix: i64) -> (&'a str, i32, bool) {
64 if !self.has_dst() {
65 return (self.std_abbrev, self.std_offset, false);
66 }
67
68 let (year, yday, sec) = unix_to_yday_sec(unix);
69 let year_sec = yday * 86400 + sec;
70
71 let start_sec = rule_to_year_sec(self.start, year, self.std_offset);
72 let end_sec = rule_to_year_sec(self.end, year, self.dst_offset);
73
74 let in_dst = if start_sec < end_sec {
75 year_sec >= start_sec && year_sec < end_sec
77 } else {
78 year_sec >= start_sec || year_sec < end_sec
80 };
81
82 if in_dst {
83 (self.dst_abbrev, self.dst_offset, true)
84 } else {
85 (self.std_abbrev, self.std_offset, false)
86 }
87 }
88
89 pub fn transitions_for_year(&self, year: i32) -> Option<(i64, i64)> {
92 if !self.has_dst() {
93 return None;
94 }
95 let year_start = year_to_unix(year);
96 let start_sec = rule_to_year_sec(self.start, year, self.std_offset);
97 let end_sec = rule_to_year_sec(self.end, year, self.dst_offset);
98 Some((year_start + start_sec as i64, year_start + end_sec as i64))
99 }
100}
101
102impl fmt::Display for PosixTz<'_> {
103 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
104 write_name(f, self.std_abbrev)?;
105 write_offset(f, -self.std_offset)?;
106
107 if !self.has_dst() {
108 return Ok(());
109 }
110
111 write_name(f, self.dst_abbrev)?;
112 if self.dst_offset != self.std_offset + 3600 {
113 write_offset(f, -self.dst_offset)?;
114 }
115
116 f.write_str(",")?;
117 write_rule(f, self.start)?;
118 f.write_str(",")?;
119 write_rule(f, self.end)
120 }
121}
122
123pub fn parse_posix_tz(s: &str) -> Result<PosixTz<'_>, Error> {
125 let (std_abbrev, rest) = parse_tz_name(s)?;
127 if std_abbrev.is_empty() {
128 return Err(Error::BadPosixTz("empty standard timezone name"));
129 }
130
131 let (off, rest) = parse_tz_offset(rest)?;
134 let std_offset = -off;
135
136 let mut p = PosixTz {
137 std_abbrev,
138 std_offset,
139 dst_abbrev: "",
140 dst_offset: 0,
141 start: DEFAULT_RULE,
142 end: DEFAULT_RULE,
143 };
144
145 if rest.is_empty() {
146 return Ok(p); }
148
149 let (dst_abbrev, rest) = parse_tz_name(rest)?;
151 if dst_abbrev.is_empty() {
152 return Err(Error::BadPosixTz("empty DST timezone name"));
153 }
154 p.dst_abbrev = dst_abbrev;
155
156 let rest = if !rest.is_empty() && !rest.starts_with(',') {
158 let (off, rest) = parse_tz_offset(rest)?;
159 p.dst_offset = -off;
160 rest
161 } else {
162 p.dst_offset = p.std_offset + 3600;
163 rest
164 };
165
166 if rest.is_empty() {
168 p.start = TransitionRule {
170 kind: RuleKind::MonthWeekDay,
171 mon: 3,
172 week: 2,
173 day: 0,
174 time: 7200,
175 };
176 p.end = TransitionRule {
177 kind: RuleKind::MonthWeekDay,
178 mon: 11,
179 week: 1,
180 day: 0,
181 time: 7200,
182 };
183 return Ok(p);
184 }
185
186 let rest = rest
187 .strip_prefix(',')
188 .ok_or(Error::BadPosixTz("expected ',' before transition rules"))?;
189
190 let (start, rest) = parse_tz_rule(rest)?;
191 p.start = start;
192
193 let rest = rest
194 .strip_prefix(',')
195 .ok_or(Error::BadPosixTz("expected ',' between transition rules"))?;
196
197 let (end, _rest) = parse_tz_rule(rest)?;
198 p.end = end;
199
200 Ok(p)
201}
202
203const DEFAULT_RULE: TransitionRule = TransitionRule {
204 kind: RuleKind::MonthWeekDay,
205 day: 0,
206 week: 0,
207 mon: 0,
208 time: 7200,
209};
210
211fn parse_tz_name(s: &str) -> Result<(&str, &str), Error> {
214 if s.is_empty() {
215 return Ok(("", ""));
216 }
217 let b = s.as_bytes();
218 if b[0] == b'<' {
219 let end = s
221 .find('>')
222 .ok_or(Error::BadPosixTz("unterminated '<' in TZ name"))?;
223 return Ok((&s[1..end], &s[end + 1..]));
224 }
225 let mut i = 0;
227 while i < b.len() && is_alpha(b[i]) {
228 i += 1;
229 }
230 Ok((&s[..i], &s[i..]))
231}
232
233fn parse_tz_offset(s: &str) -> Result<(i32, &str), Error> {
234 if s.is_empty() {
235 return Err(Error::BadPosixTz("expected offset"));
236 }
237 let mut rest = s;
238 let mut neg = false;
239 if let Some(r) = rest.strip_prefix('-') {
240 neg = true;
241 rest = r;
242 } else if let Some(r) = rest.strip_prefix('+') {
243 rest = r;
244 }
245
246 let (hours, mut rest) = parse_tz_num(rest, 0, 167)?;
247 let mut mins = 0;
248 let mut secs = 0;
249 if let Some(r) = rest.strip_prefix(':') {
250 let (m, r) = parse_tz_num(r, 0, 59)?;
251 mins = m;
252 rest = r;
253 if let Some(r) = rest.strip_prefix(':') {
254 let (sx, r) = parse_tz_num(r, 0, 59)?;
255 secs = sx;
256 rest = r;
257 }
258 }
259
260 let mut offset = hours * 3600 + mins * 60 + secs;
261 if neg {
262 offset = -offset;
263 }
264 Ok((offset, rest))
265}
266
267fn parse_tz_rule(s: &str) -> Result<(TransitionRule, &str), Error> {
268 if s.is_empty() {
269 return Err(Error::BadPosixTz("empty transition rule"));
270 }
271 let mut r = TransitionRule {
272 kind: RuleKind::DayOfYear,
273 day: 0,
274 week: 0,
275 mon: 0,
276 time: 7200,
277 };
278
279 let b = s.as_bytes();
280 let mut rest;
281 if b[0] == b'M' {
282 r.kind = RuleKind::MonthWeekDay;
284 let (mon, after) = parse_tz_num(&s[1..], 1, 12)?;
285 r.mon = mon;
286 rest = after
287 .strip_prefix('.')
288 .ok_or(Error::BadPosixTz("expected '.' after month in rule"))?;
289 let (week, after) = parse_tz_num(rest, 1, 5)?;
290 r.week = week;
291 rest = after
292 .strip_prefix('.')
293 .ok_or(Error::BadPosixTz("expected '.' after week in rule"))?;
294 let (day, after) = parse_tz_num(rest, 0, 6)?;
295 r.day = day;
296 rest = after;
297 } else if b[0] == b'J' {
298 r.kind = RuleKind::Julian;
300 let (day, after) = parse_tz_num(&s[1..], 1, 365)?;
301 r.day = day;
302 rest = after;
303 } else {
304 r.kind = RuleKind::DayOfYear;
306 let (day, after) = parse_tz_num(s, 0, 365)?;
307 r.day = day;
308 rest = after;
309 }
310
311 if let Some(after) = rest.strip_prefix('/') {
313 let (off, after) = parse_tz_offset(after)?;
314 r.time = off;
315 rest = after;
316 }
317
318 Ok((r, rest))
319}
320
321fn parse_tz_num(s: &str, min: i32, max: i32) -> Result<(i32, &str), Error> {
322 let b = s.as_bytes();
323 if b.is_empty() || !is_digit(b[0]) {
324 return Err(Error::BadPosixTz("expected digit"));
325 }
326 let mut n: i32 = 0;
327 let mut i = 0;
328 while i < b.len() && is_digit(b[i]) {
329 n = n * 10 + (b[i] - b'0') as i32;
330 i += 1;
331 }
332 if n < min || n > max {
333 return Err(Error::BadPosixTz("number out of range"));
334 }
335 Ok((n, &s[i..]))
336}
337
338fn is_alpha(c: u8) -> bool {
339 c.is_ascii_alphabetic()
340}
341
342fn is_digit(c: u8) -> bool {
343 c.is_ascii_digit()
344}
345
346fn is_leap_year(year: i32) -> bool {
349 year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
350}
351
352const DAYS_IN_MONTH: [i32; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
353
354fn year_to_unix(year: i32) -> i64 {
356 let y = year as i64 - 1970;
357 let mut days = 365 * y;
358
359 if year > 1970 {
361 days += (y + 1) / 4;
362 days -= (y + 69) / 100;
363 days += (y + 369) / 400;
364 } else if year < 1970 {
365 days += (y - 2) / 4;
366 days -= (y - 30) / 100;
367 days += (y - 30) / 400;
368 }
369
370 days * 86400
371}
372
373pub(crate) fn year_of(unix: i64) -> i32 {
375 unix_to_yday_sec(unix).0
376}
377
378fn unix_to_yday_sec(unix: i64) -> (i32, i32, i32) {
380 let mut unix = unix;
381 let mut sec = (unix % 86400) as i32;
382 if sec < 0 {
383 sec += 86400;
384 unix -= 86400;
385 }
386 let days = (unix / 86400) as i32;
387
388 let mut year = 1970 + days / 365;
390 loop {
391 let year_start = (year_to_unix(year) / 86400) as i32;
392 if year_start <= days {
393 let mut year_end = year_start + 365;
394 if is_leap_year(year) {
395 year_end += 1;
396 }
397 if days < year_end {
398 return (year, days - year_start, sec);
399 }
400 year += 1;
401 } else {
402 year -= 1;
403 }
404 }
405}
406
407fn rule_to_year_sec(r: TransitionRule, year: i32, offset: i32) -> i32 {
410 let leap = is_leap_year(year);
411
412 let yday = match r.kind {
413 RuleKind::Julian => {
414 let mut d = r.day - 1;
416 if leap && d >= 59 {
417 d += 1; }
419 d
420 }
421 RuleKind::DayOfYear => {
422 r.day
424 }
425 RuleKind::MonthWeekDay => {
426 let m = (r.mon - 1) as usize; let mut first_yday = 0;
431 for (i, &dim) in DAYS_IN_MONTH.iter().enumerate().take(m) {
432 first_yday += dim;
433 if i == 1 && leap {
434 first_yday += 1;
435 }
436 }
437
438 let jan1_wday = (((year_to_unix(year) / 86400) % 7 + 4 + 7 * 53) % 7) as i32;
441
442 let first_wday = (jan1_wday + first_yday) % 7;
444
445 let days_until = (r.day - first_wday + 7) % 7;
447
448 let mut y = first_yday + days_until + (r.week - 1) * 7;
450
451 let mut month_days = DAYS_IN_MONTH[m];
453 if m == 1 && leap {
454 month_days += 1;
455 }
456 while y - first_yday >= month_days {
457 y -= 7;
458 }
459 y
460 }
461 };
462
463 yday * 86400 + r.time - offset
466}
467
468fn write_name(f: &mut fmt::Formatter<'_>, name: &str) -> fmt::Result {
471 let needs_quote = name.bytes().any(|c| !is_alpha(c));
472 if needs_quote {
473 write!(f, "<{name}>")
474 } else {
475 f.write_str(name)
476 }
477}
478
479fn write_offset(f: &mut fmt::Formatter<'_>, posix_off: i32) -> fmt::Result {
480 let mut v = posix_off;
481 if v < 0 {
482 f.write_str("-")?;
483 v = -v;
484 }
485 let hours = v / 3600;
486 let mins = (v % 3600) / 60;
487 let secs = v % 60;
488
489 write!(f, "{hours}")?;
490 if mins != 0 || secs != 0 {
491 write!(f, ":{mins:02}")?;
492 if secs != 0 {
493 write!(f, ":{secs:02}")?;
494 }
495 }
496 Ok(())
497}
498
499fn write_rule(f: &mut fmt::Formatter<'_>, r: TransitionRule) -> fmt::Result {
500 match r.kind {
501 RuleKind::Julian => write!(f, "J{}", r.day)?,
502 RuleKind::DayOfYear => write!(f, "{}", r.day)?,
503 RuleKind::MonthWeekDay => write!(f, "M{}.{}.{}", r.mon, r.week, r.day)?,
504 }
505 if r.time != 7200 {
506 f.write_str("/")?;
507 write_offset(f, r.time)?;
508 }
509 Ok(())
510}