arrow_array/
timezone.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Timezone for timestamp arrays
19
20use arrow_schema::ArrowError;
21use chrono::FixedOffset;
22pub use private::{Tz, TzOffset};
23
24/// Parses a fixed offset of the form "+09:00", "-09" or "+0930"
25fn parse_fixed_offset(tz: &str) -> Option<FixedOffset> {
26    let bytes = tz.as_bytes();
27
28    let mut values = match bytes.len() {
29        // [+-]XX:XX
30        6 if bytes[3] == b':' => [bytes[1], bytes[2], bytes[4], bytes[5]],
31        // [+-]XXXX
32        5 => [bytes[1], bytes[2], bytes[3], bytes[4]],
33        // [+-]XX
34        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    /// An [`Offset`] for [`Tz`]
59    #[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    /// An Arrow [`TimeZone`]
78    #[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            // Daylight savings ends
181            // When local daylight time was about to reach
182            // Sunday, 4 April 2021, 3:00:00 am clocks were turned backward 1 hour to
183            // Sunday, 4 April 2021, 2:00:00 am local standard time instead.
184
185            // Daylight savings starts
186            // When local standard time was about to reach
187            // Sunday, 3 October 2021, 2:00:00 am clocks were turned forward 1 hour to
188            // Sunday, 3 October 2021, 3:00:00 am local daylight time instead.
189
190            // Sydney 2021-04-04T02:30:00+11:00 is 2021-04-03T15:30:00Z
191            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            // Sydney 2021-04-04T02:30:00+10:00 is 2021-04-03T16:30:00Z
201            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            // Sydney 2021-10-03T01:30:00+10:00 is 2021-10-02T15:30:00Z
211            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            // Sydney 2021-04-04T03:30:00+11:00 is 2021-10-02T16:30:00Z
221            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    /// An [`Offset`] for [`Tz`]
242    #[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    /// An Arrow [`TimeZone`]
258    #[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}