## Coverage Report

#### Created: 2025-08-26 07:03

next uncovered line (L), next uncovered region (R), next uncovered branch (B)

    /Users/andrewlamb/Software/arrow-rs/arrow-array/src/timezone.rs

    Line
    Count
    Source
    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
    20
    use arrow_schema::ArrowError;
    21
    use chrono::FixedOffset;
    22
    pub use private::{Tz, TzOffset};
    23
    24
    /// Parses a fixed offset of the form "+09:00", "-09" or "+0930"
    25
    0
    fn parse_fixed_offset(tz: &str) -> Option<FixedOffset> {
    26
    0
        let bytes = tz.as_bytes();
    27
    28
    0
        let mut values = match bytes.len() {
    29
            // [+-]XX:XX
    30
    0
            6 if bytes[3] == b':' => [bytes[1], bytes[2], bytes[4], bytes[5]],
    31
            // [+-]XXXX
    32
    0
            5 => [bytes[1], bytes[2], bytes[3], bytes[4]],
    33
            // [+-]XX
    34
    0
            3 => [bytes[1], bytes[2], b'0', b'0'],
    35
    0
            _ => return None,
    36
        };
    37
    0
        values.iter_mut().for_each(|x| *x = x.wrapping_sub(b'0'));
    38
    0
        if values.iter().any(|x| *x > 9) {
    39
    0
            return None;
    40
    0
        }
    41
    0
        let secs =
    42
    0
            (values[0] * 10 + values[1]) as i32 * 60 * 60 + (values[2] * 10 + values[3]) as i32 * 60;
    43
    44
    0
        match bytes[0] {
    45
    0
            b'+' => FixedOffset::east_opt(secs),
    46
    0
            b'-' => FixedOffset::west_opt(secs),
    47
    0
            _ => None,
    48
        }
    49
    0
    }
    50
    51
    #[cfg(feature = "chrono-tz")]
    52
    mod 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"))]
    235
    mod 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
    0
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
    247
    0
                self.0.fmt(f)
    248
    0
            }
    249
        }
    250
    251
        impl Offset for TzOffset {
    252
    0
            fn fix(&self) -> FixedOffset {
    253
    0
                self.0
    254
    0
            }
    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
    0
            fn from_str(tz: &str) -> Result<Self, Self::Err> {
    265
    0
                let offset = parse_fixed_offset(tz).ok_or_else(|| {
    266
    0
                    ArrowError::ParseError(format!(
    267
    0
                        "Invalid timezone \"{tz}\": only offset based timezones supported without chrono-tz feature"
    268
    0
                    ))
    269
    0
                })?;
    270
    0
                Ok(Self(offset))
    271
    0
            }
    272
        }
    273
    274
        impl TimeZone for Tz {
    275
            type Offset = TzOffset;
    276
    277
    0
            fn from_offset(offset: &Self::Offset) -> Self {
    278
    0
                Self(offset.0)
    279
    0
            }
    280
    281
    0
            fn offset_from_local_date(&self, local: &NaiveDate) -> LocalResult<Self::Offset> {
    282
    0
                self.0.offset_from_local_date(local).map(TzOffset)
    283
    0
            }
    284
    285
    0
            fn offset_from_local_datetime(&self, local: &NaiveDateTime) -> LocalResult<Self::Offset> {
    286
    0
                self.0.offset_from_local_datetime(local).map(TzOffset)
    287
    0
            }
    288
    289
    0
            fn offset_from_utc_date(&self, utc: &NaiveDate) -> Self::Offset {
    290
    0
                TzOffset(self.0.offset_from_utc_date(utc).fix())
    291
    0
            }
    292
    293
    0
            fn offset_from_utc_datetime(&self, utc: &NaiveDateTime) -> Self::Offset {
    294
    0
                TzOffset(self.0.offset_from_utc_datetime(utc).fix())
    295
    0
            }
    296
        }
    297
    }
    298
    299
    #[cfg(test)]
    300
    mod 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
    }