time_tz/
lib.rs

1// Copyright (c) 2022, Yuri6037
2//
3// All rights reserved.
4//
5// Redistribution and use in source and binary forms, with or without modification,
6// are permitted provided that the following conditions are met:
7//
8// * Redistributions of source code must retain the above copyright notice,
9// this list of conditions and the following disclaimer.
10// * Redistributions in binary form must reproduce the above copyright notice,
11// this list of conditions and the following disclaimer in the documentation
12// and/or other materials provided with the distribution.
13// * Neither the name of time-tz nor the names of its contributors
14// may be used to endorse or promote products derived from this software
15// without specific prior written permission.
16//
17// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
18// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
19// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
20// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
21// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
22// EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
23// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
24// PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
25// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
26// NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
29//! This provides traits and utilities to work with timezones to time-rs and additionally has an
30//! implementation of IANA timezone database. To disable the integrated IANA/windows databases,
31//! one can simply remove the `db` default feature.
32
33// See https://doc.rust-lang.org/beta/unstable-book/language-features/doc-cfg.html & https://github.com/rust-lang/rust/pull/89596
34#![cfg_attr(docsrs, feature(doc_auto_cfg))]
35
36use time::{OffsetDateTime, PrimitiveDateTime};
37
38mod sealing {
39    pub trait OffsetDateTimeExt {}
40    pub trait PrimitiveDateTimeExt {}
41
42    impl OffsetDateTimeExt for time::OffsetDateTime {}
43    impl PrimitiveDateTimeExt for time::PrimitiveDateTime {}
44}
45
46// This trait is sealed and is only implemented in this library.
47pub trait OffsetDateTimeExt: sealing::OffsetDateTimeExt {
48    /// Converts this OffsetDateTime to a different TimeZone.
49    fn to_timezone<T: TimeZone>(&self, tz: &T) -> OffsetDateTime;
50}
51
52/// This trait is sealed and is only implemented in this library.
53pub trait PrimitiveDateTimeExt: sealing::PrimitiveDateTimeExt {
54    /// Creates a new OffsetDateTime from a PrimitiveDateTime by assigning the main offset of the
55    /// target timezone.
56    ///
57    /// *This assumes the PrimitiveDateTime is already in the target timezone.*
58    ///
59    /// # Arguments
60    ///
61    /// * `tz`: the target timezone.
62    ///
63    /// returns: `OffsetResult<OffsetDateTime>`
64    fn assume_timezone<T: TimeZone>(&self, tz: &T) -> OffsetResult<OffsetDateTime>;
65
66    /// Creates a new OffsetDateTime with the proper offset in the given timezone.
67    ///
68    /// *This assumes the PrimitiveDateTime is in UTC offset.*
69    ///
70    /// # Arguments
71    ///
72    /// * `tz`: the target timezone.
73    ///
74    /// returns: OffsetDateTime
75    fn assume_timezone_utc<T: TimeZone>(&self, tz: &T) -> OffsetDateTime;
76}
77
78impl PrimitiveDateTimeExt for PrimitiveDateTime {
79    fn assume_timezone<T: TimeZone>(&self, tz: &T) -> OffsetResult<OffsetDateTime> {
80        match tz.get_offset_local(&self.assume_utc()) {
81            OffsetResult::Some(a) => OffsetResult::Some(self.assume_offset(a.to_utc())),
82            OffsetResult::Ambiguous(a, b) => OffsetResult::Ambiguous(
83                self.assume_offset(a.to_utc()),
84                self.assume_offset(b.to_utc()),
85            ),
86            OffsetResult::None => OffsetResult::None,
87        }
88    }
89
90    fn assume_timezone_utc<T: TimeZone>(&self, tz: &T) -> OffsetDateTime {
91        let offset = tz.get_offset_utc(&self.assume_utc());
92        self.assume_offset(offset.to_utc())
93    }
94}
95
96impl OffsetDateTimeExt for OffsetDateTime {
97    fn to_timezone<T: TimeZone>(&self, tz: &T) -> OffsetDateTime {
98        let offset = tz.get_offset_utc(self);
99        self.to_offset(offset.to_utc())
100    }
101}
102
103mod binary_search;
104mod interface;
105mod timezone_impl;
106
107#[cfg(feature = "db")]
108pub mod timezones;
109
110pub use interface::*;
111
112#[cfg(feature = "system")]
113pub mod system;
114
115#[cfg(feature = "posix-tz")]
116pub mod posix_tz;
117
118#[cfg(feature = "db")]
119pub use timezone_impl::Tz;
120
121#[cfg(test)]
122mod tests {
123    use crate::timezones;
124    use crate::Offset;
125    use crate::OffsetDateTimeExt;
126    use crate::PrimitiveDateTimeExt;
127    use crate::TimeZone;
128    use time::macros::{datetime, offset};
129    use time::OffsetDateTime;
130
131    #[test]
132    fn names() {
133        //This test verifies that windows timezone names work fine.
134        let shanghai = timezones::get_by_name("Asia/Shanghai");
135        let china = timezones::get_by_name("China Standard Time");
136        assert!(shanghai.is_some());
137        assert!(china.is_some());
138        assert_eq!(shanghai, china);
139    }
140
141    #[test]
142    fn find() {
143        let zones_iana = timezones::find_by_name("Asia");
144        //let zones_win = timezones::find_by_name("China Standard Time");
145        assert!(zones_iana.len() > 1);
146        //assert!(zones_win.len() > 1);
147    }
148
149    #[test]
150    fn offsets_and_name() {
151        let tz = timezones::db::europe::LONDON;
152        assert_eq!(tz.name(), "Europe/London");
153        let offset = tz.get_offset_utc(&OffsetDateTime::now_utc());
154        assert!(!offset.name().is_empty());
155    }
156
157    #[test]
158    fn london_to_berlin() {
159        let dt = datetime!(2016-10-8 17:0:0).assume_timezone_utc(timezones::db::europe::LONDON);
160        let converted = dt.to_timezone(timezones::db::europe::BERLIN);
161        let expected =
162            datetime!(2016-10-8 18:0:0).assume_timezone_utc(timezones::db::europe::BERLIN);
163        assert_eq!(converted, expected);
164    }
165
166    #[test]
167    fn dst() {
168        let london = timezones::db::europe::LONDON;
169        let odt1 = datetime!(2021-01-01 12:0:0 UTC);
170        assert_eq!(odt1.to_timezone(london), datetime!(2021-01-01 12:0:0 +0));
171        let odt2 = datetime!(2021-07-01 12:0:0 UTC);
172        // Adding offset to datetime call causes VERY surprising result: hours randomly changes!!
173        // When using UTC followed by .to_offset no surprising result.
174        assert_eq!(
175            odt2.to_timezone(london),
176            datetime!(2021-07-01 12:0:0 UTC).to_offset(offset!(+1))
177        );
178    }
179
180    #[test]
181    fn handles_forward_changeover() {
182        assert_eq!(
183            datetime!(2022-03-27 01:30)
184                .assume_timezone(timezones::db::CET)
185                .unwrap(),
186            datetime!(2022-03-27 01:30 +01:00)
187        );
188    }
189
190    #[test]
191    fn handles_after_changeover() {
192        assert_eq!(
193            datetime!(2022-03-27 03:30)
194                .assume_timezone(timezones::db::CET)
195                .unwrap(),
196            datetime!(2022-03-27 03:30 +02:00)
197        );
198    }
199
200    #[test]
201    fn handles_broken_time() {
202        assert!(datetime!(2022-03-27 02:30)
203            .assume_timezone(timezones::db::CET)
204            .is_none());
205    }
206
207    #[test]
208    fn handles_backward_changeover() {
209        // During backward changeover, the hour between 02:00 and 03:00 occurs twice, so either answer is correct
210        assert_eq!(
211            datetime!(2022-10-30 02:30)
212                .assume_timezone(timezones::db::CET)
213                .unwrap_first(),
214            datetime!(2022-10-30 02:30 +02:00)
215        );
216        assert_eq!(
217            datetime!(2022-10-30 02:30)
218                .assume_timezone(timezones::db::CET)
219                .unwrap_second(),
220            datetime!(2022-10-30 02:30 +01:00)
221        );
222    }
223}