chrono_tz/
lib.rs

1//! # Chrono-TZ
2//!
3//! `Chrono-TZ` is a library that provides implementors of the
4//! [`TimeZone`][timezone] trait for [`chrono`][chrono]. The
5//! impls are generated by a build script using the [`IANA database`][iana]
6//! and [`zoneinfo_parse`][zoneinfo_parse].
7//!
8//! [chrono]: https://github.com/lifthrasiir/rust-chrono
9//! [timezone]: https://lifthrasiir.github.io/rust-chrono/chrono/offset/trait.TimeZone.html
10//! [iana]: http://www.iana.org/time-zones
11//! [zoneinfo_parse]: https://github.com/rust-datetime/zoneinfo-parse
12//!
13//! ## Examples
14//!
15//! Create a time in one timezone and convert it to UTC
16//!
17//! ```
18//! # extern crate chrono;
19//! # extern crate chrono_tz;
20//! use chrono::{TimeZone, Utc};
21//! use chrono_tz::US::Pacific;
22//!
23//! # fn main() {
24//! let pacific_time = Pacific.ymd(1990, 5, 6).and_hms(12, 30, 45);
25//! let utc_time = pacific_time.with_timezone(&Utc);
26//! assert_eq!(utc_time, Utc.ymd(1990, 5, 6).and_hms(19, 30, 45));
27//! # }
28//! ```
29//!
30//! Create a naive datetime and convert it to a timezone-aware datetime
31//!
32//! ```
33//! # extern crate chrono;
34//! # extern crate chrono_tz;
35//! use chrono::{TimeZone, NaiveDate};
36//! use chrono_tz::Africa::Johannesburg;
37//!
38//! # fn main() {
39//! let naive_dt = NaiveDate::from_ymd(2038, 1, 19).and_hms(3, 14, 08);
40//! let tz_aware = Johannesburg.from_local_datetime(&naive_dt).unwrap();
41//! assert_eq!(tz_aware.to_string(), "2038-01-19 03:14:08 SAST");
42//! # }
43//! ```
44//!
45//! London and New York change their clocks on different days in March
46//! so only have a 4-hour difference on certain days.
47//!
48//! ```
49//! # extern crate chrono;
50//! # extern crate chrono_tz;
51//! use chrono::TimeZone;
52//! use chrono_tz::Europe::London;
53//! use chrono_tz::America::New_York;
54//!
55//! # fn main() {
56//! let london_time = London.ymd(2016, 3, 18).and_hms(3, 0, 0);
57//! let ny_time = london_time.with_timezone(&New_York);
58//! assert_eq!(ny_time, New_York.ymd(2016, 3, 17).and_hms(23, 0, 0));
59//! # }
60//! ```
61//!
62//! Adding 24 hours across a daylight savings change causes a change
63//! in local time
64//!
65//! ```
66//! # extern crate chrono;
67//! # extern crate chrono_tz;
68//! use chrono::{TimeZone, Duration};
69//! use chrono_tz::Europe::London;
70//!
71//! # fn main() {
72//! let dt = London.ymd(2016, 10, 29).and_hms(12, 0, 0);
73//! let later = dt + Duration::hours(24);
74//! assert_eq!(later, London.ymd(2016, 10, 30).and_hms(11, 0, 0));
75//! # }
76//! ```
77//!
78//! And of course you can always convert a local time to a unix timestamp
79//!
80//! ```
81//! # extern crate chrono;
82//! # extern crate chrono_tz;
83//! use chrono::TimeZone;
84//! use chrono_tz::Asia::Kolkata;
85//!
86//! # fn main() {
87//! let dt = Kolkata.ymd(2000, 1, 1).and_hms(0, 0, 0);
88//! let timestamp = dt.timestamp();
89//! assert_eq!(timestamp, 946665000);
90//! # }
91//! ```
92//!
93//! Pretty-printing a string will use the correct abbreviation for the timezone
94//!
95//! ```
96//! # extern crate chrono;
97//! # extern crate chrono_tz;
98//! use chrono::TimeZone;
99//! use chrono_tz::Europe::London;
100//!
101//! # fn main() {
102//! let dt = London.ymd(2016, 5, 10).and_hms(12, 0, 0);
103//! assert_eq!(dt.to_string(), "2016-05-10 12:00:00 BST");
104//! assert_eq!(dt.to_rfc3339(), "2016-05-10T12:00:00+01:00");
105//! # }
106//! ```
107//!
108//! You can convert a timezone string to a timezone using the `FromStr` trait
109//!
110//! ```
111//! # extern crate chrono;
112//! # extern crate chrono_tz;
113//! use chrono::TimeZone;
114//! use chrono_tz::Tz;
115//! use chrono_tz::UTC;
116//!
117//! # fn main() {
118//! let tz: Tz = "Antarctica/South_Pole".parse().unwrap();
119//! let dt = tz.ymd(2016, 10, 22).and_hms(12, 0, 0);
120//! let utc = dt.with_timezone(&UTC);
121//! assert_eq!(utc.to_string(), "2016-10-21 23:00:00 UTC");
122//! # }
123//! ```
124//!
125//! If you need to iterate over all variants you can use the `TZ_VARIANTS` array
126//! ```
127//! use chrono_tz::{TZ_VARIANTS, Tz};
128//! assert!(TZ_VARIANTS.iter().any(|v| *v == Tz::UTC));
129//! ```
130
131#![cfg_attr(not(any(feature = "std", test)), no_std)]
132#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))]
133
134#[cfg(feature = "serde")]
135mod serde;
136
137mod binary_search;
138#[cfg(not(any(feature = "case-insensitive", feature = "filter-by-regex")))]
139use prebuilt::directory;
140mod prebuilt;
141#[cfg(any(feature = "case-insensitive", feature = "filter-by-regex"))]
142mod directory {
143    #![allow(
144        dead_code,
145        non_camel_case_types,
146        non_snake_case,
147        non_upper_case_globals
148    )]
149    include!(concat!(env!("OUT_DIR"), "/directory.rs"));
150}
151mod timezone_impl;
152#[cfg(not(any(feature = "case-insensitive", feature = "filter-by-regex")))]
153use prebuilt::timezones;
154#[cfg(any(feature = "case-insensitive", feature = "filter-by-regex"))]
155mod timezones {
156    #![allow(non_camel_case_types, clippy::unreadable_literal)]
157    include!(concat!(env!("OUT_DIR"), "/timezones.rs"));
158}
159
160pub use crate::timezone_impl::{GapInfo, OffsetComponents, OffsetName, TzOffset};
161pub use directory::*;
162pub use timezones::ParseError;
163pub use timezones::Tz;
164pub use timezones::TZ_VARIANTS;
165pub use IANA_TZDB_VERSION;
166
167#[cfg(test)]
168mod tests {
169    use super::Africa::Addis_Ababa;
170    use super::America::Danmarkshavn;
171    use super::America::Scoresbysund;
172    use super::Antarctica::Casey;
173    use super::Asia::Dhaka;
174    use super::Australia::Adelaide;
175    use super::Europe::Berlin;
176    use super::Europe::London;
177    use super::Europe::Moscow;
178    use super::Europe::Vilnius;
179    use super::Europe::Warsaw;
180    use super::GapInfo;
181    use super::Pacific::Apia;
182    use super::Pacific::Noumea;
183    use super::Pacific::Tahiti;
184    use super::Tz;
185    use super::IANA_TZDB_VERSION;
186    use super::US::Eastern;
187    use super::UTC;
188    use chrono::NaiveDateTime;
189    use chrono::{Duration, NaiveDate, TimeZone};
190
191    #[test]
192    fn london_to_berlin() {
193        let dt = London.with_ymd_and_hms(2016, 10, 8, 17, 0, 0).unwrap();
194        let converted = dt.with_timezone(&Berlin);
195        let expected = Berlin.with_ymd_and_hms(2016, 10, 8, 18, 0, 0).unwrap();
196        assert_eq!(converted, expected);
197    }
198
199    #[test]
200    fn us_eastern_dst_commutativity() {
201        let dt = UTC.with_ymd_and_hms(2002, 4, 7, 7, 0, 0).unwrap();
202        for days in -420..720 {
203            let dt1 = (dt + Duration::days(days)).with_timezone(&Eastern);
204            let dt2 = dt.with_timezone(&Eastern) + Duration::days(days);
205            assert_eq!(dt1, dt2);
206        }
207    }
208
209    #[test]
210    fn test_addition_across_dst_boundary() {
211        use chrono::TimeZone;
212        let two_hours = Duration::hours(2);
213        let edt = Eastern.with_ymd_and_hms(2019, 11, 3, 0, 0, 0).unwrap();
214        let est = edt + two_hours;
215
216        assert_eq!(edt.to_string(), "2019-11-03 00:00:00 EDT".to_string());
217        assert_eq!(est.to_string(), "2019-11-03 01:00:00 EST".to_string());
218        assert_eq!(est.timestamp(), edt.timestamp() + two_hours.num_seconds());
219    }
220
221    #[test]
222    fn warsaw_tz_name() {
223        let dt = UTC.with_ymd_and_hms(1915, 8, 4, 22, 35, 59).unwrap();
224        assert_eq!(dt.with_timezone(&Warsaw).format("%Z").to_string(), "WMT");
225        let dt = dt + Duration::seconds(1);
226        assert_eq!(dt.with_timezone(&Warsaw).format("%Z").to_string(), "CET");
227    }
228
229    #[test]
230    fn vilnius_utc_offset() {
231        let dt = UTC
232            .with_ymd_and_hms(1916, 12, 31, 22, 35, 59)
233            .unwrap()
234            .with_timezone(&Vilnius);
235        assert_eq!(
236            dt,
237            Vilnius.with_ymd_and_hms(1916, 12, 31, 23, 59, 59).unwrap()
238        );
239        let dt = dt + Duration::seconds(1);
240        assert_eq!(dt, Vilnius.with_ymd_and_hms(1917, 1, 1, 0, 11, 36).unwrap());
241    }
242
243    #[test]
244    fn victorian_times() {
245        let dt = UTC
246            .with_ymd_and_hms(1847, 12, 1, 0, 1, 14)
247            .unwrap()
248            .with_timezone(&London);
249        assert_eq!(
250            dt,
251            London.with_ymd_and_hms(1847, 11, 30, 23, 59, 59).unwrap()
252        );
253        let dt = dt + Duration::seconds(1);
254        assert_eq!(dt, London.with_ymd_and_hms(1847, 12, 1, 0, 1, 15).unwrap());
255    }
256
257    #[test]
258    fn london_dst() {
259        let dt = London.with_ymd_and_hms(2016, 3, 10, 5, 0, 0).unwrap();
260        let later = dt + Duration::days(180);
261        let expected = London.with_ymd_and_hms(2016, 9, 6, 6, 0, 0).unwrap();
262        assert_eq!(later, expected);
263    }
264
265    #[test]
266    fn international_date_line_change() {
267        let dt = UTC
268            .with_ymd_and_hms(2011, 12, 30, 9, 59, 59)
269            .unwrap()
270            .with_timezone(&Apia);
271        assert_eq!(dt, Apia.with_ymd_and_hms(2011, 12, 29, 23, 59, 59).unwrap());
272        let dt = dt + Duration::seconds(1);
273        assert_eq!(dt, Apia.with_ymd_and_hms(2011, 12, 31, 0, 0, 0).unwrap());
274    }
275
276    #[test]
277    fn negative_offset_with_minutes_and_seconds() {
278        let dt = UTC
279            .with_ymd_and_hms(1900, 1, 1, 12, 0, 0)
280            .unwrap()
281            .with_timezone(&Danmarkshavn);
282        assert_eq!(
283            dt,
284            Danmarkshavn
285                .with_ymd_and_hms(1900, 1, 1, 10, 45, 20)
286                .unwrap()
287        );
288    }
289
290    #[test]
291    fn monotonicity() {
292        let mut dt = Noumea.with_ymd_and_hms(1800, 1, 1, 12, 0, 0).unwrap();
293        for _ in 0..24 * 356 * 400 {
294            let new = dt + Duration::hours(1);
295            assert!(new > dt);
296            assert!(new.with_timezone(&UTC) > dt.with_timezone(&UTC));
297            dt = new;
298        }
299    }
300
301    fn test_inverse<T: TimeZone>(tz: T, begin: i32, end: i32) {
302        for y in begin..end {
303            for d in 1..366 {
304                let date = NaiveDate::from_yo_opt(y, d).unwrap();
305                for h in 0..24 {
306                    for m in 0..60 {
307                        let dt = date.and_hms_opt(h, m, 0).unwrap().and_utc();
308                        let with_tz = dt.with_timezone(&tz);
309                        let utc = with_tz.with_timezone(&UTC);
310                        assert_eq!(dt, utc);
311                    }
312                }
313            }
314        }
315    }
316
317    #[test]
318    fn inverse_london() {
319        test_inverse(London, 1989, 1994);
320    }
321
322    #[test]
323    fn inverse_dhaka() {
324        test_inverse(Dhaka, 1995, 2000);
325    }
326
327    #[test]
328    fn inverse_apia() {
329        test_inverse(Apia, 2011, 2012);
330    }
331
332    #[test]
333    fn inverse_tahiti() {
334        test_inverse(Tahiti, 1911, 1914);
335    }
336
337    #[test]
338    fn string_representation() {
339        let dt = UTC
340            .with_ymd_and_hms(2000, 9, 1, 12, 30, 15)
341            .unwrap()
342            .with_timezone(&Adelaide);
343        assert_eq!(dt.to_string(), "2000-09-01 22:00:15 ACST");
344        assert_eq!(format!("{dt:?}"), "2000-09-01T22:00:15ACST");
345        assert_eq!(dt.to_rfc3339(), "2000-09-01T22:00:15+09:30");
346        assert_eq!(format!("{dt}"), "2000-09-01 22:00:15 ACST");
347    }
348
349    #[test]
350    fn tahiti() {
351        let dt = UTC
352            .with_ymd_and_hms(1912, 10, 1, 9, 58, 16)
353            .unwrap()
354            .with_timezone(&Tahiti);
355        let before = dt - Duration::hours(1);
356        assert_eq!(
357            before,
358            Tahiti.with_ymd_and_hms(1912, 9, 30, 23, 0, 0).unwrap()
359        );
360        let after = dt + Duration::hours(1);
361        assert_eq!(
362            after,
363            Tahiti.with_ymd_and_hms(1912, 10, 1, 0, 58, 16).unwrap()
364        );
365    }
366
367    #[test]
368    fn nonexistent_time() {
369        assert!(London
370            .with_ymd_and_hms(2016, 3, 27, 1, 30, 0)
371            .single()
372            .is_none());
373    }
374
375    #[test]
376    fn nonexistent_time_2() {
377        assert!(London
378            .with_ymd_and_hms(2016, 3, 27, 1, 0, 0)
379            .single()
380            .is_none());
381    }
382
383    #[test]
384    fn time_exists() {
385        assert!(London
386            .with_ymd_and_hms(2016, 3, 27, 2, 0, 0)
387            .single()
388            .is_some());
389    }
390
391    #[test]
392    fn ambiguous_time() {
393        let ambiguous = London.with_ymd_and_hms(2016, 10, 30, 1, 0, 0);
394        let earliest_utc = NaiveDate::from_ymd_opt(2016, 10, 30)
395            .unwrap()
396            .and_hms_opt(0, 0, 0)
397            .unwrap();
398        assert_eq!(
399            ambiguous.earliest().unwrap(),
400            London.from_utc_datetime(&earliest_utc)
401        );
402        let latest_utc = NaiveDate::from_ymd_opt(2016, 10, 30)
403            .unwrap()
404            .and_hms_opt(1, 0, 0)
405            .unwrap();
406        assert_eq!(
407            ambiguous.latest().unwrap(),
408            London.from_utc_datetime(&latest_utc)
409        );
410    }
411
412    #[test]
413    fn ambiguous_time_2() {
414        let ambiguous = London.with_ymd_and_hms(2016, 10, 30, 1, 30, 0);
415        let earliest_utc = NaiveDate::from_ymd_opt(2016, 10, 30)
416            .unwrap()
417            .and_hms_opt(0, 30, 0)
418            .unwrap();
419        assert_eq!(
420            ambiguous.earliest().unwrap(),
421            London.from_utc_datetime(&earliest_utc)
422        );
423        let latest_utc = NaiveDate::from_ymd_opt(2016, 10, 30)
424            .unwrap()
425            .and_hms_opt(1, 30, 0)
426            .unwrap();
427        assert_eq!(
428            ambiguous.latest().unwrap(),
429            London.from_utc_datetime(&latest_utc)
430        );
431    }
432
433    #[test]
434    fn ambiguous_time_3() {
435        let ambiguous = Moscow.with_ymd_and_hms(2014, 10, 26, 1, 30, 0);
436        let earliest_utc = NaiveDate::from_ymd_opt(2014, 10, 25)
437            .unwrap()
438            .and_hms_opt(21, 30, 0)
439            .unwrap();
440        assert_eq!(
441            ambiguous.earliest().unwrap().fixed_offset(),
442            Moscow.from_utc_datetime(&earliest_utc).fixed_offset()
443        );
444        let latest_utc = NaiveDate::from_ymd_opt(2014, 10, 25)
445            .unwrap()
446            .and_hms_opt(22, 30, 0)
447            .unwrap();
448        assert_eq!(
449            ambiguous.latest().unwrap(),
450            Moscow.from_utc_datetime(&latest_utc)
451        );
452    }
453
454    #[test]
455    fn ambiguous_time_4() {
456        let ambiguous = Moscow.with_ymd_and_hms(2014, 10, 26, 1, 0, 0);
457        let earliest_utc = NaiveDate::from_ymd_opt(2014, 10, 25)
458            .unwrap()
459            .and_hms_opt(21, 0, 0)
460            .unwrap();
461        assert_eq!(
462            ambiguous.earliest().unwrap().fixed_offset(),
463            Moscow.from_utc_datetime(&earliest_utc).fixed_offset()
464        );
465        let latest_utc = NaiveDate::from_ymd_opt(2014, 10, 25)
466            .unwrap()
467            .and_hms_opt(22, 0, 0)
468            .unwrap();
469        assert_eq!(
470            ambiguous.latest().unwrap(),
471            Moscow.from_utc_datetime(&latest_utc)
472        );
473    }
474
475    #[test]
476    fn unambiguous_time() {
477        assert!(London
478            .with_ymd_and_hms(2016, 10, 30, 2, 0, 0)
479            .single()
480            .is_some());
481    }
482
483    #[test]
484    fn unambiguous_time_2() {
485        assert!(Moscow
486            .with_ymd_and_hms(2014, 10, 26, 2, 0, 0)
487            .single()
488            .is_some());
489    }
490
491    #[test]
492    fn test_get_name() {
493        assert_eq!(London.name(), "Europe/London");
494        assert_eq!(Tz::Africa__Abidjan.name(), "Africa/Abidjan");
495        assert_eq!(Tz::UTC.name(), "UTC");
496        assert_eq!(Tz::Zulu.name(), "Zulu");
497    }
498
499    #[test]
500    fn test_display() {
501        assert_eq!(format!("{London}"), "Europe/London");
502        assert_eq!(format!("{}", Tz::Africa__Abidjan), "Africa/Abidjan");
503        assert_eq!(format!("{}", Tz::UTC), "UTC");
504        assert_eq!(format!("{}", Tz::Zulu), "Zulu");
505    }
506
507    #[test]
508    fn test_impl_hash() {
509        #[allow(dead_code)]
510        #[derive(Hash)]
511        struct Foo(Tz);
512    }
513
514    #[test]
515    fn test_iana_tzdb_version() {
516        // Format should be something like 2023c.
517        assert_eq!(5, IANA_TZDB_VERSION.len());
518        let numbers: Vec<&str> = IANA_TZDB_VERSION.matches(char::is_numeric).collect();
519        assert_eq!(4, numbers.len());
520        assert!(IANA_TZDB_VERSION.ends_with(|c: char| c.is_ascii_lowercase()));
521    }
522
523    #[test]
524    fn test_numeric_names() {
525        let dt = Scoresbysund.with_ymd_and_hms(2024, 5, 1, 0, 0, 0).unwrap();
526        assert_eq!(format!("{}", dt.offset()), "-01");
527        assert_eq!(format!("{:?}", dt.offset()), "-01");
528        let dt = Casey.with_ymd_and_hms(2022, 11, 1, 0, 0, 0).unwrap();
529        assert_eq!(format!("{}", dt.offset()), "+11");
530        assert_eq!(format!("{:?}", dt.offset()), "+11");
531        let dt = Addis_Ababa.with_ymd_and_hms(1937, 2, 1, 0, 0, 0).unwrap();
532        assert_eq!(format!("{}", dt.offset()), "+0245");
533        assert_eq!(format!("{:?}", dt.offset()), "+0245");
534    }
535
536    fn gap_info_test(tz: Tz, gap_begin: NaiveDateTime, gap_end: NaiveDateTime) {
537        let before = gap_begin - Duration::seconds(1);
538        let before_offset = tz.offset_from_local_datetime(&before).single().unwrap();
539
540        let gap_end = tz.from_local_datetime(&gap_end).single().unwrap();
541
542        let in_gap = gap_begin + Duration::seconds(1);
543        let GapInfo { begin, end } = GapInfo::new(&in_gap, &tz).unwrap();
544        let (begin_time, begin_offset) = begin.unwrap();
545        let end = end.unwrap();
546
547        assert_eq!(gap_begin, begin_time);
548        assert_eq!(before_offset, begin_offset);
549        assert_eq!(gap_end, end);
550    }
551
552    #[test]
553    fn gap_info_europe_london() {
554        gap_info_test(
555            Tz::Europe__London,
556            NaiveDate::from_ymd_opt(2024, 3, 31)
557                .unwrap()
558                .and_hms_opt(1, 0, 0)
559                .unwrap(),
560            NaiveDate::from_ymd_opt(2024, 3, 31)
561                .unwrap()
562                .and_hms_opt(2, 0, 0)
563                .unwrap(),
564        );
565    }
566
567    #[test]
568    fn gap_info_europe_dublin() {
569        gap_info_test(
570            Tz::Europe__Dublin,
571            NaiveDate::from_ymd_opt(2024, 3, 31)
572                .unwrap()
573                .and_hms_opt(1, 0, 0)
574                .unwrap(),
575            NaiveDate::from_ymd_opt(2024, 3, 31)
576                .unwrap()
577                .and_hms_opt(2, 0, 0)
578                .unwrap(),
579        );
580    }
581
582    #[test]
583    fn gap_info_australia_adelaide() {
584        gap_info_test(
585            Tz::Australia__Adelaide,
586            NaiveDate::from_ymd_opt(2024, 10, 6)
587                .unwrap()
588                .and_hms_opt(2, 0, 0)
589                .unwrap(),
590            NaiveDate::from_ymd_opt(2024, 10, 6)
591                .unwrap()
592                .and_hms_opt(3, 0, 0)
593                .unwrap(),
594        );
595    }
596
597    #[test]
598    fn gap_info_samoa_skips_a_day() {
599        gap_info_test(
600            Tz::Pacific__Apia,
601            NaiveDate::from_ymd_opt(2011, 12, 30)
602                .unwrap()
603                .and_hms_opt(0, 0, 0)
604                .unwrap(),
605            NaiveDate::from_ymd_opt(2011, 12, 31)
606                .unwrap()
607                .and_hms_opt(0, 0, 0)
608                .unwrap(),
609        );
610    }
611
612    #[test]
613    fn gap_info_libya_2013() {
614        gap_info_test(
615            Tz::Libya,
616            NaiveDate::from_ymd_opt(2013, 3, 29)
617                .unwrap()
618                .and_hms_opt(1, 0, 0)
619                .unwrap(),
620            NaiveDate::from_ymd_opt(2013, 3, 29)
621                .unwrap()
622                .and_hms_opt(2, 0, 0)
623                .unwrap(),
624        );
625    }
626
627    #[test]
628    fn casey_utc_change_time() {
629        assert_eq!(
630            NaiveDate::from_ymd_opt(2012, 2, 21)
631                .unwrap()
632                .and_hms_opt(16, 59, 59)
633                .unwrap()
634                .and_utc()
635                .with_timezone(&Casey)
636                .offset()
637                .to_string(),
638            "+11"
639        );
640
641        assert_eq!(
642            NaiveDate::from_ymd_opt(2012, 2, 21)
643                .unwrap()
644                .and_hms_opt(17, 00, 00)
645                .unwrap()
646                .and_utc()
647                .with_timezone(&Casey)
648                .offset()
649                .to_string(),
650            "+08"
651        );
652    }
653}