1use arrow_schema::ArrowError;
8use chrono::{DateTime, FixedOffset, NaiveDateTime};
9pub use private::{Tz, TzOffset};
10
11fn parse_fixed_offset(tz: &str) -> Option<FixedOffset> {
13 let bytes = tz.as_bytes();
14
15 let mut values = match bytes.len() {
16 6 if bytes[3] == b':' => [bytes[1], bytes[2], bytes[4], bytes[5]],
18 5 => [bytes[1], bytes[2], bytes[3], bytes[4]],
20 3 => [bytes[1], bytes[2], b'0', b'0'],
22 _ => return None,
23 };
24 values.iter_mut().for_each(|x| *x = x.wrapping_sub(b'0'));
25 if values.iter().any(|x| *x > 9) {
26 return None;
27 }
28 let secs =
29 (values[0] * 10 + values[1]) as i32 * 60 * 60 + (values[2] * 10 + values[3]) as i32 * 60;
30
31 match bytes[0] {
32 b'+' => FixedOffset::east_opt(secs),
33 b'-' => FixedOffset::west_opt(secs),
34 _ => None,
35 }
36}
37
38#[cfg(feature = "chrono-tz")]
39mod private {
40 use super::*;
41 use chrono::offset::TimeZone;
42 use chrono::{LocalResult, NaiveDate, NaiveDateTime, Offset};
43 use std::fmt::Display;
44 use std::str::FromStr;
45
46 #[derive(Debug, Copy, Clone)]
48 pub struct TzOffset {
49 tz: Tz,
50 offset: FixedOffset,
51 }
52
53 impl std::fmt::Display for TzOffset {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 self.offset.fmt(f)
56 }
57 }
58
59 impl Offset for TzOffset {
60 fn fix(&self) -> FixedOffset {
61 self.offset
62 }
63 }
64
65 #[derive(Debug, Copy, Clone)]
67 pub struct Tz(TzInner);
68
69 #[derive(Debug, Copy, Clone)]
70 enum TzInner {
71 Timezone(chrono_tz::Tz),
72 Offset(FixedOffset),
73 }
74
75 impl FromStr for Tz {
76 type Err = ArrowError;
77
78 fn from_str(tz: &str) -> Result<Self, Self::Err> {
79 match parse_fixed_offset(tz) {
80 Some(offset) => Ok(Self(TzInner::Offset(offset))),
81 None => Ok(Self(TzInner::Timezone(tz.parse().map_err(|e| {
82 ArrowError::ParseError(format!("Invalid timezone \"{tz}\": {e}"))
83 })?))),
84 }
85 }
86 }
87
88 impl Display for Tz {
89 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90 match self.0 {
91 TzInner::Timezone(tz) => tz.fmt(f),
92 TzInner::Offset(offset) => offset.fmt(f),
93 }
94 }
95 }
96
97 macro_rules! tz {
98 ($s:ident, $tz:ident, $b:block) => {
99 match $s.0 {
100 TzInner::Timezone($tz) => $b,
101 TzInner::Offset($tz) => $b,
102 }
103 };
104 }
105
106 impl TimeZone for Tz {
107 type Offset = TzOffset;
108
109 fn from_offset(offset: &Self::Offset) -> Self {
110 offset.tz
111 }
112
113 fn offset_from_local_date(&self, local: &NaiveDate) -> LocalResult<Self::Offset> {
114 tz!(self, tz, {
115 tz.offset_from_local_date(local).map(|x| TzOffset {
116 tz: *self,
117 offset: x.fix(),
118 })
119 })
120 }
121
122 fn offset_from_local_datetime(&self, local: &NaiveDateTime) -> LocalResult<Self::Offset> {
123 tz!(self, tz, {
124 tz.offset_from_local_datetime(local).map(|x| TzOffset {
125 tz: *self,
126 offset: x.fix(),
127 })
128 })
129 }
130
131 fn offset_from_utc_date(&self, utc: &NaiveDate) -> Self::Offset {
132 tz!(self, tz, {
133 TzOffset {
134 tz: *self,
135 offset: tz.offset_from_utc_date(utc).fix(),
136 }
137 })
138 }
139
140 fn offset_from_utc_datetime(&self, utc: &NaiveDateTime) -> Self::Offset {
141 tz!(self, tz, {
142 TzOffset {
143 tz: *self,
144 offset: tz.offset_from_utc_datetime(utc).fix(),
145 }
146 })
147 }
148 }
149
150 #[inline]
151 pub(super) fn new_gap_info(local: &NaiveDateTime, tz: &Tz) -> Option<GapInfo> {
152 let TzInner::Timezone(ctz) = &tz.0 else {
153 return None;
154 };
155 chrono_tz::GapInfo::new(local, ctz).map(|gi| GapInfo {
156 begin: gi.begin.map(|(ndt, ctz)| {
157 (
158 ndt,
159 TzOffset {
160 tz: *tz,
161 offset: ctz.fix(),
162 },
163 )
164 }),
165 end: gi.end.map(|dt| dt.with_timezone(tz)),
166 })
167 }
168
169 #[cfg(test)]
170 mod tests {
171 use super::*;
172 use chrono::{Timelike, Utc};
173
174 #[test]
175 fn test_with_timezone() {
176 let vals = [
177 Utc.timestamp_millis_opt(37800000).unwrap(),
178 Utc.timestamp_millis_opt(86339000).unwrap(),
179 ];
180
181 assert_eq!(10, vals[0].hour());
182 assert_eq!(23, vals[1].hour());
183
184 let tz: Tz = "America/Los_Angeles".parse().unwrap();
185
186 assert_eq!(2, vals[0].with_timezone(&tz).hour());
187 assert_eq!(15, vals[1].with_timezone(&tz).hour());
188 }
189
190 #[test]
191 fn test_using_chrono_tz_and_utc_naive_date_time() {
192 let sydney_tz = "Australia/Sydney".to_string();
193 let tz: Tz = sydney_tz.parse().unwrap();
194 let sydney_offset_without_dst = FixedOffset::east_opt(10 * 60 * 60).unwrap();
195 let sydney_offset_with_dst = FixedOffset::east_opt(11 * 60 * 60).unwrap();
196 let utc_just_before_sydney_dst_ends = NaiveDate::from_ymd_opt(2021, 4, 3)
208 .unwrap()
209 .and_hms_nano_opt(15, 30, 0, 0)
210 .unwrap();
211 assert_eq!(
212 tz.offset_from_utc_datetime(&utc_just_before_sydney_dst_ends)
213 .fix(),
214 sydney_offset_with_dst
215 );
216 let utc_just_after_sydney_dst_ends = NaiveDate::from_ymd_opt(2021, 4, 3)
218 .unwrap()
219 .and_hms_nano_opt(16, 30, 0, 0)
220 .unwrap();
221 assert_eq!(
222 tz.offset_from_utc_datetime(&utc_just_after_sydney_dst_ends)
223 .fix(),
224 sydney_offset_without_dst
225 );
226 let utc_just_before_sydney_dst_starts = NaiveDate::from_ymd_opt(2021, 10, 2)
228 .unwrap()
229 .and_hms_nano_opt(15, 30, 0, 0)
230 .unwrap();
231 assert_eq!(
232 tz.offset_from_utc_datetime(&utc_just_before_sydney_dst_starts)
233 .fix(),
234 sydney_offset_without_dst
235 );
236 let utc_just_after_sydney_dst_starts = NaiveDate::from_ymd_opt(2022, 10, 2)
238 .unwrap()
239 .and_hms_nano_opt(16, 30, 0, 0)
240 .unwrap();
241 assert_eq!(
242 tz.offset_from_utc_datetime(&utc_just_after_sydney_dst_starts)
243 .fix(),
244 sydney_offset_with_dst
245 );
246 }
247
248 fn naive(y: i32, mo: u32, d: u32, h: u32, mi: u32) -> NaiveDateTime {
249 NaiveDate::from_ymd_opt(y, mo, d)
250 .unwrap()
251 .and_hms_opt(h, mi, 0)
252 .unwrap()
253 }
254
255 fn utc(y: i32, mo: u32, d: u32, h: u32, mi: u32) -> chrono::DateTime<Utc> {
256 Utc.with_ymd_and_hms(y, mo, d, h, mi, 0).unwrap()
257 }
258
259 #[test]
260 fn test_gap_info_not_in_gap() {
261 let tz: Tz = "America/New_York".parse().unwrap();
262 assert!(GapInfo::new(&naive(2021, 3, 14, 1, 59), &tz).is_none());
264 assert!(GapInfo::new(&naive(2021, 3, 14, 3, 0), &tz).is_none());
265 assert!(GapInfo::new(&naive(2021, 11, 7, 1, 30), &tz).is_none());
267 }
268
269 #[test]
270 fn test_gap_info_america_new_york() {
271 let tz: Tz = "America/New_York".parse().unwrap();
273 for local in [
274 naive(2021, 3, 14, 2, 0),
275 naive(2021, 3, 14, 2, 30),
276 naive(2021, 3, 14, 2, 59),
277 ] {
278 assert!(tz.from_local_datetime(&local).single().is_none());
279
280 let gap = GapInfo::new(&local, &tz).unwrap();
281 let (begin, begin_offset) = gap.begin.unwrap();
282 assert_eq!(begin, naive(2021, 3, 14, 2, 0));
283 assert_eq!(
284 begin_offset.fix(),
285 FixedOffset::west_opt(5 * 60 * 60).unwrap()
286 );
287
288 let end = gap.end.unwrap();
289 assert_eq!(end.naive_local(), naive(2021, 3, 14, 3, 0));
290 assert_eq!(
291 end.offset().fix(),
292 FixedOffset::west_opt(4 * 60 * 60).unwrap()
293 );
294 assert_eq!(end, utc(2021, 3, 14, 7, 0));
295 assert_eq!(end.timezone().to_string(), "America/New_York");
296 }
297 }
298
299 #[test]
300 fn test_gap_info_europe_london() {
301 let tz: Tz = "Europe/London".parse().unwrap();
303 let gap = GapInfo::new(&naive(2021, 3, 28, 1, 30), &tz).unwrap();
304
305 let (begin, begin_offset) = gap.begin.unwrap();
306 assert_eq!(begin, naive(2021, 3, 28, 1, 0));
307 assert_eq!(begin_offset.fix(), FixedOffset::east_opt(0).unwrap());
308
309 let end = gap.end.unwrap();
310 assert_eq!(end.naive_local(), naive(2021, 3, 28, 2, 0));
311 assert_eq!(end.offset().fix(), FixedOffset::east_opt(60 * 60).unwrap());
312 assert_eq!(end, utc(2021, 3, 28, 1, 0));
313 }
314
315 #[test]
316 fn test_gap_info_australia_sydney() {
317 let tz: Tz = "Australia/Sydney".parse().unwrap();
319 let gap = GapInfo::new(&naive(2021, 10, 3, 2, 30), &tz).unwrap();
320
321 let (begin, begin_offset) = gap.begin.unwrap();
322 assert_eq!(begin, naive(2021, 10, 3, 2, 0));
323 assert_eq!(
324 begin_offset.fix(),
325 FixedOffset::east_opt(10 * 60 * 60).unwrap()
326 );
327
328 let end = gap.end.unwrap();
329 assert_eq!(end.naive_local(), naive(2021, 10, 3, 3, 0));
330 assert_eq!(
331 end.offset().fix(),
332 FixedOffset::east_opt(11 * 60 * 60).unwrap()
333 );
334 assert_eq!(end, utc(2021, 10, 2, 16, 0));
335 }
336
337 #[test]
338 fn test_timezone_display() {
339 let test_cases = ["UTC", "America/Los_Angeles", "-08:00", "+05:30"];
340 for &case in &test_cases {
341 let tz: Tz = case.parse().unwrap();
342 assert_eq!(tz.to_string(), case);
343 }
344 }
345 }
346}
347
348#[cfg(not(feature = "chrono-tz"))]
349mod private {
350 use super::*;
351 use chrono::offset::TimeZone;
352 use chrono::{LocalResult, NaiveDate, NaiveDateTime, Offset};
353 use std::str::FromStr;
354
355 #[derive(Debug, Copy, Clone)]
357 pub struct TzOffset(FixedOffset);
358
359 impl std::fmt::Display for TzOffset {
360 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
361 self.0.fmt(f)
362 }
363 }
364
365 impl Offset for TzOffset {
366 fn fix(&self) -> FixedOffset {
367 self.0
368 }
369 }
370
371 #[derive(Debug, Copy, Clone)]
373 pub struct Tz(FixedOffset);
374
375 impl FromStr for Tz {
376 type Err = ArrowError;
377
378 fn from_str(tz: &str) -> Result<Self, Self::Err> {
379 let offset = parse_fixed_offset(tz).ok_or_else(|| {
380 ArrowError::ParseError(format!(
381 "Invalid timezone \"{tz}\": only offset based timezones supported without chrono-tz feature"
382 ))
383 })?;
384 Ok(Self(offset))
385 }
386 }
387
388 impl TimeZone for Tz {
389 type Offset = TzOffset;
390
391 fn from_offset(offset: &Self::Offset) -> Self {
392 Self(offset.0)
393 }
394
395 fn offset_from_local_date(&self, local: &NaiveDate) -> LocalResult<Self::Offset> {
396 self.0.offset_from_local_date(local).map(TzOffset)
397 }
398
399 fn offset_from_local_datetime(&self, local: &NaiveDateTime) -> LocalResult<Self::Offset> {
400 self.0.offset_from_local_datetime(local).map(TzOffset)
401 }
402
403 fn offset_from_utc_date(&self, utc: &NaiveDate) -> Self::Offset {
404 TzOffset(self.0.offset_from_utc_date(utc).fix())
405 }
406
407 fn offset_from_utc_datetime(&self, utc: &NaiveDateTime) -> Self::Offset {
408 TzOffset(self.0.offset_from_utc_datetime(utc).fix())
409 }
410 }
411
412 #[inline]
413 pub(super) fn new_gap_info(_local: &NaiveDateTime, _tz: &Tz) -> Option<GapInfo> {
414 None
416 }
417}
418
419#[derive(Debug, Clone, Copy)]
466pub struct GapInfo {
467 pub begin: Option<(NaiveDateTime, TzOffset)>,
476
477 pub end: Option<DateTime<Tz>>,
479}
480
481impl GapInfo {
482 pub fn new(local: &NaiveDateTime, tz: &Tz) -> Option<Self> {
490 private::new_gap_info(local, tz)
491 }
492}
493
494#[cfg(test)]
495mod tests {
496 use super::*;
497 use chrono::{NaiveDate, Offset, TimeZone};
498
499 #[test]
500 fn test_with_offset() {
501 let t = NaiveDate::from_ymd_opt(2000, 1, 1).unwrap();
502
503 let tz: Tz = "-00:00".parse().unwrap();
504 assert_eq!(tz.offset_from_utc_date(&t).fix().local_minus_utc(), 0);
505 let tz: Tz = "+00:00".parse().unwrap();
506 assert_eq!(tz.offset_from_utc_date(&t).fix().local_minus_utc(), 0);
507
508 let tz: Tz = "-10:00".parse().unwrap();
509 assert_eq!(
510 tz.offset_from_utc_date(&t).fix().local_minus_utc(),
511 -10 * 60 * 60
512 );
513 let tz: Tz = "+09:00".parse().unwrap();
514 assert_eq!(
515 tz.offset_from_utc_date(&t).fix().local_minus_utc(),
516 9 * 60 * 60
517 );
518
519 let tz = "+09".parse::<Tz>().unwrap();
520 assert_eq!(
521 tz.offset_from_utc_date(&t).fix().local_minus_utc(),
522 9 * 60 * 60
523 );
524
525 let tz = "+0900".parse::<Tz>().unwrap();
526 assert_eq!(
527 tz.offset_from_utc_date(&t).fix().local_minus_utc(),
528 9 * 60 * 60
529 );
530
531 let err = "+9:00".parse::<Tz>().unwrap_err().to_string();
532 assert!(err.contains("Invalid timezone"), "{}", err);
533 }
534
535 #[test]
536 fn test_gap_info_fixed_offset() {
537 let times = [
538 NaiveDate::from_ymd_opt(2000, 1, 1)
539 .unwrap()
540 .and_hms_opt(0, 0, 0)
541 .unwrap(),
542 NaiveDate::from_ymd_opt(2021, 3, 14)
544 .unwrap()
545 .and_hms_opt(2, 30, 0)
546 .unwrap(),
547 NaiveDate::from_ymd_opt(2021, 3, 28)
548 .unwrap()
549 .and_hms_opt(1, 30, 0)
550 .unwrap(),
551 ];
552
553 for tz in ["+00:00", "-05:00", "+01:00", "+09:30"] {
554 let tz: Tz = tz.parse().unwrap();
555 for t in × {
556 assert!(GapInfo::new(t, &tz).is_none(), "{tz:?} {t}");
557 }
558 }
559 }
560}