qsv_dateparser/
timezone.rs1use anyhow::{Result, anyhow};
2use chrono::offset::FixedOffset;
3
4#[inline]
10pub fn parse(s: &str) -> Result<FixedOffset> {
11 FixedOffset::east_opt(if s.contains(':') {
12 parse_offset_internal(s, colon_or_space, false)?
13 } else {
14 parse_offset_2822(s)?
15 })
16 .ok_or_else(|| anyhow!("input is out of range"))
17}
18
19#[inline]
20fn parse_offset_2822(s: &str) -> Result<i32> {
21 let upto = s
23 .as_bytes()
24 .iter()
25 .position(|&c| !c.is_ascii_alphabetic())
26 .unwrap_or(s.len());
27 if upto > 0 {
28 let name = &s[..upto];
29 let offset_hours = |o| Ok(o * 3600);
30 if equals(name, "gmt") || equals(name, "ut") || equals(name, "utc") {
31 offset_hours(0)
32 } else if equals(name, "edt") {
33 offset_hours(-4)
34 } else if equals(name, "est") || equals(name, "cdt") {
35 offset_hours(-5)
36 } else if equals(name, "cst") || equals(name, "mdt") {
37 offset_hours(-6)
38 } else if equals(name, "mst") || equals(name, "pdt") {
39 offset_hours(-7)
40 } else if equals(name, "pst") {
41 offset_hours(-8)
42 } else {
43 Ok(0) }
45 } else {
46 let offset = parse_offset_internal(s, |s| Ok(s), false)?;
47 Ok(offset)
48 }
49}
50
51#[inline]
52fn parse_offset_internal<F>(
53 mut s: &str,
54 mut consume_colon: F,
55 allow_missing_minutes: bool,
56) -> Result<i32>
57where
58 F: FnMut(&str) -> Result<&str>,
59{
60 let err_out_of_range = "input is out of range";
61 let err_invalid = "input contains invalid characters";
62 let err_too_short = "premature end of input";
63
64 let digits = |s: &str| -> Result<(u8, u8)> {
65 let b = s.as_bytes();
66 if b.len() < 2 {
67 Err(anyhow!(err_too_short))
68 } else {
69 Ok((b[0], b[1]))
70 }
71 };
72 let negative = match s.as_bytes().first() {
73 Some(&b'+') => false,
74 Some(&b'-') => true,
75 Some(_) => return Err(anyhow!(err_invalid)),
76 None => return Err(anyhow!(err_too_short)),
77 };
78 s = &s[1..];
79
80 let hours = match digits(s)? {
82 (h1 @ b'0'..=b'9', h2 @ b'0'..=b'9') => i32::from((h1 - b'0') * 10 + (h2 - b'0')),
83 _ => return Err(anyhow!(err_invalid)),
84 };
85 s = &s[2..];
86
87 s = consume_colon(s)?;
89
90 let minutes = match digits(s) {
93 Ok(ds) => match ds {
94 (m1 @ b'0'..=b'5', m2 @ b'0'..=b'9') => i32::from((m1 - b'0') * 10 + (m2 - b'0')),
95 (b'6'..=b'9', b'0'..=b'9') => return Err(anyhow!(err_out_of_range)),
96 _ => return Err(anyhow!(err_invalid)),
97 },
98 _ => {
99 if allow_missing_minutes {
100 0
101 } else {
102 return Err(anyhow!(err_too_short));
103 }
104 }
105 };
106
107 let seconds = hours * 3600 + minutes * 60;
108 Ok(if negative { -seconds } else { seconds })
109}
110
111#[inline]
114fn equals(s: &str, pattern: &str) -> bool {
115 let mut xs = s.as_bytes().iter().map(|&c| match c {
116 b'A'..=b'Z' => c + 32,
117 _ => c,
118 });
119 let mut ys = pattern.as_bytes().iter().copied();
120 loop {
121 match (xs.next(), ys.next()) {
122 (None, None) => return true,
123 (None, _) | (_, None) => return false,
124 (Some(x), Some(y)) if x != y => return false,
125 _ => (),
126 }
127 }
128}
129
130#[inline]
132fn colon_or_space(s: &str) -> Result<&str> {
133 Ok(s.trim_start_matches(|c: char| c == ':' || c.is_whitespace()))
134}
135
136#[cfg(test)]
137#[allow(deprecated)]
138mod tests {
139 use super::*;
140
141 #[test]
142 fn parse() {
143 let test_cases = [
144 ("-0800", FixedOffset::west(8 * 3600)),
145 ("+10:00", FixedOffset::east(10 * 3600)),
146 ("PST", FixedOffset::west(8 * 3600)),
147 ("PDT", FixedOffset::west(7 * 3600)),
148 ("UTC", FixedOffset::west(0)),
149 ("GMT", FixedOffset::west(0)),
150 ];
151
152 for &(input, want) in test_cases.iter() {
153 assert_eq!(super::parse(input).unwrap(), want, "parse/{}", input)
154 }
155 }
156}