1use arrow_schema::ArrowError;
21use chrono::FixedOffset;
22pub use private::{Tz, TzOffset};
23
24fn parse_fixed_offset(tz: &str) -> Option<FixedOffset> {
26 let bytes = tz.as_bytes();
27
28 let mut values = match bytes.len() {
29 6 if bytes[3] == b':' => [bytes[1], bytes[2], bytes[4], bytes[5]],
31 5 => [bytes[1], bytes[2], bytes[3], bytes[4]],
33 3 => [bytes[1], bytes[2], b'0', b'0'],
35 _ => return None,
36 };
37 values.iter_mut().for_each(|x| *x = x.wrapping_sub(b'0'));
38 if values.iter().any(|x| *x > 9) {
39 return None;
40 }
41 let secs =
42 (values[0] * 10 + values[1]) as i32 * 60 * 60 + (values[2] * 10 + values[3]) as i32 * 60;
43
44 match bytes[0] {
45 b'+' => FixedOffset::east_opt(secs),
46 b'-' => FixedOffset::west_opt(secs),
47 _ => None,
48 }
49}
50
51#[cfg(feature = "chrono-tz")]
52mod private {
53 use super::*;
54 use chrono::offset::TimeZone;
55 use chrono::{LocalResult, NaiveDate, NaiveDateTime, Offset};
56 use std::str::FromStr;
57
58 #[derive(Debug, Copy, Clone)]
60 pub struct TzOffset {
61 tz: Tz,
62 offset: FixedOffset,
63 }
64
65 impl std::fmt::Display for TzOffset {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 self.offset.fmt(f)
68 }
69 }
70
71 impl Offset for TzOffset {
72 fn fix(&self) -> FixedOffset {
73 self.offset
74 }
75 }
76
77 #[derive(Debug, Copy, Clone)]
79 pub struct Tz(TzInner);
80
81 #[derive(Debug, Copy, Clone)]
82 enum TzInner {
83 Timezone(chrono_tz::Tz),
84 Offset(FixedOffset),
85 }
86
87 impl FromStr for Tz {
88 type Err = ArrowError;
89
90 fn from_str(tz: &str) -> Result<Self, Self::Err> {
91 match parse_fixed_offset(tz) {
92 Some(offset) => Ok(Self(TzInner::Offset(offset))),
93 None => Ok(Self(TzInner::Timezone(tz.parse().map_err(|e| {
94 ArrowError::ParseError(format!("Invalid timezone \"{tz}\": {e}"))
95 })?))),
96 }
97 }
98 }
99
100 macro_rules! tz {
101 ($s:ident, $tz:ident, $b:block) => {
102 match $s.0 {
103 TzInner::Timezone($tz) => $b,
104 TzInner::Offset($tz) => $b,
105 }
106 };
107 }
108
109 impl TimeZone for Tz {
110 type Offset = TzOffset;
111
112 fn from_offset(offset: &Self::Offset) -> Self {
113 offset.tz
114 }
115
116 fn offset_from_local_date(&self, local: &NaiveDate) -> LocalResult<Self::Offset> {
117 tz!(self, tz, {
118 tz.offset_from_local_date(local).map(|x| TzOffset {
119 tz: *self,
120 offset: x.fix(),
121 })
122 })
123 }
124
125 fn offset_from_local_datetime(&self, local: &NaiveDateTime) -> LocalResult<Self::Offset> {
126 tz!(self, tz, {
127 tz.offset_from_local_datetime(local).map(|x| TzOffset {
128 tz: *self,
129 offset: x.fix(),
130 })
131 })
132 }
133
134 fn offset_from_utc_date(&self, utc: &NaiveDate) -> Self::Offset {
135 tz!(self, tz, {
136 TzOffset {
137 tz: *self,
138 offset: tz.offset_from_utc_date(utc).fix(),
139 }
140 })
141 }
142
143 fn offset_from_utc_datetime(&self, utc: &NaiveDateTime) -> Self::Offset {
144 tz!(self, tz, {
145 TzOffset {
146 tz: *self,
147 offset: tz.offset_from_utc_datetime(utc).fix(),
148 }
149 })
150 }
151 }
152
153 #[cfg(test)]
154 mod tests {
155 use super::*;
156 use chrono::{Timelike, Utc};
157
158 #[test]
159 fn test_with_timezone() {
160 let vals = [
161 Utc.timestamp_millis_opt(37800000).unwrap(),
162 Utc.timestamp_millis_opt(86339000).unwrap(),
163 ];
164
165 assert_eq!(10, vals[0].hour());
166 assert_eq!(23, vals[1].hour());
167
168 let tz: Tz = "America/Los_Angeles".parse().unwrap();
169
170 assert_eq!(2, vals[0].with_timezone(&tz).hour());
171 assert_eq!(15, vals[1].with_timezone(&tz).hour());
172 }
173
174 #[test]
175 fn test_using_chrono_tz_and_utc_naive_date_time() {
176 let sydney_tz = "Australia/Sydney".to_string();
177 let tz: Tz = sydney_tz.parse().unwrap();
178 let sydney_offset_without_dst = FixedOffset::east_opt(10 * 60 * 60).unwrap();
179 let sydney_offset_with_dst = FixedOffset::east_opt(11 * 60 * 60).unwrap();
180 let utc_just_before_sydney_dst_ends = NaiveDate::from_ymd_opt(2021, 4, 3)
192 .unwrap()
193 .and_hms_nano_opt(15, 30, 0, 0)
194 .unwrap();
195 assert_eq!(
196 tz.offset_from_utc_datetime(&utc_just_before_sydney_dst_ends)
197 .fix(),
198 sydney_offset_with_dst
199 );
200 let utc_just_after_sydney_dst_ends = NaiveDate::from_ymd_opt(2021, 4, 3)
202 .unwrap()
203 .and_hms_nano_opt(16, 30, 0, 0)
204 .unwrap();
205 assert_eq!(
206 tz.offset_from_utc_datetime(&utc_just_after_sydney_dst_ends)
207 .fix(),
208 sydney_offset_without_dst
209 );
210 let utc_just_before_sydney_dst_starts = NaiveDate::from_ymd_opt(2021, 10, 2)
212 .unwrap()
213 .and_hms_nano_opt(15, 30, 0, 0)
214 .unwrap();
215 assert_eq!(
216 tz.offset_from_utc_datetime(&utc_just_before_sydney_dst_starts)
217 .fix(),
218 sydney_offset_without_dst
219 );
220 let utc_just_after_sydney_dst_starts = NaiveDate::from_ymd_opt(2022, 10, 2)
222 .unwrap()
223 .and_hms_nano_opt(16, 30, 0, 0)
224 .unwrap();
225 assert_eq!(
226 tz.offset_from_utc_datetime(&utc_just_after_sydney_dst_starts)
227 .fix(),
228 sydney_offset_with_dst
229 );
230 }
231 }
232}
233
234#[cfg(not(feature = "chrono-tz"))]
235mod private {
236 use super::*;
237 use chrono::offset::TimeZone;
238 use chrono::{LocalResult, NaiveDate, NaiveDateTime, Offset};
239 use std::str::FromStr;
240
241 #[derive(Debug, Copy, Clone)]
243 pub struct TzOffset(FixedOffset);
244
245 impl std::fmt::Display for TzOffset {
246 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
247 self.0.fmt(f)
248 }
249 }
250
251 impl Offset for TzOffset {
252 fn fix(&self) -> FixedOffset {
253 self.0
254 }
255 }
256
257 #[derive(Debug, Copy, Clone)]
259 pub struct Tz(FixedOffset);
260
261 impl FromStr for Tz {
262 type Err = ArrowError;
263
264 fn from_str(tz: &str) -> Result<Self, Self::Err> {
265 let offset = parse_fixed_offset(tz).ok_or_else(|| {
266 ArrowError::ParseError(format!(
267 "Invalid timezone \"{tz}\": only offset based timezones supported without chrono-tz feature"
268 ))
269 })?;
270 Ok(Self(offset))
271 }
272 }
273
274 impl TimeZone for Tz {
275 type Offset = TzOffset;
276
277 fn from_offset(offset: &Self::Offset) -> Self {
278 Self(offset.0)
279 }
280
281 fn offset_from_local_date(&self, local: &NaiveDate) -> LocalResult<Self::Offset> {
282 self.0.offset_from_local_date(local).map(TzOffset)
283 }
284
285 fn offset_from_local_datetime(&self, local: &NaiveDateTime) -> LocalResult<Self::Offset> {
286 self.0.offset_from_local_datetime(local).map(TzOffset)
287 }
288
289 fn offset_from_utc_date(&self, utc: &NaiveDate) -> Self::Offset {
290 TzOffset(self.0.offset_from_utc_date(utc).fix())
291 }
292
293 fn offset_from_utc_datetime(&self, utc: &NaiveDateTime) -> Self::Offset {
294 TzOffset(self.0.offset_from_utc_datetime(utc).fix())
295 }
296 }
297}
298
299#[cfg(test)]
300mod tests {
301 use super::*;
302 use chrono::{NaiveDate, Offset, TimeZone};
303
304 #[test]
305 fn test_with_offset() {
306 let t = NaiveDate::from_ymd_opt(2000, 1, 1).unwrap();
307
308 let tz: Tz = "-00:00".parse().unwrap();
309 assert_eq!(tz.offset_from_utc_date(&t).fix().local_minus_utc(), 0);
310 let tz: Tz = "+00:00".parse().unwrap();
311 assert_eq!(tz.offset_from_utc_date(&t).fix().local_minus_utc(), 0);
312
313 let tz: Tz = "-10:00".parse().unwrap();
314 assert_eq!(
315 tz.offset_from_utc_date(&t).fix().local_minus_utc(),
316 -10 * 60 * 60
317 );
318 let tz: Tz = "+09:00".parse().unwrap();
319 assert_eq!(
320 tz.offset_from_utc_date(&t).fix().local_minus_utc(),
321 9 * 60 * 60
322 );
323
324 let tz = "+09".parse::<Tz>().unwrap();
325 assert_eq!(
326 tz.offset_from_utc_date(&t).fix().local_minus_utc(),
327 9 * 60 * 60
328 );
329
330 let tz = "+0900".parse::<Tz>().unwrap();
331 assert_eq!(
332 tz.offset_from_utc_date(&t).fix().local_minus_utc(),
333 9 * 60 * 60
334 );
335
336 let err = "+9:00".parse::<Tz>().unwrap_err().to_string();
337 assert!(err.contains("Invalid timezone"), "{}", err);
338 }
339}