├── .github ├── dependabot.yml └── workflows │ ├── audit.yml │ └── ci.yml ├── .gitignore ├── Cargo.toml ├── LICENSE-Apache ├── LICENSE-MIT ├── README.md ├── check_release.sh ├── clippy.toml ├── examples ├── settings.rs ├── statics.rs └── time.rs ├── fuzz ├── .gitignore ├── Cargo.toml └── fuzz_targets │ ├── parse_file.rs │ └── parse_string.rs ├── rustfmt.toml └── src ├── constants └── mod.rs ├── datetime ├── find.rs └── mod.rs ├── error ├── datetime.rs ├── mod.rs ├── parse.rs └── timezone.rs ├── lib.rs ├── parse ├── mod.rs ├── tz_file.rs ├── tz_string.rs └── utils.rs ├── timezone ├── mod.rs └── rule.rs └── utils ├── const_fns.rs ├── mod.rs └── system_time.rs /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | -------------------------------------------------------------------------------- /.github/workflows/audit.yml: -------------------------------------------------------------------------------- 1 | name: Audit 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * 0" 6 | 7 | jobs: 8 | audit: 9 | name: "Audit" 10 | 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | 16 | - name: Install Rust 17 | id: actions-rs 18 | uses: actions-rs/toolchain@v1 19 | with: 20 | profile: minimal 21 | toolchain: stable 22 | override: true 23 | 24 | - name: Audit 25 | run: | 26 | cargo install cargo-audit 27 | cargo audit 28 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: ["master"] 6 | pull_request: 7 | branches: ["master"] 8 | schedule: 9 | - cron: "0 0 * * 0" 10 | 11 | jobs: 12 | doc: 13 | name: "Doc" 14 | 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | 20 | - name: Install Rust 21 | run: | 22 | rustup set profile minimal 23 | rustup toolchain install nightly 24 | rustup override set nightly 25 | 26 | - name: Doc 27 | run: RUSTDOCFLAGS="-D warnings --cfg docsrs" cargo doc --all-features --no-deps 28 | 29 | test: 30 | name: "Test" 31 | 32 | runs-on: ubuntu-latest 33 | steps: 34 | - name: Checkout 35 | uses: actions/checkout@v4 36 | 37 | - name: Install Rust 38 | run: | 39 | rustup set profile minimal 40 | rustup toolchain install 1.81 stable nightly --component clippy 41 | rustup override set stable 42 | 43 | - name: Test 44 | run: ./check_release.sh 45 | env: 46 | CARGO_NET_GIT_FETCH_WITH_CLI: true 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | Cargo.lock 3 | target 4 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tz-rs" 3 | version = "0.7.0" 4 | edition = "2021" 5 | rust-version = "1.81" 6 | authors = ["x-hgg-x"] 7 | repository = "https://github.com/x-hgg-x/tz-rs" 8 | description = "A pure Rust reimplementation of libc functions localtime, gmtime and mktime." 9 | license = "MIT OR Apache-2.0" 10 | keywords = ["date", "time", "timezone", "zone", "calendar"] 11 | categories = ["date-and-time", "parser-implementations"] 12 | readme = "README.md" 13 | 14 | [lib] 15 | name = "tz" 16 | 17 | [package.metadata.docs.rs] 18 | all-features = true 19 | rustdoc-args = ["--cfg", "docsrs"] 20 | 21 | [features] 22 | default = ["std"] 23 | std = ["alloc"] 24 | alloc = [] 25 | -------------------------------------------------------------------------------- /LICENSE-Apache: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 x-hgg-x 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tz-rs 2 | 3 | [![version](https://img.shields.io/crates/v/tz-rs?color=blue&style=flat-square)](https://crates.io/crates/tz-rs) 4 | ![Minimum supported Rust version](https://img.shields.io/badge/rustc-1.81+-important?logo=rust "Minimum Supported Rust Version") 5 | [![Documentation](https://docs.rs/tz-rs/badge.svg)](https://docs.rs/tz-rs) 6 | 7 | A pure Rust reimplementation of libc functions [`localtime`](https://en.cppreference.com/w/c/chrono/localtime), [`gmtime`](https://en.cppreference.com/w/c/chrono/gmtime) and [`mktime`](https://en.cppreference.com/w/c/chrono/mktime). 8 | 9 | This crate allows to convert between a [Unix timestamp](https://en.wikipedia.org/wiki/Unix_time) and a calendar time expressed in the [proleptic gregorian calendar](https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar) with a provided time zone. 10 | 11 | Time zones are provided to the library with a [POSIX `TZ` string](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html) which can be read from the environment. 12 | 13 | Two formats are currently accepted for the `TZ` string: 14 | * `std offset[dst[offset][,start[/time],end[/time]]]` providing a time zone description, 15 | * `file` or `:file` providing the path to a [TZif file](https://datatracker.ietf.org/doc/html/rfc8536), which is absolute or relative to the system timezone directory. 16 | 17 | See also the [Linux manual page of tzset(3)](https://man7.org/linux/man-pages/man3/tzset.3.html) and the [glibc documentation of the `TZ` environment variable](https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html). 18 | 19 | ## Context 20 | 21 | Calls to libc `localtime_r` and other related functions from Rust are not safe in a multithreaded application, because they may internally set the `TZ` environment variable with the `setenv` function, which is not thread-safe. 22 | 23 | See [RUSTSEC-2020-0071](https://rustsec.org/advisories/RUSTSEC-2020-0071.html) and [RUSTSEC-2020-0159](https://rustsec.org/advisories/RUSTSEC-2020-0159.html) for more information. 24 | 25 | ## Documentation 26 | 27 | Documentation is hosted on [docs.rs](https://docs.rs/tz-rs/latest/tz/). 28 | 29 | ## Platform support 30 | 31 | This crate is mainly intended for UNIX platforms. 32 | 33 | Since the time zone database files are not included in this crate, non-UNIX users can download a copy of the database on the [IANA site](https://www.iana.org/time-zones) and compile the time zone database files to a local directory. 34 | 35 | The database files can then be read by specifying an absolute path in the `TZ` string: 36 | 37 | ```rust 38 | TimeZone::from_posix_tz(format!("{local_database_dir}/usr/share/zoneinfo/Pacific/Auckland"))?; 39 | ``` 40 | 41 | Note that the determination of the local time zone with this crate is not supported on non-UNIX platforms. 42 | 43 | Alternatively, a crate like [tzdb](https://github.com/Kijewski/tzdb) can be used, which statically provides existing time zone definitions for this crate, and supports finding the local time zone for all [Tier 1](https://doc.rust-lang.org/nightly/rustc/platform-support.html) platforms. 44 | 45 | ## Date time formatting (equivalent of libc `strftime` function) 46 | 47 | This crate doesn't provide custom date time formatting support, but the [`custom-format`](https://github.com/x-hgg-x/custom-format) crate can be used to provide custom format specifiers to the standard library formatting macros. 48 | 49 | ## Compiler support 50 | 51 | Requires `rustc 1.81+`. 52 | 53 | ## License 54 | 55 | This project is licensed under either of 56 | 57 | - [Apache License, Version 2.0](https://github.com/x-hgg-x/tz-rs/blob/master/LICENSE-Apache) 58 | - [MIT license](https://github.com/x-hgg-x/tz-rs/blob/master/LICENSE-MIT) 59 | 60 | at your option. 61 | 62 | Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in 63 | this project by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any 64 | additional terms or conditions. 65 | -------------------------------------------------------------------------------- /check_release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | set -e 4 | 5 | fmt_cmd="cargo fmt --all -- --check" 6 | echo "+ $fmt_cmd" 7 | $fmt_cmd 8 | 9 | run() { 10 | cargo_arg=$1 11 | bin_arg=$2 12 | 13 | for rust in "1.81" "stable" "nightly"; do 14 | for feature in "" "alloc" "std"; do 15 | cmd="cargo +$rust -q $cargo_arg --all-targets --no-default-features --features=$const,$feature $bin_arg" 16 | echo "+ $cmd" 17 | $cmd 18 | echo "\n" 19 | done 20 | done 21 | } 22 | 23 | run "clippy" "-- -D warnings" 24 | run "test" 25 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | msrv = "1.81" 2 | -------------------------------------------------------------------------------- /examples/settings.rs: -------------------------------------------------------------------------------- 1 | fn main() -> Result<(), tz::Error> { 2 | #[cfg(feature = "std")] 3 | { 4 | use tz::TimeZoneSettings; 5 | 6 | const TIME_ZONE_SETTINGS: TimeZoneSettings<'static> = 7 | TimeZoneSettings::new(&["/usr/share/zoneinfo", "/share/zoneinfo", "/etc/zoneinfo"], |path| Ok(std::fs::read(path)?)); 8 | 9 | let time_zone_local = TIME_ZONE_SETTINGS.parse_local()?; 10 | println!("{:?}", time_zone_local.find_current_local_time_type()?); 11 | } 12 | 13 | Ok(()) 14 | } 15 | -------------------------------------------------------------------------------- /examples/statics.rs: -------------------------------------------------------------------------------- 1 | use tz::datetime::{DateTime, UtcDateTime}; 2 | use tz::error::TzError; 3 | use tz::timezone::{AlternateTime, LeapSecond, LocalTimeType, MonthWeekDay, RuleDay, TimeZoneRef, Transition, TransitionRule}; 4 | 5 | fn main() -> Result<(), TzError> { 6 | macro_rules! unwrap { 7 | ($x:expr) => { 8 | match $x { 9 | Ok(x) => x, 10 | Err(_) => panic!(), 11 | } 12 | }; 13 | } 14 | 15 | const TIME_ZONE_REF: TimeZoneRef<'static> = unwrap!(TimeZoneRef::new( 16 | &[ 17 | Transition::new(-2334101314, 1), 18 | Transition::new(-1157283000, 2), 19 | Transition::new(-1155436200, 1), 20 | Transition::new(-880198200, 3), 21 | Transition::new(-769395600, 4), 22 | Transition::new(-765376200, 1), 23 | Transition::new(-712150200, 5), 24 | ], 25 | const { 26 | &[ 27 | unwrap!(LocalTimeType::new(-37886, false, Some(b"LMT"))), 28 | unwrap!(LocalTimeType::new(-37800, false, Some(b"HST"))), 29 | unwrap!(LocalTimeType::new(-34200, true, Some(b"HDT"))), 30 | unwrap!(LocalTimeType::new(-34200, true, Some(b"HWT"))), 31 | unwrap!(LocalTimeType::new(-34200, true, Some(b"HPT"))), 32 | unwrap!(LocalTimeType::new(-36000, false, Some(b"HST"))), 33 | ] 34 | }, 35 | &[ 36 | LeapSecond::new(78796800, 1), 37 | LeapSecond::new(94694401, 2), 38 | LeapSecond::new(126230402, 3), 39 | LeapSecond::new(157766403, 4), 40 | LeapSecond::new(189302404, 5), 41 | LeapSecond::new(220924805, 6), 42 | ], 43 | const { 44 | &Some(TransitionRule::Alternate(unwrap!(AlternateTime::new( 45 | unwrap!(LocalTimeType::new(-36000, false, Some(b"HST"))), 46 | unwrap!(LocalTimeType::new(-34200, true, Some(b"HPT"))), 47 | RuleDay::MonthWeekDay(unwrap!(MonthWeekDay::new(10, 5, 0))), 48 | 93600, 49 | RuleDay::MonthWeekDay(unwrap!(MonthWeekDay::new(3, 4, 4))), 50 | 7200, 51 | )))) 52 | }, 53 | )); 54 | 55 | const LOCAL_TIME_TYPE: LocalTimeType = *unwrap!(TIME_ZONE_REF.find_local_time_type(0)); 56 | 57 | const UTC: TimeZoneRef<'static> = TimeZoneRef::utc(); 58 | 59 | const UNIX_EPOCH: UtcDateTime = unwrap!(UtcDateTime::from_timespec(0, 0)); 60 | const UTC_DATE_TIME: UtcDateTime = unwrap!(UtcDateTime::new(2000, 1, 1, 0, 0, 0, 1000)); 61 | 62 | const DATE_TIME: DateTime = unwrap!(DateTime::new(2000, 1, 1, 1, 0, 0, 1000, unwrap!(LocalTimeType::with_ut_offset(3600)))); 63 | 64 | const DATE_TIME_1: DateTime = unwrap!(UTC_DATE_TIME.project(TIME_ZONE_REF)); 65 | const DATE_TIME_2: DateTime = unwrap!(DATE_TIME_1.project(UTC)); 66 | 67 | println!("{TIME_ZONE_REF:#?}"); 68 | println!("{LOCAL_TIME_TYPE:?}"); 69 | 70 | println!("{UNIX_EPOCH:?}"); 71 | println!("{UTC_DATE_TIME:?}"); 72 | 73 | println!("{DATE_TIME:#?}"); 74 | 75 | println!("{DATE_TIME_1:#?}"); 76 | println!("{DATE_TIME_2:#?}"); 77 | 78 | Ok(()) 79 | } 80 | -------------------------------------------------------------------------------- /examples/time.rs: -------------------------------------------------------------------------------- 1 | fn main() -> Result<(), tz::Error> { 2 | #[cfg(feature = "std")] 3 | { 4 | use tz::{DateTime, LocalTimeType, TimeZone, UtcDateTime}; 5 | 6 | // 7 | // TimeZone 8 | // 9 | 10 | // 2000-01-01T00:00:00Z 11 | let unix_time = 946684800; 12 | 13 | // Get UTC time zone 14 | let time_zone_utc = TimeZone::utc(); 15 | println!("{:?}", time_zone_utc.find_local_time_type(unix_time)?); 16 | 17 | // Get fixed time zone at GMT-1 18 | let time_zone_fixed = TimeZone::fixed(-3600)?; 19 | println!("{:?}", time_zone_fixed.find_local_time_type(unix_time)?.ut_offset()); 20 | 21 | // Get local time zone (UNIX only) 22 | let time_zone_local = TimeZone::local()?; 23 | println!("{:?}", time_zone_local.find_local_time_type(unix_time)?.ut_offset()); 24 | 25 | // Get the current local time type 26 | println!("{:?}", time_zone_local.find_current_local_time_type()?); 27 | 28 | // Get time zone from a TZ string: 29 | // From an absolute file 30 | let _ = TimeZone::from_posix_tz("/usr/share/zoneinfo/Pacific/Auckland"); 31 | // From a file relative to the system timezone directory 32 | let _ = TimeZone::from_posix_tz("Pacific/Auckland"); 33 | // From a time zone description 34 | TimeZone::from_posix_tz("HST10")?; 35 | TimeZone::from_posix_tz("<-03>3")?; 36 | TimeZone::from_posix_tz("NZST-12:00:00NZDT-13:00:00,M10.1.0,M3.3.0")?; 37 | // Use a leading colon to force searching for a corresponding file 38 | let _ = TimeZone::from_posix_tz(":UTC"); 39 | 40 | // 41 | // DateTime 42 | // 43 | 44 | // Get the current UTC date time 45 | println!("{:?}", UtcDateTime::now()?); 46 | 47 | // Create a new UTC date time (2000-01-01T00:00:00.123456789Z) 48 | let utc_date_time = UtcDateTime::new(2000, 1, 1, 0, 0, 0, 123_456_789)?; 49 | println!("{utc_date_time}"); 50 | println!("{utc_date_time:?}"); 51 | 52 | // Create a new UTC date time from a Unix time with nanoseconds (2000-01-01T00:00:00.123456789Z) 53 | let other_utc_date_time = UtcDateTime::from_timespec(946684800, 123_456_789)?; 54 | println!("{other_utc_date_time}"); 55 | println!("{other_utc_date_time:?}"); 56 | 57 | // Project the UTC date time to a time zone 58 | let date_time = utc_date_time.project(TimeZone::fixed(-3600)?.as_ref())?; 59 | println!("{date_time}"); 60 | println!("{date_time:#?}"); 61 | 62 | // Project the date time to another time zone 63 | let other_date_time = date_time.project(TimeZone::fixed(3600)?.as_ref())?; 64 | println!("{other_date_time}"); 65 | println!("{other_date_time:#?}"); 66 | 67 | // Create a new date time from a Unix time with nanoseconds and a time zone (2000-01-01T00:00:00.123456789Z) 68 | let another_date_time = DateTime::from_timespec(946684800, 123_456_789, TimeZone::fixed(86400)?.as_ref())?; 69 | println!("{another_date_time}"); 70 | println!("{another_date_time:#?}"); 71 | 72 | // Get the corresponding UTC Unix times with nanoseconds 73 | println!("{:?}", (utc_date_time.unix_time(), utc_date_time.nanoseconds())); 74 | println!("{:?}", (other_utc_date_time.unix_time(), other_utc_date_time.nanoseconds())); 75 | println!("{:?}", (date_time.unix_time(), date_time.nanoseconds())); 76 | println!("{:?}", (other_date_time.unix_time(), other_date_time.nanoseconds())); 77 | 78 | // Nanoseconds are always added towards the future 79 | let neg_utc_date_time = UtcDateTime::from_timespec(-1, 123_456_789)?; 80 | println!("{neg_utc_date_time}"); 81 | println!("{}", neg_utc_date_time.total_nanoseconds()); 82 | 83 | // Get the current date time at the local time zone (UNIX only) 84 | let time_zone_local = TimeZone::local()?; 85 | println!("{:#?}", DateTime::now(time_zone_local.as_ref())?); 86 | 87 | // Create a new date time with an UTC offset (2000-01-01T01:00:00.123456789+01:00) 88 | println!("{:#?}", DateTime::new(2000, 1, 1, 1, 0, 0, 123_456_789, LocalTimeType::with_ut_offset(3600)?)?); 89 | 90 | // 91 | // Find the possible date times corresponding to a date, a time and a time zone 92 | // 93 | let time_zone = TimeZone::from_posix_tz("CET-1CEST,M3.5.0,M10.5.0/3")?; 94 | 95 | // Found date time is unique 96 | let found_date_times = DateTime::find(2000, 1, 1, 0, 0, 0, 0, time_zone.as_ref())?; 97 | println!("{found_date_times:#?}"); 98 | println!("{:#?}", found_date_times.unique()); 99 | println!("{:#?}", found_date_times.earliest()); 100 | println!("{:#?}", found_date_times.latest()); 101 | 102 | // Found date time was skipped by a forward transition 103 | let found_date_times = DateTime::find(2000, 3, 26, 2, 30, 0, 0, time_zone.as_ref())?; 104 | println!("{found_date_times:#?}"); 105 | println!("{:#?}", found_date_times.unique()); 106 | println!("{:#?}", found_date_times.earliest()); 107 | println!("{:#?}", found_date_times.latest()); 108 | 109 | // Found date time is ambiguous because of a backward transition 110 | let found_date_times = DateTime::find(2000, 10, 29, 2, 30, 0, 0, time_zone.as_ref())?; 111 | println!("{found_date_times:#?}"); 112 | println!("{:#?}", found_date_times.unique()); 113 | println!("{:#?}", found_date_times.earliest()); 114 | println!("{:#?}", found_date_times.latest()); 115 | } 116 | 117 | Ok(()) 118 | } 119 | -------------------------------------------------------------------------------- /fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | corpus 3 | artifacts 4 | -------------------------------------------------------------------------------- /fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tz-rs-fuzz" 3 | version = "0.0.0" 4 | authors = ["Automatically generated"] 5 | publish = false 6 | edition = "2018" 7 | 8 | [package.metadata] 9 | cargo-fuzz = true 10 | 11 | [dependencies] 12 | libfuzzer-sys = "0.4" 13 | 14 | [dependencies.tz-rs] 15 | path = ".." 16 | 17 | # Prevent this from interfering with workspaces 18 | [workspace] 19 | members = ["."] 20 | 21 | [[bin]] 22 | name = "parse_file" 23 | path = "fuzz_targets/parse_file.rs" 24 | test = false 25 | doc = false 26 | 27 | [[bin]] 28 | name = "parse_string" 29 | path = "fuzz_targets/parse_string.rs" 30 | test = false 31 | doc = false 32 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/parse_file.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use libfuzzer_sys::fuzz_target; 3 | 4 | fuzz_target!(|data: &[u8]| { 5 | use tz::TimeZone; 6 | 7 | let _ = TimeZone::from_tz_data(data); 8 | }); 9 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/parse_string.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use libfuzzer_sys::fuzz_target; 3 | 4 | fuzz_target!(|data: &[u8]| { 5 | use tz::TimeZone; 6 | 7 | if let Ok(data) = core::str::from_utf8(data) { 8 | let _ = TimeZone::from_posix_tz(data); 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | max_width = 160 2 | use_small_heuristics = "Max" 3 | -------------------------------------------------------------------------------- /src/constants/mod.rs: -------------------------------------------------------------------------------- 1 | //! Some useful constants. 2 | 3 | /// Number of nanoseconds in one second 4 | pub const NANOSECONDS_PER_SECOND: u32 = 1_000_000_000; 5 | /// Number of seconds in one minute 6 | pub const SECONDS_PER_MINUTE: i64 = 60; 7 | /// Number of minutes in one hour 8 | pub const MINUTES_PER_HOUR: i64 = 60; 9 | /// Number of hours in one day 10 | pub const HOURS_PER_DAY: i64 = 24; 11 | /// Number of seconds in one hour 12 | pub const SECONDS_PER_HOUR: i64 = 3600; 13 | /// Number of seconds in one day 14 | pub const SECONDS_PER_DAY: i64 = SECONDS_PER_HOUR * HOURS_PER_DAY; 15 | /// Number of days in one week 16 | pub const DAYS_PER_WEEK: i64 = 7; 17 | /// Number of seconds in one week 18 | pub const SECONDS_PER_WEEK: i64 = SECONDS_PER_DAY * DAYS_PER_WEEK; 19 | /// Number of seconds in 28 days 20 | pub const SECONDS_PER_28_DAYS: i64 = SECONDS_PER_DAY * 28; 21 | /// Number of months in one year 22 | pub const MONTHS_PER_YEAR: i64 = 12; 23 | /// Number of days in a normal year 24 | pub const DAYS_PER_NORMAL_YEAR: i64 = 365; 25 | /// Number of seconds in a normal year 26 | pub const SECONDS_PER_NORMAL_YEAR: i64 = DAYS_PER_NORMAL_YEAR * SECONDS_PER_DAY; 27 | /// Number of seconds in a leap year 28 | pub const SECONDS_PER_LEAP_YEAR: i64 = (DAYS_PER_NORMAL_YEAR + 1) * SECONDS_PER_DAY; 29 | /// Number of days in 4 years (including 1 leap year) 30 | pub const DAYS_PER_4_YEARS: i64 = DAYS_PER_NORMAL_YEAR * 4 + 1; 31 | /// Number of days in 100 years (including 24 leap years) 32 | pub const DAYS_PER_100_YEARS: i64 = DAYS_PER_NORMAL_YEAR * 100 + 24; 33 | /// Number of days in 400 years (including 97 leap years) 34 | pub const DAYS_PER_400_YEARS: i64 = DAYS_PER_NORMAL_YEAR * 400 + 97; 35 | 36 | /// Month days in a normal year 37 | pub const DAYS_IN_MONTHS_NORMAL_YEAR: [i64; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; 38 | /// Cumulated month days in a normal year 39 | pub const CUMUL_DAYS_IN_MONTHS_NORMAL_YEAR: [i64; 12] = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]; 40 | /// Cumulated month days in a leap year 41 | pub const CUMUL_DAYS_IN_MONTHS_LEAP_YEAR: [i64; 12] = [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335]; 42 | 43 | /// Unix time at `2000-03-01T00:00:00Z` (Wednesday) 44 | pub const UNIX_OFFSET_SECS: i64 = 951868800; 45 | /// Offset year 46 | pub const OFFSET_YEAR: i64 = 2000; 47 | /// Month days in a leap year from March 48 | pub const DAY_IN_MONTHS_LEAP_YEAR_FROM_MARCH: [i64; 12] = [31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 31, 29]; 49 | -------------------------------------------------------------------------------- /src/datetime/find.rs: -------------------------------------------------------------------------------- 1 | //! Types related to the [`DateTime::find`] method. 2 | 3 | use crate::datetime::{check_date_time_inputs, unix_time, DateTime, UtcDateTime}; 4 | use crate::error::TzError; 5 | use crate::timezone::{TimeZoneRef, TransitionRule}; 6 | 7 | #[cfg(feature = "alloc")] 8 | use alloc::vec::Vec; 9 | 10 | /// Type of a found date time created by the [`DateTime::find`] method 11 | #[derive(Debug, Copy, Clone, PartialEq)] 12 | pub enum FoundDateTimeKind { 13 | /// Found date time is valid 14 | Normal(DateTime), 15 | /// Found date time is invalid because it was skipped by a forward transition. 16 | /// 17 | /// This variant gives the two [`DateTime`] corresponding to the transition instant, just before and just after the transition. 18 | /// 19 | /// This is different from the `mktime` behavior, which allows invalid date times when no DST information is available (by specifying `tm_isdst = -1`). 20 | Skipped { 21 | /// Date time just before the forward transition 22 | before_transition: DateTime, 23 | /// Date time just after the forward transition 24 | after_transition: DateTime, 25 | }, 26 | } 27 | 28 | /// List containing the found date times created by the [`DateTime::find`] method. 29 | /// 30 | /// It can be empty if no local time type was found for the provided date, time and time zone. 31 | /// 32 | #[cfg(feature = "alloc")] 33 | #[derive(Debug, Default, Clone, PartialEq)] 34 | pub struct FoundDateTimeList(Vec); 35 | 36 | #[cfg(feature = "alloc")] 37 | impl FoundDateTimeList { 38 | /// Returns the found date time if existing and unique 39 | pub fn unique(&self) -> Option { 40 | match *self.0.as_slice() { 41 | [FoundDateTimeKind::Normal(date_time)] => Some(date_time), 42 | _ => None, 43 | } 44 | } 45 | 46 | /// Returns the earliest found date time if existing 47 | pub fn earliest(&self) -> Option { 48 | // Found date times are computed in ascending order of Unix times 49 | match *self.0.first()? { 50 | FoundDateTimeKind::Normal(date_time) => Some(date_time), 51 | FoundDateTimeKind::Skipped { before_transition, .. } => Some(before_transition), 52 | } 53 | } 54 | 55 | /// Returns the latest found date time if existing 56 | pub fn latest(&self) -> Option { 57 | // Found date times are computed in ascending order of Unix times 58 | match *self.0.last()? { 59 | FoundDateTimeKind::Normal(date_time) => Some(date_time), 60 | FoundDateTimeKind::Skipped { after_transition, .. } => Some(after_transition), 61 | } 62 | } 63 | 64 | /// Extracts and returns the inner list of found date times 65 | pub fn into_inner(self) -> Vec { 66 | self.0 67 | } 68 | } 69 | 70 | /// Wrapper reference type with methods for extracting the found date times, created by the [`DateTime::find_n`] method 71 | #[derive(Debug, PartialEq)] 72 | pub struct FoundDateTimeListRefMut<'a> { 73 | /// Preallocated buffer 74 | buf: &'a mut [Option], 75 | /// Current index 76 | current_index: usize, 77 | /// Total count of found date times 78 | count: usize, 79 | } 80 | 81 | impl<'a> FoundDateTimeListRefMut<'a> { 82 | /// Construct a new [`FoundDateTimeListRefMut`] value 83 | pub fn new(buf: &'a mut [Option]) -> Self { 84 | Self { buf, current_index: 0, count: 0 } 85 | } 86 | 87 | /// Returns the found date time if existing and unique 88 | pub fn unique(&self) -> Option { 89 | let mut iter = self.data().iter().flatten(); 90 | let first = iter.next(); 91 | let second = iter.next(); 92 | 93 | match (first, second) { 94 | (Some(FoundDateTimeKind::Normal(date_time)), None) => Some(*date_time), 95 | _ => None, 96 | } 97 | } 98 | 99 | /// Returns the earliest found date time if existing 100 | pub fn earliest(&self) -> Option { 101 | // Found date times are computed in ascending order of Unix times 102 | match *self.data().iter().flatten().next()? { 103 | FoundDateTimeKind::Normal(date_time) => Some(date_time), 104 | FoundDateTimeKind::Skipped { before_transition, .. } => Some(before_transition), 105 | } 106 | } 107 | 108 | /// Returns the latest found date time if existing 109 | pub fn latest(&self) -> Option { 110 | // Found date times are computed in ascending order of Unix times 111 | match *self.data().iter().flatten().next_back()? { 112 | FoundDateTimeKind::Normal(date_time) => Some(date_time), 113 | FoundDateTimeKind::Skipped { after_transition, .. } => Some(after_transition), 114 | } 115 | } 116 | 117 | /// Returns the subslice of written data 118 | pub fn data(&self) -> &[Option] { 119 | &self.buf[..self.current_index] 120 | } 121 | 122 | /// Returns the count of found date times 123 | pub fn count(&self) -> usize { 124 | self.count 125 | } 126 | 127 | /// Returns `true` if all found date times have been written in the buffer 128 | pub fn is_exhaustive(&self) -> bool { 129 | self.current_index == self.count 130 | } 131 | } 132 | 133 | /// Trait representing a list of found date times 134 | pub(super) trait DateTimeList { 135 | /// Appends a found date time to the list 136 | fn push(&mut self, found_date_time: FoundDateTimeKind); 137 | } 138 | 139 | #[cfg(feature = "alloc")] 140 | impl DateTimeList for FoundDateTimeList { 141 | fn push(&mut self, found_date_time: FoundDateTimeKind) { 142 | self.0.push(found_date_time); 143 | } 144 | } 145 | 146 | impl DateTimeList for FoundDateTimeListRefMut<'_> { 147 | fn push(&mut self, found_date_time: FoundDateTimeKind) { 148 | if let Some(x) = self.buf.get_mut(self.current_index) { 149 | *x = Some(found_date_time); 150 | self.current_index += 1 151 | } 152 | 153 | self.count += 1; 154 | } 155 | } 156 | 157 | /// Find the possible date times corresponding to a date, a time and a time zone 158 | /// 159 | /// ## Inputs 160 | /// 161 | /// * `found_date_time_list`: Buffer containing found date times 162 | /// * `year`: Year 163 | /// * `month`: Month in `[1, 12]` 164 | /// * `month_day`: Day of the month in `[1, 31]` 165 | /// * `hour`: Hours since midnight in `[0, 23]` 166 | /// * `minute`: Minutes in `[0, 59]` 167 | /// * `second`: Seconds in `[0, 60]`, with a possible leap second 168 | /// * `nanoseconds`: Nanoseconds in `[0, 999_999_999]` 169 | /// * `time_zone_ref`: Reference to a time zone 170 | /// 171 | #[allow(clippy::too_many_arguments)] 172 | pub(super) fn find_date_time( 173 | found_date_time_list: &mut impl DateTimeList, 174 | year: i32, 175 | month: u8, 176 | month_day: u8, 177 | hour: u8, 178 | minute: u8, 179 | second: u8, 180 | nanoseconds: u32, 181 | time_zone_ref: TimeZoneRef<'_>, 182 | ) -> Result<(), TzError> { 183 | let transitions = time_zone_ref.transitions(); 184 | let local_time_types = time_zone_ref.local_time_types(); 185 | let extra_rule = time_zone_ref.extra_rule(); 186 | 187 | if transitions.is_empty() && extra_rule.is_none() { 188 | let date_time = DateTime::new(year, month, month_day, hour, minute, second, nanoseconds, local_time_types[0])?; 189 | found_date_time_list.push(FoundDateTimeKind::Normal(date_time)); 190 | return Ok(()); 191 | } 192 | 193 | let new_datetime = |local_time_type, unix_time| DateTime { year, month, month_day, hour, minute, second, local_time_type, unix_time, nanoseconds }; 194 | 195 | check_date_time_inputs(year, month, month_day, hour, minute, second, nanoseconds)?; 196 | let utc_unix_time = unix_time(year, month, month_day, hour, minute, second); 197 | 198 | // Process transitions 199 | if !transitions.is_empty() { 200 | let mut last_cached_time = None; 201 | 202 | let mut get_time = |local_time_type_index: usize| -> Result<_, TzError> { 203 | match last_cached_time { 204 | Some((index, value)) if index == local_time_type_index => Ok(value), 205 | _ => { 206 | // Overflow is not possible 207 | let unix_time = utc_unix_time - local_time_types[local_time_type_index].ut_offset() as i64; 208 | let unix_leap_time = time_zone_ref.unix_time_to_unix_leap_time(unix_time)?; 209 | 210 | last_cached_time = Some((local_time_type_index, (unix_time, unix_leap_time))); 211 | Ok((unix_time, unix_leap_time)) 212 | } 213 | } 214 | }; 215 | 216 | let mut previous_transition_unix_leap_time = i64::MIN; 217 | let mut previous_local_time_type_index = 0; 218 | 219 | // Check transitions in order 220 | for (index, transition) in transitions.iter().enumerate() { 221 | let local_time_type_before = local_time_types[previous_local_time_type_index]; 222 | let (unix_time_before, unix_leap_time_before) = get_time(previous_local_time_type_index)?; 223 | 224 | if previous_transition_unix_leap_time <= unix_leap_time_before && unix_leap_time_before < transition.unix_leap_time() { 225 | UtcDateTime::check_unix_time(unix_time_before)?; 226 | found_date_time_list.push(FoundDateTimeKind::Normal(new_datetime(local_time_type_before, unix_time_before))); 227 | } else { 228 | // The last transition is ignored if no extra rules are defined 229 | if index < transitions.len() - 1 || extra_rule.is_some() { 230 | let local_time_type_after = local_time_types[transition.local_time_type_index()]; 231 | let (_, unix_leap_time_after) = get_time(transition.local_time_type_index())?; 232 | 233 | // Check for a forward transition 234 | if unix_leap_time_before >= transition.unix_leap_time() && unix_leap_time_after < transition.unix_leap_time() { 235 | let transition_unix_time = time_zone_ref.unix_leap_time_to_unix_time(transition.unix_leap_time())?; 236 | 237 | found_date_time_list.push(FoundDateTimeKind::Skipped { 238 | before_transition: DateTime::from_timespec_and_local(transition_unix_time, nanoseconds, local_time_type_before)?, 239 | after_transition: DateTime::from_timespec_and_local(transition_unix_time, nanoseconds, local_time_type_after)?, 240 | }); 241 | } 242 | } 243 | } 244 | 245 | previous_transition_unix_leap_time = transition.unix_leap_time(); 246 | previous_local_time_type_index = transition.local_time_type_index(); 247 | } 248 | } 249 | 250 | // Process extra rule 251 | match extra_rule { 252 | None => {} 253 | Some(TransitionRule::Fixed(local_time_type)) => { 254 | // Overflow is not possible 255 | let unix_time = utc_unix_time - local_time_type.ut_offset() as i64; 256 | 257 | let condition = match transitions.last() { 258 | Some(last_transition) => unix_time >= time_zone_ref.unix_leap_time_to_unix_time(last_transition.unix_leap_time())?, 259 | None => true, 260 | }; 261 | 262 | if condition { 263 | UtcDateTime::check_unix_time(unix_time)?; 264 | found_date_time_list.push(FoundDateTimeKind::Normal(new_datetime(*local_time_type, unix_time))); 265 | } 266 | } 267 | Some(TransitionRule::Alternate(alternate_time)) => { 268 | let std_ut_offset = alternate_time.std().ut_offset() as i64; 269 | let dst_ut_offset = alternate_time.dst().ut_offset() as i64; 270 | 271 | // Overflow is not possible 272 | let unix_time_std = utc_unix_time - std_ut_offset; 273 | let unix_time_dst = utc_unix_time - dst_ut_offset; 274 | 275 | let dst_start_time_in_utc = alternate_time.dst_start_time() as i64 - std_ut_offset; 276 | let dst_end_time_in_utc = alternate_time.dst_end_time() as i64 - dst_ut_offset; 277 | 278 | // Check if the associated UTC date times are valid 279 | UtcDateTime::check_unix_time(unix_time_std)?; 280 | UtcDateTime::check_unix_time(unix_time_dst)?; 281 | 282 | // Check if the year is valid for the following computations 283 | if !(i32::MIN + 2..=i32::MAX - 2).contains(&year) { 284 | return Err(TzError::OutOfRange); 285 | } 286 | 287 | // Check DST start/end Unix times for previous/current/next years to support for transition day times outside of [0h, 24h] range. 288 | // This is sufficient since the absolute value of DST start/end time in UTC is less than 2 weeks. 289 | // Moreover, inconsistent DST transition rules are not allowed, so there won't be additional transitions at the year boundary. 290 | let mut additional_transition_times = [ 291 | alternate_time.dst_start().unix_time(year - 1, dst_start_time_in_utc), 292 | alternate_time.dst_end().unix_time(year - 1, dst_end_time_in_utc), 293 | alternate_time.dst_start().unix_time(year, dst_start_time_in_utc), 294 | alternate_time.dst_end().unix_time(year, dst_end_time_in_utc), 295 | alternate_time.dst_start().unix_time(year + 1, dst_start_time_in_utc), 296 | alternate_time.dst_end().unix_time(year + 1, dst_end_time_in_utc), 297 | i64::MAX, 298 | ]; 299 | 300 | // Sort transitions 301 | let sorted = additional_transition_times.windows(2).all(|x| x[0] <= x[1]); 302 | 303 | if !sorted { 304 | for chunk in additional_transition_times.chunks_exact_mut(2) { 305 | chunk.swap(0, 1); 306 | } 307 | }; 308 | 309 | let transition_start = (alternate_time.std(), alternate_time.dst(), unix_time_std, unix_time_dst); 310 | let transition_end = (alternate_time.dst(), alternate_time.std(), unix_time_dst, unix_time_std); 311 | 312 | let additional_transitions = if sorted { 313 | [&transition_start, &transition_end, &transition_start, &transition_end, &transition_start, &transition_end, &transition_start] 314 | } else { 315 | [&transition_end, &transition_start, &transition_end, &transition_start, &transition_end, &transition_start, &transition_end] 316 | }; 317 | 318 | let mut previous_transition_unix_time = match transitions.last() { 319 | Some(last_transition) => time_zone_ref.unix_leap_time_to_unix_time(last_transition.unix_leap_time())?, 320 | None => i64::MIN, 321 | }; 322 | 323 | // Check transitions in order 324 | if let Some(first_valid) = additional_transition_times.iter().position(|&unix_time| previous_transition_unix_time < unix_time) { 325 | let valid_transition_times = &additional_transition_times[first_valid..]; 326 | let valid_transitions = &additional_transitions[first_valid..]; 327 | 328 | let valid_iter = valid_transition_times.iter().copied().zip(valid_transitions.iter().copied()); 329 | 330 | for (transition_unix_time, &(&local_time_type_before, &local_time_type_after, unix_time_before, unix_time_after)) in valid_iter { 331 | if previous_transition_unix_time <= unix_time_before && unix_time_before < transition_unix_time { 332 | found_date_time_list.push(FoundDateTimeKind::Normal(new_datetime(local_time_type_before, unix_time_before))); 333 | } else { 334 | // Check for a forward transition 335 | if unix_time_before >= transition_unix_time && unix_time_after < transition_unix_time { 336 | found_date_time_list.push(FoundDateTimeKind::Skipped { 337 | before_transition: DateTime::from_timespec_and_local(transition_unix_time, nanoseconds, local_time_type_before)?, 338 | after_transition: DateTime::from_timespec_and_local(transition_unix_time, nanoseconds, local_time_type_after)?, 339 | }); 340 | } 341 | } 342 | 343 | previous_transition_unix_time = transition_unix_time; 344 | } 345 | } 346 | } 347 | } 348 | 349 | Ok(()) 350 | } 351 | 352 | #[cfg(feature = "alloc")] 353 | #[cfg(test)] 354 | mod tests { 355 | use super::*; 356 | use crate::datetime::tests::check_equal_date_time; 357 | use crate::timezone::{AlternateTime, Julian0WithLeap, Julian1WithoutLeap, LocalTimeType, RuleDay, TimeZone, Transition}; 358 | 359 | use alloc::vec; 360 | 361 | fn check_equal_option_date_time(x: &Option, y: &Option) { 362 | match (x, y) { 363 | (None, None) => (), 364 | (Some(x), Some(y)) => check_equal_date_time(x, y), 365 | _ => panic!("not equal"), 366 | } 367 | } 368 | 369 | enum Check { 370 | Normal([i32; 1]), 371 | Skipped([(i32, u8, u8, u8, u8, u8, i32); 2]), 372 | } 373 | 374 | fn check( 375 | time_zone_ref: TimeZoneRef<'_>, 376 | posssible_date_time_results: &[Check], 377 | searched: (i32, u8, u8, u8, u8, u8), 378 | result_indices: &[usize], 379 | unique: Option<[usize; 2]>, 380 | earlier: Option<[usize; 2]>, 381 | later: Option<[usize; 2]>, 382 | ) -> Result<(), TzError> { 383 | let new_date_time = |(year, month, month_day, hour, minute, second, ut_offset)| { 384 | DateTime::new(year, month, month_day, hour, minute, second, 0, LocalTimeType::with_ut_offset(ut_offset)?) 385 | }; 386 | 387 | let (year, month, month_day, hour, minute, second) = searched; 388 | 389 | let mut found_date_times = FoundDateTimeList::default(); 390 | find_date_time(&mut found_date_times, year, month, month_day, hour, minute, second, 0, time_zone_ref)?; 391 | 392 | let mut buf = vec![None; result_indices.len()]; 393 | let mut found_date_time_list = FoundDateTimeListRefMut::new(&mut buf); 394 | find_date_time(&mut found_date_time_list, year, month, month_day, hour, minute, second, 0, time_zone_ref)?; 395 | 396 | let indexed_date_time = |[index_1, index_2]: [usize; 2]| match posssible_date_time_results[index_1] { 397 | Check::Normal(arr) => new_date_time((year, month, month_day, hour, minute, second, arr[index_2])), 398 | Check::Skipped(arr) => new_date_time(arr[index_2]), 399 | }; 400 | 401 | check_equal_option_date_time(&found_date_times.unique(), &unique.map(indexed_date_time).transpose()?); 402 | check_equal_option_date_time(&found_date_times.earliest(), &earlier.map(indexed_date_time).transpose()?); 403 | check_equal_option_date_time(&found_date_times.latest(), &later.map(indexed_date_time).transpose()?); 404 | 405 | let found_date_times_inner = found_date_times.into_inner(); 406 | assert_eq!(found_date_times_inner.len(), result_indices.len()); 407 | 408 | assert!(found_date_time_list.is_exhaustive()); 409 | assert_eq!(found_date_times_inner, buf.iter().copied().flatten().collect::>()); 410 | 411 | for (found_date_time, &result_index) in found_date_times_inner.iter().zip(result_indices) { 412 | match posssible_date_time_results[result_index] { 413 | Check::Normal([ut_offset]) => { 414 | assert_eq!(*found_date_time, FoundDateTimeKind::Normal(new_date_time((year, month, month_day, hour, minute, second, ut_offset))?)); 415 | } 416 | Check::Skipped([before, after]) => { 417 | let skipped = FoundDateTimeKind::Skipped { before_transition: new_date_time(before)?, after_transition: new_date_time(after)? }; 418 | assert_eq!(*found_date_time, skipped); 419 | } 420 | }; 421 | } 422 | 423 | Ok(()) 424 | } 425 | 426 | #[test] 427 | fn test_find_date_time_fixed() -> Result<(), TzError> { 428 | let local_time_type = LocalTimeType::with_ut_offset(3600)?; 429 | 430 | let results = &[Check::Normal([3600])]; 431 | 432 | let time_zone_1 = TimeZone::new(vec![], vec![local_time_type], vec![], None)?; 433 | let time_zone_2 = TimeZone::new(vec![], vec![local_time_type], vec![], Some(TransitionRule::Fixed(local_time_type)))?; 434 | 435 | check(time_zone_1.as_ref(), results, (2000, 1, 1, 0, 0, 0), &[0], Some([0, 0]), Some([0, 0]), Some([0, 0]))?; 436 | check(time_zone_2.as_ref(), results, (2000, 1, 1, 0, 0, 0), &[0], Some([0, 0]), Some([0, 0]), Some([0, 0]))?; 437 | 438 | let time_zone_3 = TimeZone::new(vec![Transition::new(0, 0)], vec![local_time_type], vec![], Some(TransitionRule::Fixed(local_time_type)))?; 439 | 440 | check(time_zone_3.as_ref(), results, (1960, 1, 1, 0, 0, 0), &[0], Some([0, 0]), Some([0, 0]), Some([0, 0]))?; 441 | check(time_zone_3.as_ref(), results, (1980, 1, 1, 0, 0, 0), &[0], Some([0, 0]), Some([0, 0]), Some([0, 0]))?; 442 | 443 | Ok(()) 444 | } 445 | 446 | #[test] 447 | fn test_find_date_time_no_offset() -> Result<(), TzError> { 448 | let local_time_types = [ 449 | LocalTimeType::new(0, false, Some(b"STD1"))?, 450 | LocalTimeType::new(0, true, Some(b"DST1"))?, 451 | LocalTimeType::new(0, false, Some(b"STD2"))?, 452 | LocalTimeType::new(0, true, Some(b"DST2"))?, 453 | ]; 454 | 455 | let time_zone = TimeZone::new( 456 | vec![Transition::new(3600, 1), Transition::new(7200, 2)], 457 | local_time_types.to_vec(), 458 | vec![], 459 | Some(TransitionRule::Alternate(AlternateTime::new( 460 | local_time_types[2], 461 | local_time_types[3], 462 | RuleDay::Julian0WithLeap(Julian0WithLeap::new(0)?), 463 | 10800, 464 | RuleDay::Julian0WithLeap(Julian0WithLeap::new(0)?), 465 | 14400, 466 | )?)), 467 | )?; 468 | 469 | let time_zone_ref = time_zone.as_ref(); 470 | 471 | let find_unique_local_time_type = |year, month, month_day, hour, minute, second, nanoseconds| -> Result<_, TzError> { 472 | let mut found_date_time_list = FoundDateTimeList::default(); 473 | find_date_time(&mut found_date_time_list, year, month, month_day, hour, minute, second, nanoseconds, time_zone_ref)?; 474 | 475 | let mut buf = [None; 1]; 476 | let mut found_date_time_list_ref_mut = FoundDateTimeListRefMut::new(&mut buf); 477 | find_date_time(&mut found_date_time_list_ref_mut, year, month, month_day, hour, minute, second, 0, time_zone_ref)?; 478 | assert!(found_date_time_list_ref_mut.is_exhaustive()); 479 | 480 | let datetime_1 = found_date_time_list.unique().unwrap(); 481 | let datetime_2 = found_date_time_list_ref_mut.unique().unwrap(); 482 | assert_eq!(datetime_1, datetime_2); 483 | 484 | Ok(*datetime_1.local_time_type()) 485 | }; 486 | 487 | assert_eq!(local_time_types[0], find_unique_local_time_type(1970, 1, 1, 0, 30, 0, 0)?); 488 | assert_eq!(local_time_types[1], find_unique_local_time_type(1970, 1, 1, 1, 30, 0, 0)?); 489 | assert_eq!(local_time_types[2], find_unique_local_time_type(1970, 1, 1, 2, 30, 0, 0)?); 490 | assert_eq!(local_time_types[3], find_unique_local_time_type(1970, 1, 1, 3, 30, 0, 0)?); 491 | assert_eq!(local_time_types[2], find_unique_local_time_type(1970, 1, 1, 4, 30, 0, 0)?); 492 | 493 | Ok(()) 494 | } 495 | 496 | #[test] 497 | fn test_find_date_time_extra_rule_only() -> Result<(), TzError> { 498 | let time_zone = TimeZone::new( 499 | vec![], 500 | vec![LocalTimeType::utc(), LocalTimeType::with_ut_offset(3600)?], 501 | vec![], 502 | Some(TransitionRule::Alternate(AlternateTime::new( 503 | LocalTimeType::utc(), 504 | LocalTimeType::with_ut_offset(3600)?, 505 | RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(1)?), 506 | 7200, 507 | RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(1)?), 508 | 12600, 509 | )?)), 510 | )?; 511 | 512 | let time_zone_ref = time_zone.as_ref(); 513 | 514 | let results = &[ 515 | Check::Normal([0]), 516 | Check::Normal([3600]), 517 | Check::Skipped([(2000, 1, 1, 2, 0, 0, 0), (2000, 1, 1, 3, 0, 0, 3600)]), 518 | Check::Skipped([(2010, 1, 1, 2, 0, 0, 0), (2010, 1, 1, 3, 0, 0, 3600)]), 519 | ]; 520 | 521 | check(time_zone_ref, results, (2000, 1, 1, 1, 45, 0), &[0], Some([0, 0]), Some([0, 0]), Some([0, 0]))?; 522 | check(time_zone_ref, results, (2000, 1, 1, 2, 15, 0), &[2], None, Some([2, 0]), Some([2, 1]))?; 523 | check(time_zone_ref, results, (2000, 1, 1, 2, 45, 0), &[2, 0], None, Some([2, 0]), Some([0, 0]))?; 524 | check(time_zone_ref, results, (2000, 1, 1, 3, 15, 0), &[1, 0], None, Some([1, 0]), Some([0, 0]))?; 525 | check(time_zone_ref, results, (2000, 1, 1, 3, 45, 0), &[0], Some([0, 0]), Some([0, 0]), Some([0, 0]))?; 526 | 527 | check(time_zone_ref, results, (2010, 1, 1, 1, 45, 0), &[0], Some([0, 0]), Some([0, 0]), Some([0, 0]))?; 528 | check(time_zone_ref, results, (2010, 1, 1, 2, 15, 0), &[3], None, Some([3, 0]), Some([3, 1]))?; 529 | check(time_zone_ref, results, (2010, 1, 1, 2, 45, 0), &[3, 0], None, Some([3, 0]), Some([0, 0]))?; 530 | check(time_zone_ref, results, (2010, 1, 1, 3, 15, 0), &[1, 0], None, Some([1, 0]), Some([0, 0]))?; 531 | check(time_zone_ref, results, (2010, 1, 1, 3, 45, 0), &[0], Some([0, 0]), Some([0, 0]), Some([0, 0]))?; 532 | 533 | Ok(()) 534 | } 535 | 536 | #[test] 537 | fn test_find_date_time_transitions_only() -> Result<(), TzError> { 538 | let time_zone = TimeZone::new( 539 | vec![ 540 | Transition::new(0, 0), 541 | Transition::new(7200, 1), 542 | Transition::new(14400, 2), 543 | Transition::new(25200, 3), 544 | Transition::new(28800, 4), 545 | Transition::new(32400, 0), 546 | ], 547 | vec![ 548 | LocalTimeType::new(0, false, None)?, 549 | LocalTimeType::new(3600, false, None)?, 550 | LocalTimeType::new(-10800, false, None)?, 551 | LocalTimeType::new(-19800, false, None)?, 552 | LocalTimeType::new(-16200, false, None)?, 553 | ], 554 | vec![], 555 | None, 556 | )?; 557 | 558 | let time_zone_ref = time_zone.as_ref(); 559 | 560 | let results = &[ 561 | Check::Normal([0]), 562 | Check::Normal([3600]), 563 | Check::Normal([-10800]), 564 | Check::Normal([-19800]), 565 | Check::Normal([-16200]), 566 | Check::Skipped([(1970, 1, 1, 2, 0, 0, 0), (1970, 1, 1, 3, 0, 0, 3600)]), 567 | Check::Skipped([(1970, 1, 1, 2, 30, 0, -19800), (1970, 1, 1, 3, 30, 0, -16200)]), 568 | ]; 569 | 570 | check(time_zone_ref, results, (1970, 1, 1, 0, 0, 0), &[0], Some([0, 0]), Some([0, 0]), Some([0, 0]))?; 571 | check(time_zone_ref, results, (1970, 1, 1, 1, 0, 0), &[0, 2], None, Some([0, 0]), Some([2, 0]))?; 572 | check(time_zone_ref, results, (1970, 1, 1, 1, 15, 0), &[0, 2], None, Some([0, 0]), Some([2, 0]))?; 573 | check(time_zone_ref, results, (1970, 1, 1, 1, 30, 0), &[0, 2, 3], None, Some([0, 0]), Some([3, 0]))?; 574 | check(time_zone_ref, results, (1970, 1, 1, 1, 45, 0), &[0, 2, 3], None, Some([0, 0]), Some([3, 0]))?; 575 | check(time_zone_ref, results, (1970, 1, 1, 2, 0, 0), &[5, 2, 3], None, Some([5, 0]), Some([3, 0]))?; 576 | check(time_zone_ref, results, (1970, 1, 1, 2, 15, 0), &[5, 2, 3], None, Some([5, 0]), Some([3, 0]))?; 577 | check(time_zone_ref, results, (1970, 1, 1, 2, 30, 0), &[5, 2, 6], None, Some([5, 0]), Some([6, 1]))?; 578 | check(time_zone_ref, results, (1970, 1, 1, 2, 45, 0), &[5, 2, 6], None, Some([5, 0]), Some([6, 1]))?; 579 | check(time_zone_ref, results, (1970, 1, 1, 3, 0, 0), &[1, 2, 6], None, Some([1, 0]), Some([6, 1]))?; 580 | check(time_zone_ref, results, (1970, 1, 1, 3, 15, 0), &[1, 2, 6], None, Some([1, 0]), Some([6, 1]))?; 581 | check(time_zone_ref, results, (1970, 1, 1, 3, 30, 0), &[1, 2, 4], None, Some([1, 0]), Some([4, 0]))?; 582 | check(time_zone_ref, results, (1970, 1, 1, 3, 45, 0), &[1, 2, 4], None, Some([1, 0]), Some([4, 0]))?; 583 | check(time_zone_ref, results, (1970, 1, 1, 4, 0, 0), &[1, 4], None, Some([1, 0]), Some([4, 0]))?; 584 | check(time_zone_ref, results, (1970, 1, 1, 4, 15, 0), &[1, 4], None, Some([1, 0]), Some([4, 0]))?; 585 | check(time_zone_ref, results, (1970, 1, 1, 4, 30, 0), &[1], Some([1, 0]), Some([1, 0]), Some([1, 0]))?; 586 | check(time_zone_ref, results, (1970, 1, 1, 4, 45, 0), &[1], Some([1, 0]), Some([1, 0]), Some([1, 0]))?; 587 | check(time_zone_ref, results, (1970, 1, 1, 5, 0, 0), &[], None, None, None)?; 588 | 589 | Ok(()) 590 | } 591 | 592 | #[test] 593 | fn test_find_date_time_transitions_with_extra_rule() -> Result<(), TzError> { 594 | let time_zone = TimeZone::new( 595 | vec![Transition::new(0, 0), Transition::new(3600, 1), Transition::new(7200, 0), Transition::new(10800, 2)], 596 | vec![LocalTimeType::utc(), LocalTimeType::with_ut_offset(i32::MAX)?, LocalTimeType::with_ut_offset(3600)?], 597 | vec![], 598 | Some(TransitionRule::Alternate(AlternateTime::new( 599 | LocalTimeType::utc(), 600 | LocalTimeType::with_ut_offset(3600)?, 601 | RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(300)?), 602 | 0, 603 | RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(90)?), 604 | 3600, 605 | )?)), 606 | )?; 607 | 608 | let time_zone_ref = time_zone.as_ref(); 609 | 610 | let results = &[ 611 | Check::Normal([0]), 612 | Check::Normal([3600]), 613 | Check::Normal([i32::MAX]), 614 | Check::Skipped([(1970, 1, 1, 1, 0, 0, 0), (2038, 1, 19, 4, 14, 7, i32::MAX)]), 615 | Check::Skipped([(1970, 1, 1, 3, 0, 0, 0), (1970, 1, 1, 4, 0, 0, 3600)]), 616 | Check::Skipped([(1970, 10, 27, 0, 0, 0, 0), (1970, 10, 27, 1, 0, 0, 3600)]), 617 | Check::Skipped([(2000, 10, 27, 0, 0, 0, 0), (2000, 10, 27, 1, 0, 0, 3600)]), 618 | Check::Skipped([(2030, 10, 27, 0, 0, 0, 0), (2030, 10, 27, 1, 0, 0, 3600)]), 619 | Check::Skipped([(2038, 10, 27, 0, 0, 0, 0), (2038, 10, 27, 1, 0, 0, 3600)]), 620 | ]; 621 | 622 | check(time_zone_ref, results, (1970, 1, 1, 0, 30, 0), &[0], Some([0, 0]), Some([0, 0]), Some([0, 0]))?; 623 | check(time_zone_ref, results, (1970, 1, 1, 1, 30, 0), &[3], None, Some([3, 0]), Some([3, 1]))?; 624 | check(time_zone_ref, results, (1970, 1, 1, 2, 30, 0), &[3, 0], None, Some([3, 0]), Some([0, 0]))?; 625 | check(time_zone_ref, results, (1970, 1, 1, 3, 30, 0), &[3, 4], None, Some([3, 0]), Some([4, 1]))?; 626 | check(time_zone_ref, results, (1970, 1, 1, 4, 30, 0), &[3, 1], None, Some([3, 0]), Some([1, 0]))?; 627 | 628 | check(time_zone_ref, results, (1970, 2, 1, 0, 0, 0), &[3, 1], None, Some([3, 0]), Some([1, 0]))?; 629 | check(time_zone_ref, results, (1970, 3, 31, 0, 30, 0), &[3, 1, 0], None, Some([3, 0]), Some([0, 0]))?; 630 | check(time_zone_ref, results, (1970, 6, 1, 0, 0, 0), &[3, 0], None, Some([3, 0]), Some([0, 0]))?; 631 | check(time_zone_ref, results, (1970, 10, 27, 0, 30, 0), &[3, 5], None, Some([3, 0]), Some([5, 1]))?; 632 | check(time_zone_ref, results, (1970, 11, 1, 0, 0, 0), &[3, 1], None, Some([3, 0]), Some([1, 0]))?; 633 | 634 | check(time_zone_ref, results, (2000, 2, 1, 0, 0, 0), &[3, 1], None, Some([3, 0]), Some([1, 0]))?; 635 | check(time_zone_ref, results, (2000, 3, 31, 0, 30, 0), &[3, 1, 0], None, Some([3, 0]), Some([0, 0]))?; 636 | check(time_zone_ref, results, (2000, 6, 1, 0, 0, 0), &[3, 0], None, Some([3, 0]), Some([0, 0]))?; 637 | check(time_zone_ref, results, (2000, 10, 27, 0, 30, 0), &[3, 6], None, Some([3, 0]), Some([6, 1]))?; 638 | check(time_zone_ref, results, (2000, 11, 1, 0, 0, 0), &[3, 1], None, Some([3, 0]), Some([1, 0]))?; 639 | 640 | check(time_zone_ref, results, (2030, 2, 1, 0, 0, 0), &[3, 1], None, Some([3, 0]), Some([1, 0]))?; 641 | check(time_zone_ref, results, (2030, 3, 31, 0, 30, 0), &[3, 1, 0], None, Some([3, 0]), Some([0, 0]))?; 642 | check(time_zone_ref, results, (2030, 6, 1, 0, 0, 0), &[3, 0], None, Some([3, 0]), Some([0, 0]))?; 643 | check(time_zone_ref, results, (2030, 10, 27, 0, 30, 0), &[3, 7], None, Some([3, 0]), Some([7, 1]))?; 644 | check(time_zone_ref, results, (2030, 11, 1, 0, 0, 0), &[3, 1], None, Some([3, 0]), Some([1, 0]))?; 645 | 646 | check(time_zone_ref, results, (2038, 1, 19, 5, 0, 0), &[2, 1], None, Some([2, 0]), Some([1, 0]))?; 647 | check(time_zone_ref, results, (2038, 2, 1, 0, 0, 0), &[1], Some([1, 0]), Some([1, 0]), Some([1, 0]))?; 648 | check(time_zone_ref, results, (2038, 3, 31, 0, 30, 0), &[1, 0], None, Some([1, 0]), Some([0, 0]))?; 649 | check(time_zone_ref, results, (2038, 6, 1, 0, 0, 0), &[0], Some([0, 0]), Some([0, 0]), Some([0, 0]))?; 650 | check(time_zone_ref, results, (2038, 10, 27, 0, 30, 0), &[8], None, Some([8, 0]), Some([8, 1]))?; 651 | check(time_zone_ref, results, (2038, 11, 1, 0, 0, 0), &[1], Some([1, 0]), Some([1, 0]), Some([1, 0]))?; 652 | 653 | Ok(()) 654 | } 655 | 656 | #[test] 657 | fn test_find_date_time_ref_mut() -> Result<(), TzError> { 658 | let transitions = &[Transition::new(3600, 1), Transition::new(86400, 0), Transition::new(i64::MAX, 0)]; 659 | let local_time_types = &[LocalTimeType::new(0, false, Some(b"STD"))?, LocalTimeType::new(3600, true, Some(b"DST"))?]; 660 | let time_zone_ref = TimeZoneRef::new(transitions, local_time_types, &[], &None)?; 661 | 662 | let mut small_buf = [None; 1]; 663 | let mut found_date_time_small_list = FoundDateTimeListRefMut::new(&mut small_buf); 664 | find_date_time(&mut found_date_time_small_list, 1970, 1, 2, 0, 30, 0, 0, time_zone_ref)?; 665 | assert!(!found_date_time_small_list.is_exhaustive()); 666 | 667 | let mut buf = [None; 2]; 668 | let mut found_date_time_list_1 = FoundDateTimeListRefMut::new(&mut buf); 669 | find_date_time(&mut found_date_time_list_1, 1970, 1, 2, 0, 30, 0, 0, time_zone_ref)?; 670 | let data = found_date_time_list_1.data(); 671 | assert!(found_date_time_list_1.is_exhaustive()); 672 | assert_eq!(found_date_time_list_1.count(), 2); 673 | assert!(matches!(data, [Some(FoundDateTimeKind::Normal(..)), Some(FoundDateTimeKind::Normal(..))])); 674 | 675 | let mut found_date_time_list_2 = FoundDateTimeListRefMut::new(&mut buf); 676 | find_date_time(&mut found_date_time_list_2, 1970, 1, 1, 1, 30, 0, 0, time_zone_ref)?; 677 | let data = found_date_time_list_2.data(); 678 | assert!(found_date_time_list_2.is_exhaustive()); 679 | assert_eq!(found_date_time_list_2.count(), 1); 680 | assert!(found_date_time_list_2.unique().is_none()); 681 | assert!(matches!(data, &[Some(FoundDateTimeKind::Skipped { .. })])); 682 | 683 | Ok(()) 684 | } 685 | } 686 | -------------------------------------------------------------------------------- /src/error/datetime.rs: -------------------------------------------------------------------------------- 1 | //! Date time error types. 2 | 3 | use core::error::Error; 4 | use core::fmt; 5 | 6 | /// Date time error 7 | #[non_exhaustive] 8 | #[derive(Debug)] 9 | pub enum DateTimeError { 10 | /// Invalid month 11 | InvalidMonth, 12 | /// Invalid month day 13 | InvalidMonthDay, 14 | /// Invalid hour 15 | InvalidHour, 16 | /// Invalid minute 17 | InvalidMinute, 18 | /// Invalid second 19 | InvalidSecond, 20 | /// Invalid nanoseconds 21 | InvalidNanoseconds, 22 | } 23 | 24 | impl fmt::Display for DateTimeError { 25 | fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 26 | match self { 27 | Self::InvalidMonth => f.write_str("invalid month"), 28 | Self::InvalidMonthDay => f.write_str("invalid month day"), 29 | Self::InvalidHour => f.write_str("invalid hour"), 30 | Self::InvalidMinute => f.write_str("invalid minute"), 31 | Self::InvalidSecond => f.write_str("invalid second"), 32 | Self::InvalidNanoseconds => f.write_str("invalid nanoseconds"), 33 | } 34 | } 35 | } 36 | 37 | impl Error for DateTimeError {} 38 | -------------------------------------------------------------------------------- /src/error/mod.rs: -------------------------------------------------------------------------------- 1 | //! Error types. 2 | 3 | pub mod datetime; 4 | pub mod timezone; 5 | 6 | #[cfg(feature = "alloc")] 7 | pub mod parse; 8 | 9 | use datetime::DateTimeError; 10 | use timezone::{LocalTimeTypeError, TimeZoneError, TransitionRuleError}; 11 | 12 | #[cfg(feature = "alloc")] 13 | use parse::{TzFileError, TzStringError}; 14 | 15 | use core::error; 16 | use core::fmt; 17 | 18 | #[cfg(feature = "alloc")] 19 | use alloc::boxed::Box; 20 | 21 | /// Unified error type for everything in the crate 22 | #[non_exhaustive] 23 | #[derive(Debug)] 24 | pub enum Error { 25 | /// I/O error 26 | #[cfg(feature = "alloc")] 27 | Io(Box), 28 | /// Unified error type for every non I/O error in the crate 29 | Tz(TzError), 30 | } 31 | 32 | impl fmt::Display for Error { 33 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 34 | match self { 35 | #[cfg(feature = "alloc")] 36 | Self::Io(error) => error.fmt(f), 37 | Self::Tz(error) => error.fmt(f), 38 | } 39 | } 40 | } 41 | 42 | impl error::Error for Error {} 43 | 44 | impl> From for Error { 45 | fn from(error: T) -> Self { 46 | Self::Tz(error.into()) 47 | } 48 | } 49 | 50 | /// Unified error type for every non I/O error in the crate 51 | #[non_exhaustive] 52 | #[derive(Debug)] 53 | pub enum TzError { 54 | /// Unified error for parsing a TZif file 55 | #[cfg(feature = "alloc")] 56 | TzFile(TzFileError), 57 | /// Unified error for parsing a TZ string 58 | #[cfg(feature = "alloc")] 59 | TzString(TzStringError), 60 | /// Local time type error 61 | LocalTimeType(LocalTimeTypeError), 62 | /// Transition rule error 63 | TransitionRule(TransitionRuleError), 64 | /// Time zone error 65 | TimeZone(TimeZoneError), 66 | /// Date time error 67 | DateTime(DateTimeError), 68 | /// Out of range operation 69 | OutOfRange, 70 | /// No available local time type 71 | NoAvailableLocalTimeType, 72 | } 73 | 74 | impl fmt::Display for TzError { 75 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 76 | match self { 77 | #[cfg(feature = "alloc")] 78 | Self::TzFile(error) => write!(f, "invalid TZ file: {error}"), 79 | #[cfg(feature = "alloc")] 80 | Self::TzString(error) => write!(f, "invalid TZ string: {error}"), 81 | Self::LocalTimeType(error) => write!(f, "invalid local time type: {error}"), 82 | Self::TransitionRule(error) => write!(f, "invalid transition rule: {error}"), 83 | Self::TimeZone(error) => write!(f, "invalid time zone: {error}"), 84 | Self::DateTime(error) => write!(f, "invalid date time: {error}"), 85 | Self::OutOfRange => f.write_str("out of range operation"), 86 | Self::NoAvailableLocalTimeType => write!(f, "no local time type is available for the specified timestamp"), 87 | } 88 | } 89 | } 90 | 91 | impl error::Error for TzError {} 92 | 93 | #[cfg(feature = "alloc")] 94 | impl From for TzError { 95 | fn from(error: TzFileError) -> Self { 96 | Self::TzFile(error) 97 | } 98 | } 99 | 100 | #[cfg(feature = "alloc")] 101 | impl From for TzError { 102 | fn from(error: TzStringError) -> Self { 103 | Self::TzString(error) 104 | } 105 | } 106 | 107 | impl From for TzError { 108 | fn from(error: LocalTimeTypeError) -> Self { 109 | Self::LocalTimeType(error) 110 | } 111 | } 112 | 113 | impl From for TzError { 114 | fn from(error: TransitionRuleError) -> Self { 115 | Self::TransitionRule(error) 116 | } 117 | } 118 | 119 | impl From for TzError { 120 | fn from(error: TimeZoneError) -> Self { 121 | Self::TimeZone(error) 122 | } 123 | } 124 | 125 | impl From for TzError { 126 | fn from(error: DateTimeError) -> Self { 127 | Self::DateTime(error) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/error/parse.rs: -------------------------------------------------------------------------------- 1 | //! Parsing error types. 2 | 3 | use core::error::Error; 4 | use core::fmt; 5 | use core::num::ParseIntError; 6 | use core::str::Utf8Error; 7 | 8 | /// Parse data error 9 | #[non_exhaustive] 10 | #[derive(Debug)] 11 | pub enum ParseDataError { 12 | /// Unexpected end of data 13 | UnexpectedEof, 14 | /// Invalid data 15 | InvalidData, 16 | } 17 | 18 | impl fmt::Display for ParseDataError { 19 | fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 20 | match self { 21 | Self::UnexpectedEof => f.write_str("unexpected end of data"), 22 | Self::InvalidData => f.write_str("invalid data"), 23 | } 24 | } 25 | } 26 | 27 | impl Error for ParseDataError {} 28 | 29 | /// Unified error type for parsing a TZ string 30 | #[non_exhaustive] 31 | #[derive(Debug)] 32 | pub enum TzStringError { 33 | /// UTF-8 error 34 | Utf8(Utf8Error), 35 | /// Integer parsing error 36 | ParseInt(ParseIntError), 37 | /// Parse data error 38 | ParseData(ParseDataError), 39 | /// Invalid offset hour 40 | InvalidOffsetHour, 41 | /// Invalid offset minute 42 | InvalidOffsetMinute, 43 | /// Invalid offset second 44 | InvalidOffsetSecond, 45 | /// Invalid day time hour 46 | InvalidDayTimeHour, 47 | /// Invalid day time minute 48 | InvalidDayTimeMinute, 49 | /// Invalid day time second 50 | InvalidDayTimeSecond, 51 | /// Missing DST start and end rules 52 | MissingDstStartEndRules, 53 | /// Remaining data was found after parsing TZ string 54 | RemainingData, 55 | /// Empty TZ string 56 | Empty, 57 | } 58 | 59 | impl fmt::Display for TzStringError { 60 | fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 61 | match self { 62 | Self::Utf8(error) => error.fmt(f), 63 | Self::ParseInt(error) => error.fmt(f), 64 | Self::ParseData(error) => error.fmt(f), 65 | Self::InvalidOffsetHour => f.write_str("invalid offset hour"), 66 | Self::InvalidOffsetMinute => f.write_str("invalid offset minute"), 67 | Self::InvalidOffsetSecond => f.write_str("invalid offset second"), 68 | Self::InvalidDayTimeHour => f.write_str("invalid day time hour"), 69 | Self::InvalidDayTimeMinute => f.write_str("invalid day time minute"), 70 | Self::InvalidDayTimeSecond => f.write_str("invalid day time second"), 71 | Self::MissingDstStartEndRules => f.write_str("missing DST start and end rules"), 72 | Self::RemainingData => f.write_str("remaining data after parsing TZ string"), 73 | Self::Empty => f.write_str("empty TZ string"), 74 | } 75 | } 76 | } 77 | 78 | impl Error for TzStringError {} 79 | 80 | impl From for TzStringError { 81 | fn from(error: Utf8Error) -> Self { 82 | Self::Utf8(error) 83 | } 84 | } 85 | 86 | impl From for TzStringError { 87 | fn from(error: ParseIntError) -> Self { 88 | Self::ParseInt(error) 89 | } 90 | } 91 | 92 | impl From for TzStringError { 93 | fn from(error: ParseDataError) -> Self { 94 | Self::ParseData(error) 95 | } 96 | } 97 | 98 | /// Unified error type for parsing a TZif file 99 | #[non_exhaustive] 100 | #[derive(Debug)] 101 | pub enum TzFileError { 102 | /// UTF-8 error 103 | Utf8(Utf8Error), 104 | /// Parse data error 105 | ParseData(ParseDataError), 106 | /// Invalid magic number 107 | InvalidMagicNumber, 108 | /// Unsupported TZif version 109 | UnsupportedTzFileVersion, 110 | /// Invalid header 111 | InvalidHeader, 112 | /// Invalid footer 113 | InvalidFooter, 114 | /// Invalid DST indicator 115 | InvalidDstIndicator, 116 | /// Invalid time zone designation char index 117 | InvalidTimeZoneDesignationCharIndex, 118 | /// Invalid couple of standard/wall and UT/local indicators 119 | InvalidStdWallUtLocal, 120 | /// Remaining data after the end of a TZif v1 data block 121 | RemainingDataV1, 122 | } 123 | 124 | impl fmt::Display for TzFileError { 125 | fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 126 | match self { 127 | Self::Utf8(error) => error.fmt(f), 128 | Self::ParseData(error) => error.fmt(f), 129 | Self::InvalidMagicNumber => f.write_str("invalid magic number"), 130 | Self::UnsupportedTzFileVersion => write!(f, "unsupported TZ file version"), 131 | Self::InvalidHeader => f.write_str("invalid header"), 132 | Self::InvalidFooter => f.write_str("invalid footer"), 133 | Self::InvalidDstIndicator => f.write_str("invalid DST indicator"), 134 | Self::InvalidTimeZoneDesignationCharIndex => f.write_str("invalid time zone designation char index"), 135 | Self::InvalidStdWallUtLocal => f.write_str("invalid couple of standard/wall and UT/local indicators"), 136 | Self::RemainingDataV1 => f.write_str("remaining data after the end of a TZif v1 data block"), 137 | } 138 | } 139 | } 140 | 141 | impl Error for TzFileError {} 142 | 143 | impl From for TzFileError { 144 | fn from(error: Utf8Error) -> Self { 145 | Self::Utf8(error) 146 | } 147 | } 148 | 149 | impl From for TzFileError { 150 | fn from(error: ParseDataError) -> Self { 151 | Self::ParseData(error) 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/error/timezone.rs: -------------------------------------------------------------------------------- 1 | //! Time zone error types. 2 | 3 | use core::error::Error; 4 | use core::fmt; 5 | 6 | /// Local time type error 7 | #[non_exhaustive] 8 | #[derive(Debug)] 9 | pub enum LocalTimeTypeError { 10 | /// Invalid time zone designation length 11 | InvalidTimeZoneDesignationLength, 12 | /// Invalid characters in time zone designation 13 | InvalidTimeZoneDesignationChar, 14 | /// Invalid UTC offset 15 | InvalidUtcOffset, 16 | } 17 | 18 | impl fmt::Display for LocalTimeTypeError { 19 | fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 20 | match self { 21 | Self::InvalidTimeZoneDesignationLength => f.write_str("time zone designation must have between 3 and 7 characters"), 22 | Self::InvalidTimeZoneDesignationChar => f.write_str("invalid characters in time zone designation"), 23 | Self::InvalidUtcOffset => f.write_str("invalid UTC offset"), 24 | } 25 | } 26 | } 27 | 28 | impl Error for LocalTimeTypeError {} 29 | 30 | /// Transition rule error 31 | #[non_exhaustive] 32 | #[derive(Debug)] 33 | pub enum TransitionRuleError { 34 | /// Invalid rule day julian day 35 | InvalidRuleDayJulianDay, 36 | /// Invalid rule day month 37 | InvalidRuleDayMonth, 38 | /// Invalid rule day week 39 | InvalidRuleDayWeek, 40 | /// Invalid rule day week day 41 | InvalidRuleDayWeekDay, 42 | /// Invalid standard time UTC offset 43 | InvalidStdUtcOffset, 44 | /// Invalid Daylight Saving Time UTC offset 45 | InvalidDstUtcOffset, 46 | /// Invalid DST start or end time 47 | InvalidDstStartEndTime, 48 | /// Inconsistent DST transition rules from one year to another 49 | InconsistentRule, 50 | } 51 | 52 | impl fmt::Display for TransitionRuleError { 53 | fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 54 | match self { 55 | Self::InvalidRuleDayJulianDay => f.write_str("invalid rule day julian day"), 56 | Self::InvalidRuleDayMonth => f.write_str("invalid rule day month"), 57 | Self::InvalidRuleDayWeek => f.write_str("invalid rule day week"), 58 | Self::InvalidRuleDayWeekDay => f.write_str("invalid rule day week day"), 59 | Self::InvalidStdUtcOffset => f.write_str("invalid standard time UTC offset"), 60 | Self::InvalidDstUtcOffset => f.write_str("invalid Daylight Saving Time UTC offset"), 61 | Self::InvalidDstStartEndTime => f.write_str("invalid DST start or end time"), 62 | Self::InconsistentRule => f.write_str("DST transition rules are not consistent from one year to another"), 63 | } 64 | } 65 | } 66 | 67 | impl Error for TransitionRuleError {} 68 | 69 | /// Time zone error 70 | #[non_exhaustive] 71 | #[derive(Debug)] 72 | pub enum TimeZoneError { 73 | /// No local time type 74 | NoLocalTimeType, 75 | /// Invalid local time type index 76 | InvalidLocalTimeTypeIndex, 77 | /// Invalid transition 78 | InvalidTransition, 79 | /// Invalid leap second 80 | InvalidLeapSecond, 81 | /// Inconsistent extra transition rule relative to the last transition 82 | InconsistentExtraRule, 83 | } 84 | 85 | impl fmt::Display for TimeZoneError { 86 | fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { 87 | match self { 88 | Self::NoLocalTimeType => f.write_str("list of local time types must not be empty"), 89 | Self::InvalidLocalTimeTypeIndex => f.write_str("invalid local time type index"), 90 | Self::InvalidTransition => f.write_str("invalid transition"), 91 | Self::InvalidLeapSecond => f.write_str("invalid leap second"), 92 | Self::InconsistentExtraRule => f.write_str("extra transition rule is inconsistent with the last transition"), 93 | } 94 | } 95 | } 96 | 97 | impl Error for TimeZoneError {} 98 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![forbid(unsafe_code)] 2 | #![deny(missing_docs)] 3 | #![cfg_attr(not(feature = "std"), no_std)] 4 | #![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] 5 | 6 | //! This crate provides the [`TimeZone`] and [`DateTime`] types, which can be used to determine local time on a given time zone. 7 | //! 8 | //! This allows to convert between an [Unix timestamp](https://en.wikipedia.org/wiki/Unix_time) and a calendar time expressed in the [proleptic gregorian calendar](https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar) with a provided time zone. 9 | //! 10 | //! Time zones are provided to the library with a [POSIX `TZ` string](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html) which can be read from the environment. 11 | //! 12 | //! Two formats are currently accepted for the `TZ` string: 13 | //! * `std offset[dst[offset][,start[/time],end[/time]]]` providing a time zone description, 14 | //! * `file` or `:file` providing the path to a [TZif file](https://datatracker.ietf.org/doc/html/rfc8536), which is absolute or relative to the system timezone directory. 15 | //! 16 | //! See also the [Linux manual page of tzset(3)](https://man7.org/linux/man-pages/man3/tzset.3.html) and the [glibc documentation of the `TZ` environment variable](https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html). 17 | //! 18 | //! # Usage 19 | //! 20 | //! ## Time zone 21 | //! 22 | //! ```rust 23 | //! # fn main() -> Result<(), tz::Error> { 24 | //! # #[cfg(feature = "std")] { 25 | //! use tz::TimeZone; 26 | //! 27 | //! // 2000-01-01T00:00:00Z 28 | //! let unix_time = 946684800; 29 | //! 30 | //! // Get UTC time zone 31 | //! let time_zone_utc = TimeZone::utc(); 32 | //! assert_eq!(time_zone_utc.find_local_time_type(unix_time)?.ut_offset(), 0); 33 | //! 34 | //! // Get fixed time zone at GMT-1 35 | //! let time_zone_fixed = TimeZone::fixed(-3600)?; 36 | //! assert_eq!(time_zone_fixed.find_local_time_type(unix_time)?.ut_offset(), -3600); 37 | //! 38 | //! // Get local time zone (UNIX only) 39 | //! let time_zone_local = TimeZone::local()?; 40 | //! // Get the current local time type 41 | //! let _current_local_time_type = time_zone_local.find_current_local_time_type()?; 42 | //! 43 | //! // Get time zone from a TZ string: 44 | //! // From an absolute file 45 | //! let _ = TimeZone::from_posix_tz("/usr/share/zoneinfo/Pacific/Auckland"); 46 | //! // From a file relative to the system timezone directory 47 | //! let _ = TimeZone::from_posix_tz("Pacific/Auckland"); 48 | //! // From a time zone description 49 | //! TimeZone::from_posix_tz("HST10")?; 50 | //! TimeZone::from_posix_tz("<-03>3")?; 51 | //! TimeZone::from_posix_tz("NZST-12:00:00NZDT-13:00:00,M10.1.0,M3.3.0")?; 52 | //! // Use a leading colon to force searching for a corresponding file 53 | //! let _ = TimeZone::from_posix_tz(":UTC"); 54 | //! # } 55 | //! # Ok(()) 56 | //! # } 57 | //! ``` 58 | //! 59 | //! ## Date time 60 | //! 61 | //! ```rust 62 | //! # fn main() -> Result<(), tz::Error> { 63 | //! # #[cfg(feature = "std")] { 64 | //! use tz::{DateTime, LocalTimeType, TimeZone, UtcDateTime}; 65 | //! 66 | //! // Get the current UTC date time 67 | //! let _current_utc_date_time = UtcDateTime::now()?; 68 | //! 69 | //! // Create a new UTC date time (2000-01-01T00:00:00.123456789Z) 70 | //! let utc_date_time = UtcDateTime::new(2000, 1, 1, 0, 0, 0, 123_456_789)?; 71 | //! assert_eq!(utc_date_time.year(), 2000); 72 | //! assert_eq!(utc_date_time.month(), 1); 73 | //! assert_eq!(utc_date_time.month_day(), 1); 74 | //! assert_eq!(utc_date_time.hour(), 0); 75 | //! assert_eq!(utc_date_time.minute(), 0); 76 | //! assert_eq!(utc_date_time.second(), 0); 77 | //! assert_eq!(utc_date_time.week_day(), 6); 78 | //! assert_eq!(utc_date_time.year_day(), 0); 79 | //! assert_eq!(utc_date_time.unix_time(), 946684800); 80 | //! assert_eq!(utc_date_time.nanoseconds(), 123_456_789); 81 | //! assert_eq!(utc_date_time.to_string(), "2000-01-01T00:00:00.123456789Z"); 82 | //! 83 | //! // Create a new UTC date time from a Unix time with nanoseconds (2000-01-01T00:00:00.123456789Z) 84 | //! let other_utc_date_time = UtcDateTime::from_timespec(946684800, 123_456_789)?; 85 | //! assert_eq!(other_utc_date_time, utc_date_time); 86 | //! 87 | //! // Project the UTC date time to a time zone 88 | //! let date_time = utc_date_time.project(TimeZone::fixed(-3600)?.as_ref())?; 89 | //! assert_eq!(date_time.year(), 1999); 90 | //! assert_eq!(date_time.month(), 12); 91 | //! assert_eq!(date_time.month_day(), 31); 92 | //! assert_eq!(date_time.hour(), 23); 93 | //! assert_eq!(date_time.minute(), 0); 94 | //! assert_eq!(date_time.second(), 0); 95 | //! assert_eq!(date_time.week_day(), 5); 96 | //! assert_eq!(date_time.year_day(), 364); 97 | //! assert_eq!(date_time.local_time_type().ut_offset(), -3600); 98 | //! assert_eq!(date_time.unix_time(), 946684800); 99 | //! assert_eq!(date_time.nanoseconds(), 123_456_789); 100 | //! assert_eq!(date_time.to_string(), "1999-12-31T23:00:00.123456789-01:00"); 101 | //! 102 | //! // Project the date time to another time zone 103 | //! let other_date_time = date_time.project(TimeZone::fixed(3600)?.as_ref())?; 104 | //! assert_eq!(other_date_time.year(), 2000); 105 | //! assert_eq!(other_date_time.month(), 1); 106 | //! assert_eq!(other_date_time.month_day(), 1); 107 | //! assert_eq!(other_date_time.hour(), 1); 108 | //! assert_eq!(other_date_time.minute(), 0); 109 | //! assert_eq!(other_date_time.second(), 0); 110 | //! assert_eq!(other_date_time.week_day(), 6); 111 | //! assert_eq!(other_date_time.year_day(), 0); 112 | //! assert_eq!(other_date_time.local_time_type().ut_offset(), 3600); 113 | //! assert_eq!(other_date_time.unix_time(), 946684800); 114 | //! assert_eq!(other_date_time.nanoseconds(), 123_456_789); 115 | //! assert_eq!(other_date_time.to_string(), "2000-01-01T01:00:00.123456789+01:00"); 116 | //! 117 | //! // Create a new date time from a Unix time with nanoseconds and a time zone (2000-01-01T00:00:00.123456789Z) 118 | //! let another_date_time = DateTime::from_timespec(946684800, 123_456_789, TimeZone::fixed(86400)?.as_ref())?; 119 | //! 120 | //! // DateTime objects are compared by their Unix time and nanoseconds 121 | //! assert_eq!(another_date_time, other_date_time); 122 | //! 123 | //! // Get the current date time at the local time zone (UNIX only) 124 | //! let time_zone_local = TimeZone::local()?; 125 | //! let _date_time = DateTime::now(time_zone_local.as_ref())?; 126 | //! 127 | //! // Create a new date time with an UTC offset (2000-01-01T01:00:00.123456789+01:00) 128 | //! let date_time = DateTime::new(2000, 1, 1, 1, 0, 0, 123_456_789, LocalTimeType::with_ut_offset(3600)?)?; 129 | //! assert_eq!(date_time.year(), 2000); 130 | //! assert_eq!(date_time.month(), 1); 131 | //! assert_eq!(date_time.month_day(), 1); 132 | //! assert_eq!(date_time.hour(), 1); 133 | //! assert_eq!(date_time.minute(), 0); 134 | //! assert_eq!(date_time.second(), 0); 135 | //! assert_eq!(date_time.week_day(), 6); 136 | //! assert_eq!(date_time.year_day(), 0); 137 | //! assert_eq!(date_time.unix_time(), 946684800); 138 | //! assert_eq!(date_time.nanoseconds(), 123_456_789); 139 | //! assert_eq!(date_time.to_string(), "2000-01-01T01:00:00.123456789+01:00"); 140 | //! 141 | //! // 142 | //! // Find the possible date times corresponding to a date, a time and a time zone 143 | //! // 144 | //! let time_zone = TimeZone::from_posix_tz("CET-1CEST,M3.5.0,M10.5.0/3")?; 145 | //! 146 | //! // Found date time is unique 147 | //! let found_date_times = DateTime::find(2000, 1, 1, 0, 0, 0, 0, time_zone.as_ref())?; 148 | //! let unique = found_date_times.unique().unwrap(); 149 | //! assert_eq!(unique, found_date_times.earliest().unwrap()); 150 | //! assert_eq!(unique, found_date_times.latest().unwrap()); 151 | //! assert_eq!(unique.local_time_type().ut_offset(), 3600); 152 | //! assert_eq!(unique.local_time_type().time_zone_designation(), "CET"); 153 | //! 154 | //! // Found date time was skipped by a forward transition 155 | //! let found_date_times = DateTime::find(2000, 3, 26, 2, 30, 0, 0, time_zone.as_ref())?; 156 | //! 157 | //! assert_eq!(found_date_times.unique(), None); 158 | //! 159 | //! let earliest = found_date_times.earliest().unwrap(); 160 | //! assert_eq!(earliest.hour(), 2); 161 | //! assert_eq!(earliest.minute(), 0); 162 | //! assert_eq!(earliest.local_time_type().ut_offset(), 3600); 163 | //! assert_eq!(earliest.local_time_type().time_zone_designation(), "CET"); 164 | //! 165 | //! let latest = found_date_times.latest().unwrap(); 166 | //! assert_eq!(latest.hour(), 3); 167 | //! assert_eq!(latest.minute(), 0); 168 | //! assert_eq!(latest.local_time_type().ut_offset(), 7200); 169 | //! assert_eq!(latest.local_time_type().time_zone_designation(), "CEST"); 170 | //! 171 | //! // Found date time is ambiguous because of a backward transition 172 | //! let found_date_times = DateTime::find(2000, 10, 29, 2, 30, 0, 0, time_zone.as_ref())?; 173 | //! 174 | //! assert_eq!(found_date_times.unique(), None); 175 | //! 176 | //! let earliest = found_date_times.earliest().unwrap(); 177 | //! assert_eq!(earliest.hour(), 2); 178 | //! assert_eq!(earliest.minute(), 30); 179 | //! assert_eq!(earliest.local_time_type().ut_offset(), 7200); 180 | //! assert_eq!(earliest.local_time_type().time_zone_designation(), "CEST"); 181 | //! 182 | //! let latest = found_date_times.latest().unwrap(); 183 | //! assert_eq!(latest.hour(), 2); 184 | //! assert_eq!(latest.minute(), 30); 185 | //! assert_eq!(latest.local_time_type().ut_offset(), 3600); 186 | //! assert_eq!(latest.local_time_type().time_zone_designation(), "CET"); 187 | //! # } 188 | //! # Ok(()) 189 | //! # } 190 | //! ``` 191 | //! 192 | //! # No std 193 | //! 194 | //! This crate can be used in `no_std` context. 195 | //! 196 | //! The `settings.rs` example shows how to construct a [`TimeZone`] by specifying a custom `read_file` function via the [`TimeZoneSettings`] struct. 197 | //! 198 | 199 | #[cfg(feature = "alloc")] 200 | extern crate alloc; 201 | 202 | mod constants; 203 | mod utils; 204 | 205 | #[cfg(feature = "alloc")] 206 | mod parse; 207 | 208 | pub mod datetime; 209 | pub mod error; 210 | pub mod timezone; 211 | 212 | #[doc(inline)] 213 | pub use datetime::{DateTime, UtcDateTime}; 214 | 215 | #[doc(inline)] 216 | pub use error::{Error, TzError}; 217 | 218 | #[doc(inline)] 219 | pub use timezone::{LocalTimeType, TimeZoneRef}; 220 | 221 | #[doc(inline)] 222 | #[cfg(feature = "alloc")] 223 | pub use timezone::{TimeZone, TimeZoneSettings}; 224 | -------------------------------------------------------------------------------- /src/parse/mod.rs: -------------------------------------------------------------------------------- 1 | //! Parsing functions. 2 | 3 | mod tz_file; 4 | mod tz_string; 5 | mod utils; 6 | 7 | pub(crate) use tz_file::parse_tz_file; 8 | pub(crate) use tz_string::parse_posix_tz; 9 | -------------------------------------------------------------------------------- /src/parse/tz_file.rs: -------------------------------------------------------------------------------- 1 | //! Functions used for parsing a TZif file. 2 | 3 | use crate::error::parse::TzFileError; 4 | use crate::error::TzError; 5 | use crate::parse::tz_string::parse_posix_tz; 6 | use crate::parse::utils::{read_chunk_exact, read_exact, Cursor}; 7 | use crate::timezone::{LeapSecond, LocalTimeType, TimeZone, Transition, TransitionRule}; 8 | 9 | use alloc::vec::Vec; 10 | use core::iter; 11 | use core::str; 12 | 13 | /// TZif version 14 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 15 | enum Version { 16 | /// Version 1 17 | V1, 18 | /// Version 2 19 | V2, 20 | /// Version 3 21 | V3, 22 | } 23 | 24 | /// TZif header 25 | #[derive(Debug)] 26 | struct Header { 27 | /// TZif version 28 | version: Version, 29 | /// Number of UT/local indicators 30 | ut_local_count: usize, 31 | /// Number of standard/wall indicators 32 | std_wall_count: usize, 33 | /// Number of leap-second records 34 | leap_count: usize, 35 | /// Number of transition times 36 | transition_count: usize, 37 | /// Number of local time type records 38 | type_count: usize, 39 | /// Number of time zone designations bytes 40 | char_count: usize, 41 | } 42 | 43 | /// Parse TZif header 44 | fn parse_header(cursor: &mut Cursor<'_>) -> Result { 45 | let magic = read_exact(cursor, 4)?; 46 | if magic != *b"TZif" { 47 | return Err(TzFileError::InvalidMagicNumber); 48 | } 49 | 50 | let version = match read_exact(cursor, 1)? { 51 | [0x00] => Version::V1, 52 | [0x32] => Version::V2, 53 | [0x33] => Version::V3, 54 | _ => return Err(TzFileError::UnsupportedTzFileVersion), 55 | }; 56 | 57 | read_exact(cursor, 15)?; 58 | 59 | let ut_local_count = u32::from_be_bytes(*read_chunk_exact(cursor)?); 60 | let std_wall_count = u32::from_be_bytes(*read_chunk_exact(cursor)?); 61 | let leap_count = u32::from_be_bytes(*read_chunk_exact(cursor)?); 62 | let transition_count = u32::from_be_bytes(*read_chunk_exact(cursor)?); 63 | let type_count = u32::from_be_bytes(*read_chunk_exact(cursor)?); 64 | let char_count = u32::from_be_bytes(*read_chunk_exact(cursor)?); 65 | 66 | if !(type_count != 0 && char_count != 0 && (ut_local_count == 0 || ut_local_count == type_count) && (std_wall_count == 0 || std_wall_count == type_count)) { 67 | return Err(TzFileError::InvalidHeader); 68 | } 69 | 70 | Ok(Header { 71 | version, 72 | ut_local_count: ut_local_count as usize, 73 | std_wall_count: std_wall_count as usize, 74 | leap_count: leap_count as usize, 75 | transition_count: transition_count as usize, 76 | type_count: type_count as usize, 77 | char_count: char_count as usize, 78 | }) 79 | } 80 | 81 | /// Parse TZif footer 82 | fn parse_footer(footer: &[u8], use_string_extensions: bool) -> Result, TzError> { 83 | let footer = str::from_utf8(footer).map_err(TzFileError::from)?; 84 | if !(footer.starts_with('\n') && footer.ends_with('\n')) { 85 | return Err(TzError::TzFile(TzFileError::InvalidFooter)); 86 | } 87 | 88 | let tz_string = footer.trim_matches(|c: char| c.is_ascii_whitespace()); 89 | if tz_string.starts_with(':') || tz_string.contains('\0') { 90 | return Err(TzError::TzFile(TzFileError::InvalidFooter)); 91 | } 92 | 93 | if !tz_string.is_empty() { 94 | Ok(Some(parse_posix_tz(tz_string.as_bytes(), use_string_extensions)).transpose()?) 95 | } else { 96 | Ok(None) 97 | } 98 | } 99 | 100 | /// TZif data blocks 101 | struct DataBlocks<'a, const TIME_SIZE: usize> { 102 | /// Transition times data block 103 | transition_times: &'a [u8], 104 | /// Transition types data block 105 | transition_types: &'a [u8], 106 | /// Local time types data block 107 | local_time_types: &'a [u8], 108 | /// Time zone designations data block 109 | time_zone_designations: &'a [u8], 110 | /// Leap seconds data block 111 | leap_seconds: &'a [u8], 112 | /// UT/local indicators data block 113 | std_walls: &'a [u8], 114 | /// Standard/wall indicators data block 115 | ut_locals: &'a [u8], 116 | } 117 | 118 | /// Read TZif data blocks 119 | fn read_data_blocks<'a, const TIME_SIZE: usize>(cursor: &mut Cursor<'a>, header: &Header) -> Result, TzFileError> { 120 | Ok(DataBlocks { 121 | transition_times: read_exact(cursor, header.transition_count * TIME_SIZE)?, 122 | transition_types: read_exact(cursor, header.transition_count)?, 123 | local_time_types: read_exact(cursor, header.type_count * 6)?, 124 | time_zone_designations: read_exact(cursor, header.char_count)?, 125 | leap_seconds: read_exact(cursor, header.leap_count * (TIME_SIZE + 4))?, 126 | std_walls: read_exact(cursor, header.std_wall_count)?, 127 | ut_locals: read_exact(cursor, header.ut_local_count)?, 128 | }) 129 | } 130 | 131 | trait ParseTime { 132 | type TimeData; 133 | 134 | fn parse_time(&self, data: &Self::TimeData) -> i64; 135 | } 136 | 137 | impl ParseTime for DataBlocks<'_, 4> { 138 | type TimeData = [u8; 4]; 139 | 140 | fn parse_time(&self, data: &Self::TimeData) -> i64 { 141 | i32::from_be_bytes(*data).into() 142 | } 143 | } 144 | 145 | impl ParseTime for DataBlocks<'_, 8> { 146 | type TimeData = [u8; 8]; 147 | 148 | fn parse_time(&self, data: &Self::TimeData) -> i64 { 149 | i64::from_be_bytes(*data) 150 | } 151 | } 152 | 153 | impl<'a, const TIME_SIZE: usize> DataBlocks<'a, TIME_SIZE> 154 | where 155 | DataBlocks<'a, TIME_SIZE>: ParseTime, 156 | { 157 | /// Parse time zone data 158 | fn parse(&self, header: &Header, footer: Option<&[u8]>) -> Result { 159 | let mut transitions = Vec::with_capacity(header.transition_count); 160 | for (time_data, &local_time_type_index) in self.transition_times.chunks_exact(TIME_SIZE).zip(self.transition_types) { 161 | let time_data = time_data.first_chunk::().unwrap(); 162 | 163 | let unix_leap_time = self.parse_time(time_data); 164 | let local_time_type_index = local_time_type_index as usize; 165 | transitions.push(Transition::new(unix_leap_time, local_time_type_index)); 166 | } 167 | 168 | let mut local_time_types = Vec::with_capacity(header.type_count); 169 | for data in self.local_time_types.chunks_exact(6) { 170 | let [d0, d1, d2, d3, d4, d5] = <[u8; 6]>::try_from(data).unwrap(); 171 | 172 | let ut_offset = i32::from_be_bytes([d0, d1, d2, d3]); 173 | 174 | let is_dst = match d4 { 175 | 0 => false, 176 | 1 => true, 177 | _ => return Err(TzError::TzFile(TzFileError::InvalidDstIndicator)), 178 | }; 179 | 180 | let char_index = d5 as usize; 181 | if char_index >= header.char_count { 182 | return Err(TzError::TzFile(TzFileError::InvalidTimeZoneDesignationCharIndex)); 183 | } 184 | 185 | let time_zone_designation = match self.time_zone_designations[char_index..].iter().position(|&c| c == b'\0') { 186 | None => return Err(TzError::TzFile(TzFileError::InvalidTimeZoneDesignationCharIndex)), 187 | Some(position) => { 188 | let time_zone_designation = &self.time_zone_designations[char_index..char_index + position]; 189 | 190 | if !time_zone_designation.is_empty() { 191 | Some(time_zone_designation) 192 | } else { 193 | None 194 | } 195 | } 196 | }; 197 | 198 | local_time_types.push(LocalTimeType::new(ut_offset, is_dst, time_zone_designation)?); 199 | } 200 | 201 | let mut leap_seconds = Vec::with_capacity(header.leap_count); 202 | for data in self.leap_seconds.chunks_exact(TIME_SIZE + 4) { 203 | let (time_data, tail) = data.split_first_chunk::().unwrap(); 204 | let correction_data = tail.first_chunk::<4>().unwrap(); 205 | 206 | let unix_leap_time = self.parse_time(time_data); 207 | let correction = i32::from_be_bytes(*correction_data); 208 | leap_seconds.push(LeapSecond::new(unix_leap_time, correction)); 209 | } 210 | 211 | let std_walls_iter = self.std_walls.iter().copied().chain(iter::repeat(0)); 212 | let ut_locals_iter = self.ut_locals.iter().copied().chain(iter::repeat(0)); 213 | for (std_wall, ut_local) in std_walls_iter.zip(ut_locals_iter).take(header.type_count) { 214 | if !matches!((std_wall, ut_local), (0, 0) | (1, 0) | (1, 1)) { 215 | return Err(TzError::TzFile(TzFileError::InvalidStdWallUtLocal)); 216 | } 217 | } 218 | 219 | let extra_rule = footer.and_then(|footer| parse_footer(footer, header.version == Version::V3).transpose()).transpose()?; 220 | 221 | TimeZone::new(transitions, local_time_types, leap_seconds, extra_rule) 222 | } 223 | } 224 | 225 | /// Parse TZif file as described in [RFC 8536](https://datatracker.ietf.org/doc/html/rfc8536) 226 | pub(crate) fn parse_tz_file(bytes: &[u8]) -> Result { 227 | let mut cursor = bytes; 228 | 229 | let header = parse_header(&mut cursor)?; 230 | 231 | match header.version { 232 | Version::V1 => { 233 | let data_blocks = read_data_blocks::<4>(&mut cursor, &header)?; 234 | 235 | if !cursor.is_empty() { 236 | return Err(TzError::TzFile(TzFileError::RemainingDataV1)); 237 | } 238 | 239 | Ok(data_blocks.parse(&header, None)?) 240 | } 241 | Version::V2 | Version::V3 => { 242 | // Skip v1 data block 243 | read_data_blocks::<4>(&mut cursor, &header)?; 244 | 245 | let header = parse_header(&mut cursor)?; 246 | let data_blocks = read_data_blocks::<8>(&mut cursor, &header)?; 247 | let footer = cursor; 248 | 249 | Ok(data_blocks.parse(&header, Some(footer))?) 250 | } 251 | } 252 | } 253 | 254 | #[cfg(test)] 255 | mod tests { 256 | use super::*; 257 | use crate::timezone::{AlternateTime, MonthWeekDay, RuleDay, TimeZone}; 258 | 259 | use alloc::vec; 260 | 261 | #[test] 262 | fn test_v1_file_with_leap_seconds() -> Result<(), TzError> { 263 | let bytes = b"TZif\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\x01\0\0\0\x1b\0\0\0\0\0\0\0\x01\0\0\0\x04\0\0\0\0\0\0UTC\0\x04\xb2\x58\0\0\0\0\x01\x05\xa4\xec\x01\0\0\0\x02\x07\x86\x1f\x82\0\0\0\x03\x09\x67\x53\x03\0\0\0\x04\x0b\x48\x86\x84\0\0\0\x05\x0d\x2b\x0b\x85\0\0\0\x06\x0f\x0c\x3f\x06\0\0\0\x07\x10\xed\x72\x87\0\0\0\x08\x12\xce\xa6\x08\0\0\0\x09\x15\x9f\xca\x89\0\0\0\x0a\x17\x80\xfe\x0a\0\0\0\x0b\x19\x62\x31\x8b\0\0\0\x0c\x1d\x25\xea\x0c\0\0\0\x0d\x21\xda\xe5\x0d\0\0\0\x0e\x25\x9e\x9d\x8e\0\0\0\x0f\x27\x7f\xd1\x0f\0\0\0\x10\x2a\x50\xf5\x90\0\0\0\x11\x2c\x32\x29\x11\0\0\0\x12\x2e\x13\x5c\x92\0\0\0\x13\x30\xe7\x24\x13\0\0\0\x14\x33\xb8\x48\x94\0\0\0\x15\x36\x8c\x10\x15\0\0\0\x16\x43\xb7\x1b\x96\0\0\0\x17\x49\x5c\x07\x97\0\0\0\x18\x4f\xef\x93\x18\0\0\0\x19\x55\x93\x2d\x99\0\0\0\x1a\x58\x68\x46\x9a\0\0\0\x1b\0\0"; 264 | 265 | let time_zone = parse_tz_file(bytes)?; 266 | 267 | let time_zone_result = TimeZone::new( 268 | vec![], 269 | vec![LocalTimeType::new(0, false, Some(b"UTC"))?], 270 | vec![ 271 | LeapSecond::new(78796800, 1), 272 | LeapSecond::new(94694401, 2), 273 | LeapSecond::new(126230402, 3), 274 | LeapSecond::new(157766403, 4), 275 | LeapSecond::new(189302404, 5), 276 | LeapSecond::new(220924805, 6), 277 | LeapSecond::new(252460806, 7), 278 | LeapSecond::new(283996807, 8), 279 | LeapSecond::new(315532808, 9), 280 | LeapSecond::new(362793609, 10), 281 | LeapSecond::new(394329610, 11), 282 | LeapSecond::new(425865611, 12), 283 | LeapSecond::new(489024012, 13), 284 | LeapSecond::new(567993613, 14), 285 | LeapSecond::new(631152014, 15), 286 | LeapSecond::new(662688015, 16), 287 | LeapSecond::new(709948816, 17), 288 | LeapSecond::new(741484817, 18), 289 | LeapSecond::new(773020818, 19), 290 | LeapSecond::new(820454419, 20), 291 | LeapSecond::new(867715220, 21), 292 | LeapSecond::new(915148821, 22), 293 | LeapSecond::new(1136073622, 23), 294 | LeapSecond::new(1230768023, 24), 295 | LeapSecond::new(1341100824, 25), 296 | LeapSecond::new(1435708825, 26), 297 | LeapSecond::new(1483228826, 27), 298 | ], 299 | None, 300 | )?; 301 | 302 | assert_eq!(time_zone, time_zone_result); 303 | 304 | Ok(()) 305 | } 306 | 307 | #[test] 308 | fn test_v2_file() -> Result<(), TzError> { 309 | let bytes = b"TZif2\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x06\0\0\0\x06\0\0\0\0\0\0\0\x07\0\0\0\x06\0\0\0\x14\x80\0\0\0\xbb\x05\x43\x48\xbb\x21\x71\x58\xcb\x89\x3d\xc8\xd2\x23\xf4\x70\xd2\x61\x49\x38\xd5\x8d\x73\x48\x01\x02\x01\x03\x04\x01\x05\xff\xff\x6c\x02\0\0\xff\xff\x6c\x58\0\x04\xff\xff\x7a\x68\x01\x08\xff\xff\x7a\x68\x01\x0c\xff\xff\x7a\x68\x01\x10\xff\xff\x73\x60\0\x04LMT\0HST\0HDT\0HWT\0HPT\0\0\0\0\0\x01\0\0\0\0\0\x01\0TZif2\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x06\0\0\0\x06\0\0\0\0\0\0\0\x07\0\0\0\x06\0\0\0\x14\xff\xff\xff\xff\x74\xe0\x70\xbe\xff\xff\xff\xff\xbb\x05\x43\x48\xff\xff\xff\xff\xbb\x21\x71\x58\xff\xff\xff\xff\xcb\x89\x3d\xc8\xff\xff\xff\xff\xd2\x23\xf4\x70\xff\xff\xff\xff\xd2\x61\x49\x38\xff\xff\xff\xff\xd5\x8d\x73\x48\x01\x02\x01\x03\x04\x01\x05\xff\xff\x6c\x02\0\0\xff\xff\x6c\x58\0\x04\xff\xff\x7a\x68\x01\x08\xff\xff\x7a\x68\x01\x0c\xff\xff\x7a\x68\x01\x10\xff\xff\x73\x60\0\x04LMT\0HST\0HDT\0HWT\0HPT\0\0\0\0\0\x01\0\0\0\0\0\x01\0\x0aHST10\x0a"; 310 | 311 | let time_zone = parse_tz_file(bytes)?; 312 | 313 | let time_zone_result = TimeZone::new( 314 | vec![ 315 | Transition::new(-2334101314, 1), 316 | Transition::new(-1157283000, 2), 317 | Transition::new(-1155436200, 1), 318 | Transition::new(-880198200, 3), 319 | Transition::new(-769395600, 4), 320 | Transition::new(-765376200, 1), 321 | Transition::new(-712150200, 5), 322 | ], 323 | vec![ 324 | LocalTimeType::new(-37886, false, Some(b"LMT"))?, 325 | LocalTimeType::new(-37800, false, Some(b"HST"))?, 326 | LocalTimeType::new(-34200, true, Some(b"HDT"))?, 327 | LocalTimeType::new(-34200, true, Some(b"HWT"))?, 328 | LocalTimeType::new(-34200, true, Some(b"HPT"))?, 329 | LocalTimeType::new(-36000, false, Some(b"HST"))?, 330 | ], 331 | vec![], 332 | Some(TransitionRule::Fixed(LocalTimeType::new(-36000, false, Some(b"HST"))?)), 333 | )?; 334 | 335 | assert_eq!(time_zone, time_zone_result); 336 | 337 | assert_eq!(*time_zone.find_local_time_type(-1156939200)?, LocalTimeType::new(-34200, true, Some(b"HDT"))?); 338 | assert_eq!(*time_zone.find_local_time_type(1546300800)?, LocalTimeType::new(-36000, false, Some(b"HST"))?); 339 | 340 | Ok(()) 341 | } 342 | 343 | #[test] 344 | fn test_v3_file() -> Result<(), TzError> { 345 | let bytes = b"TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\x04\0\0\x1c\x20\0\0IST\0TZif3\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\x01\0\0\0\x01\0\0\0\0\0\0\0\x01\0\0\0\x01\0\0\0\x04\0\0\0\0\x7f\xe8\x17\x80\0\0\0\x1c\x20\0\0IST\0\x01\x01\x0aIST-2IDT,M3.4.4/26,M10.5.0\x0a"; 346 | 347 | let time_zone = parse_tz_file(bytes)?; 348 | 349 | let time_zone_result = TimeZone::new( 350 | vec![Transition::new(2145916800, 0)], 351 | vec![LocalTimeType::new(7200, false, Some(b"IST"))?], 352 | vec![], 353 | Some(TransitionRule::Alternate(AlternateTime::new( 354 | LocalTimeType::new(7200, false, Some(b"IST"))?, 355 | LocalTimeType::new(10800, true, Some(b"IDT"))?, 356 | RuleDay::MonthWeekDay(MonthWeekDay::new(3, 4, 4)?), 357 | 93600, 358 | RuleDay::MonthWeekDay(MonthWeekDay::new(10, 5, 0)?), 359 | 7200, 360 | )?)), 361 | )?; 362 | 363 | assert_eq!(time_zone, time_zone_result); 364 | 365 | Ok(()) 366 | } 367 | } 368 | -------------------------------------------------------------------------------- /src/parse/tz_string.rs: -------------------------------------------------------------------------------- 1 | //! Functions used for parsing a TZ string. 2 | 3 | use crate::error::parse::{ParseDataError, TzStringError}; 4 | use crate::error::TzError; 5 | use crate::parse::utils::{read_exact, read_optional_tag, read_tag, read_until, read_while, Cursor}; 6 | use crate::timezone::{AlternateTime, Julian0WithLeap, Julian1WithoutLeap, LocalTimeType, MonthWeekDay, RuleDay, TransitionRule}; 7 | 8 | use core::num::ParseIntError; 9 | use core::str::{self, FromStr}; 10 | 11 | /// Convert the `Err` variant of a `Result` 12 | fn map_err(result: Result) -> Result { 13 | Ok(result?) 14 | } 15 | 16 | /// Parse integer from a slice of bytes 17 | fn parse_int>(bytes: &[u8]) -> Result { 18 | Ok(str::from_utf8(bytes)?.parse()?) 19 | } 20 | 21 | /// Parse time zone designation 22 | fn parse_time_zone_designation<'a>(cursor: &mut Cursor<'a>) -> Result<&'a [u8], ParseDataError> { 23 | let unquoted = if cursor.first() == Some(&b'<') { 24 | read_exact(cursor, 1)?; 25 | let unquoted = read_until(cursor, |&x| x == b'>')?; 26 | read_exact(cursor, 1)?; 27 | unquoted 28 | } else { 29 | read_while(cursor, u8::is_ascii_alphabetic)? 30 | }; 31 | 32 | Ok(unquoted) 33 | } 34 | 35 | /// Parse hours, minutes and seconds 36 | fn parse_hhmmss(cursor: &mut Cursor<'_>) -> Result<(i32, i32, i32), TzStringError> { 37 | let hour = parse_int(read_while(cursor, u8::is_ascii_digit)?)?; 38 | 39 | let mut minute = 0; 40 | let mut second = 0; 41 | 42 | if read_optional_tag(cursor, b":")? { 43 | minute = parse_int(read_while(cursor, u8::is_ascii_digit)?)?; 44 | 45 | if read_optional_tag(cursor, b":")? { 46 | second = parse_int(read_while(cursor, u8::is_ascii_digit)?)?; 47 | } 48 | } 49 | 50 | Ok((hour, minute, second)) 51 | } 52 | 53 | /// Parse signed hours, minutes and seconds 54 | fn parse_signed_hhmmss(cursor: &mut Cursor<'_>) -> Result<(i32, i32, i32, i32), TzStringError> { 55 | let mut sign = 1; 56 | if let Some(&c @ b'+') | Some(&c @ b'-') = cursor.first() { 57 | read_exact(cursor, 1)?; 58 | if c == b'-' { 59 | sign = -1; 60 | } 61 | } 62 | 63 | let (hour, minute, second) = parse_hhmmss(cursor)?; 64 | Ok((sign, hour, minute, second)) 65 | } 66 | 67 | /// Parse time zone offset 68 | fn parse_offset(cursor: &mut Cursor<'_>) -> Result { 69 | let (sign, hour, minute, second) = parse_signed_hhmmss(cursor)?; 70 | 71 | if !(0..=24).contains(&hour) { 72 | return Err(TzStringError::InvalidOffsetHour); 73 | } 74 | if !(0..=59).contains(&minute) { 75 | return Err(TzStringError::InvalidOffsetMinute); 76 | } 77 | if !(0..=59).contains(&second) { 78 | return Err(TzStringError::InvalidOffsetSecond); 79 | } 80 | 81 | Ok(sign * (hour * 3600 + minute * 60 + second)) 82 | } 83 | 84 | /// Parse transition rule day 85 | fn parse_rule_day(cursor: &mut Cursor<'_>) -> Result { 86 | match cursor.first() { 87 | Some(b'J') => { 88 | map_err(read_exact(cursor, 1))?; 89 | let data = map_err(read_while(cursor, u8::is_ascii_digit))?; 90 | Ok(RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(parse_int(data)?)?)) 91 | } 92 | Some(b'M') => { 93 | map_err(read_exact(cursor, 1))?; 94 | 95 | let month = parse_int(map_err(read_while(cursor, u8::is_ascii_digit))?)?; 96 | map_err(read_tag(cursor, b"."))?; 97 | let week = parse_int(map_err(read_while(cursor, u8::is_ascii_digit))?)?; 98 | map_err(read_tag(cursor, b"."))?; 99 | let week_day = parse_int(map_err(read_while(cursor, u8::is_ascii_digit))?)?; 100 | 101 | Ok(RuleDay::MonthWeekDay(MonthWeekDay::new(month, week, week_day)?)) 102 | } 103 | _ => { 104 | let data = map_err(read_while(cursor, u8::is_ascii_digit))?; 105 | Ok(RuleDay::Julian0WithLeap(Julian0WithLeap::new(parse_int(data)?)?)) 106 | } 107 | } 108 | } 109 | 110 | /// Parse transition rule time 111 | fn parse_rule_time(cursor: &mut Cursor<'_>) -> Result { 112 | let (hour, minute, second) = parse_hhmmss(cursor)?; 113 | 114 | if !(0..=24).contains(&hour) { 115 | return Err(TzStringError::InvalidDayTimeHour); 116 | } 117 | if !(0..=59).contains(&minute) { 118 | return Err(TzStringError::InvalidDayTimeMinute); 119 | } 120 | if !(0..=59).contains(&second) { 121 | return Err(TzStringError::InvalidDayTimeSecond); 122 | } 123 | 124 | Ok(hour * 3600 + minute * 60 + second) 125 | } 126 | 127 | /// Parse transition rule time with TZ string extensions 128 | fn parse_rule_time_extended(cursor: &mut Cursor<'_>) -> Result { 129 | let (sign, hour, minute, second) = parse_signed_hhmmss(cursor)?; 130 | 131 | if !(-167..=167).contains(&hour) { 132 | return Err(TzStringError::InvalidDayTimeHour); 133 | } 134 | if !(0..=59).contains(&minute) { 135 | return Err(TzStringError::InvalidDayTimeMinute); 136 | } 137 | if !(0..=59).contains(&second) { 138 | return Err(TzStringError::InvalidDayTimeSecond); 139 | } 140 | 141 | Ok(sign * (hour * 3600 + minute * 60 + second)) 142 | } 143 | 144 | /// Parse transition rule 145 | fn parse_rule_block(cursor: &mut Cursor<'_>, use_string_extensions: bool) -> Result<(RuleDay, i32), TzError> { 146 | let date = parse_rule_day(cursor)?; 147 | 148 | let time = if map_err(read_optional_tag(cursor, b"/"))? { 149 | if use_string_extensions { 150 | parse_rule_time_extended(cursor)? 151 | } else { 152 | parse_rule_time(cursor)? 153 | } 154 | } else { 155 | 2 * 3600 156 | }; 157 | 158 | Ok((date, time)) 159 | } 160 | 161 | /// Parse a POSIX TZ string containing a time zone description, as described in [the POSIX documentation of the `TZ` environment variable](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html). 162 | /// 163 | /// TZ string extensions from [RFC 8536](https://datatracker.ietf.org/doc/html/rfc8536#section-3.3.1) may be used. 164 | /// 165 | pub(crate) fn parse_posix_tz(tz_string: &[u8], use_string_extensions: bool) -> Result { 166 | let mut cursor = tz_string; 167 | 168 | let std_time_zone = Some(map_err(parse_time_zone_designation(&mut cursor))?); 169 | let std_offset = parse_offset(&mut cursor)?; 170 | 171 | if cursor.is_empty() { 172 | return Ok(TransitionRule::Fixed(LocalTimeType::new(-std_offset, false, std_time_zone)?)); 173 | } 174 | 175 | let dst_time_zone = Some(map_err(parse_time_zone_designation(&mut cursor))?); 176 | 177 | let dst_offset = match cursor.first() { 178 | Some(&b',') => std_offset - 3600, 179 | Some(_) => parse_offset(&mut cursor)?, 180 | None => return Err(TzError::TzString(TzStringError::MissingDstStartEndRules)), 181 | }; 182 | 183 | if cursor.is_empty() { 184 | return Err(TzError::TzString(TzStringError::MissingDstStartEndRules)); 185 | } 186 | 187 | map_err(read_tag(&mut cursor, b","))?; 188 | let (dst_start, dst_start_time) = parse_rule_block(&mut cursor, use_string_extensions)?; 189 | 190 | map_err(read_tag(&mut cursor, b","))?; 191 | let (dst_end, dst_end_time) = parse_rule_block(&mut cursor, use_string_extensions)?; 192 | 193 | if !cursor.is_empty() { 194 | return Err(TzError::TzString(TzStringError::RemainingData)); 195 | } 196 | 197 | Ok(TransitionRule::Alternate(AlternateTime::new( 198 | LocalTimeType::new(-std_offset, false, std_time_zone)?, 199 | LocalTimeType::new(-dst_offset, true, dst_time_zone)?, 200 | dst_start, 201 | dst_start_time, 202 | dst_end, 203 | dst_end_time, 204 | )?)) 205 | } 206 | 207 | #[cfg(test)] 208 | mod tests { 209 | use super::*; 210 | 211 | #[test] 212 | fn test_no_dst() -> Result<(), TzError> { 213 | let tz_string = b"HST10"; 214 | 215 | let transition_rule = parse_posix_tz(tz_string, false)?; 216 | let transition_rule_result = TransitionRule::Fixed(LocalTimeType::new(-36000, false, Some(b"HST"))?); 217 | 218 | assert_eq!(transition_rule, transition_rule_result); 219 | 220 | Ok(()) 221 | } 222 | 223 | #[test] 224 | fn test_quoted() -> Result<(), TzError> { 225 | let tz_string = b"<-03>+3<+03>-3,J1,J365"; 226 | 227 | let transition_rule = parse_posix_tz(tz_string, false)?; 228 | 229 | let transition_rule_result = TransitionRule::Alternate(AlternateTime::new( 230 | LocalTimeType::new(-10800, false, Some(b"-03"))?, 231 | LocalTimeType::new(10800, true, Some(b"+03"))?, 232 | RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(1)?), 233 | 7200, 234 | RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(365)?), 235 | 7200, 236 | )?); 237 | 238 | assert_eq!(transition_rule, transition_rule_result); 239 | 240 | Ok(()) 241 | } 242 | 243 | #[test] 244 | fn test_full() -> Result<(), TzError> { 245 | let tz_string = b"NZST-12:00:00NZDT-13:00:00,M10.1.0/02:00:00,M3.3.0/02:00:00"; 246 | 247 | let transition_rule = parse_posix_tz(tz_string, false)?; 248 | 249 | let transition_rule_result = TransitionRule::Alternate(AlternateTime::new( 250 | LocalTimeType::new(43200, false, Some(b"NZST"))?, 251 | LocalTimeType::new(46800, true, Some(b"NZDT"))?, 252 | RuleDay::MonthWeekDay(MonthWeekDay::new(10, 1, 0)?), 253 | 7200, 254 | RuleDay::MonthWeekDay(MonthWeekDay::new(3, 3, 0)?), 255 | 7200, 256 | )?); 257 | 258 | assert_eq!(transition_rule, transition_rule_result); 259 | 260 | Ok(()) 261 | } 262 | 263 | #[test] 264 | fn test_negative_dst() -> Result<(), TzError> { 265 | let tz_string = b"IST-1GMT0,M10.5.0,M3.5.0/1"; 266 | 267 | let transition_rule = parse_posix_tz(tz_string, false)?; 268 | 269 | let transition_rule_result = TransitionRule::Alternate(AlternateTime::new( 270 | LocalTimeType::new(3600, false, Some(b"IST"))?, 271 | LocalTimeType::new(0, true, Some(b"GMT"))?, 272 | RuleDay::MonthWeekDay(MonthWeekDay::new(10, 5, 0)?), 273 | 7200, 274 | RuleDay::MonthWeekDay(MonthWeekDay::new(3, 5, 0)?), 275 | 3600, 276 | )?); 277 | 278 | assert_eq!(transition_rule, transition_rule_result); 279 | 280 | Ok(()) 281 | } 282 | 283 | #[test] 284 | fn test_negative_hour() -> Result<(), TzError> { 285 | let tz_string = b"<-03>3<-02>,M3.5.0/-2,M10.5.0/-1"; 286 | 287 | assert!(parse_posix_tz(tz_string, false).is_err()); 288 | 289 | let transition_rule = parse_posix_tz(tz_string, true)?; 290 | 291 | let transition_rule_result = TransitionRule::Alternate(AlternateTime::new( 292 | LocalTimeType::new(-10800, false, Some(b"-03"))?, 293 | LocalTimeType::new(-7200, true, Some(b"-02"))?, 294 | RuleDay::MonthWeekDay(MonthWeekDay::new(3, 5, 0)?), 295 | -7200, 296 | RuleDay::MonthWeekDay(MonthWeekDay::new(10, 5, 0)?), 297 | -3600, 298 | )?); 299 | 300 | assert_eq!(transition_rule, transition_rule_result); 301 | 302 | Ok(()) 303 | } 304 | 305 | #[test] 306 | fn test_all_year_dst() -> Result<(), TzError> { 307 | let tz_string = b"EST5EDT,0/0,J365/25"; 308 | 309 | assert!(parse_posix_tz(tz_string, false).is_err()); 310 | 311 | let transition_rule = parse_posix_tz(tz_string, true)?; 312 | 313 | let transition_rule_result = TransitionRule::Alternate(AlternateTime::new( 314 | LocalTimeType::new(-18000, false, Some(b"EST"))?, 315 | LocalTimeType::new(-14400, true, Some(b"EDT"))?, 316 | RuleDay::Julian0WithLeap(Julian0WithLeap::new(0)?), 317 | 0, 318 | RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(365)?), 319 | 90000, 320 | )?); 321 | 322 | assert_eq!(transition_rule, transition_rule_result); 323 | 324 | Ok(()) 325 | } 326 | 327 | #[test] 328 | fn test_min_dst_offset() -> Result<(), TzError> { 329 | let tz_string = b"STD24:59:59DST,J1,J365"; 330 | 331 | let transition_rule = parse_posix_tz(tz_string, false)?; 332 | 333 | let transition_rule_result = TransitionRule::Alternate(AlternateTime::new( 334 | LocalTimeType::new(-89999, false, Some(b"STD"))?, 335 | LocalTimeType::new(-86399, true, Some(b"DST"))?, 336 | RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(1)?), 337 | 7200, 338 | RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(365)?), 339 | 7200, 340 | )?); 341 | 342 | assert_eq!(transition_rule, transition_rule_result); 343 | 344 | Ok(()) 345 | } 346 | 347 | #[test] 348 | fn test_max_dst_offset() -> Result<(), TzError> { 349 | let tz_string = b"STD-24:59:59DST,J1,J365"; 350 | 351 | let transition_rule = parse_posix_tz(tz_string, false)?; 352 | 353 | let transition_rule_result = TransitionRule::Alternate(AlternateTime::new( 354 | LocalTimeType::new(89999, false, Some(b"STD"))?, 355 | LocalTimeType::new(93599, true, Some(b"DST"))?, 356 | RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(1)?), 357 | 7200, 358 | RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(365)?), 359 | 7200, 360 | )?); 361 | 362 | assert_eq!(transition_rule, transition_rule_result); 363 | 364 | Ok(()) 365 | } 366 | 367 | #[test] 368 | fn test_error() -> Result<(), TzError> { 369 | assert!(matches!(parse_posix_tz(b"IST-1GMT0", false), Err(TzError::TzString(TzStringError::MissingDstStartEndRules)))); 370 | assert!(matches!(parse_posix_tz(b"EET-2EEST", false), Err(TzError::TzString(TzStringError::MissingDstStartEndRules)))); 371 | 372 | Ok(()) 373 | } 374 | } 375 | -------------------------------------------------------------------------------- /src/parse/utils.rs: -------------------------------------------------------------------------------- 1 | //! Some useful functions. 2 | 3 | use crate::error::parse::ParseDataError; 4 | 5 | /// Cursor type alias 6 | pub(super) type Cursor<'a> = &'a [u8]; 7 | 8 | /// Read exactly `count` bytes and reduce remaining data 9 | pub(super) fn read_exact<'a>(cursor: &mut Cursor<'a>, count: usize) -> Result<&'a [u8], ParseDataError> { 10 | match cursor.split_at_checked(count) { 11 | Some((result, tail)) => { 12 | *cursor = tail; 13 | Ok(result) 14 | } 15 | None => Err(ParseDataError::UnexpectedEof), 16 | } 17 | } 18 | 19 | /// Read exactly `N` bytes into an array and reduce remaining data 20 | pub(super) fn read_chunk_exact<'a, const N: usize>(cursor: &mut Cursor<'a>) -> Result<&'a [u8; N], ParseDataError> { 21 | match cursor.split_first_chunk::() { 22 | Some((result, tail)) => { 23 | *cursor = tail; 24 | Ok(result) 25 | } 26 | None => Err(ParseDataError::UnexpectedEof), 27 | } 28 | } 29 | 30 | /// Read bytes and compare them to the provided tag 31 | pub(super) fn read_tag(cursor: &mut Cursor<'_>, tag: &[u8]) -> Result<(), ParseDataError> { 32 | if read_exact(cursor, tag.len())? == tag { 33 | Ok(()) 34 | } else { 35 | Err(ParseDataError::InvalidData) 36 | } 37 | } 38 | 39 | /// Read bytes if the remaining data is prefixed by the provided tag 40 | pub(super) fn read_optional_tag(cursor: &mut Cursor<'_>, tag: &[u8]) -> Result { 41 | if cursor.starts_with(tag) { 42 | read_exact(cursor, tag.len())?; 43 | Ok(true) 44 | } else { 45 | Ok(false) 46 | } 47 | } 48 | 49 | /// Read bytes as long as the provided predicate is true 50 | pub(super) fn read_while<'a, F: Fn(&u8) -> bool>(cursor: &mut Cursor<'a>, f: F) -> Result<&'a [u8], ParseDataError> { 51 | read_exact(cursor, cursor.iter().position(|x| !f(x)).unwrap_or(cursor.len())) 52 | } 53 | 54 | /// Read bytes until the provided predicate is true 55 | pub(super) fn read_until<'a, F: Fn(&u8) -> bool>(cursor: &mut Cursor<'a>, f: F) -> Result<&'a [u8], ParseDataError> { 56 | read_exact(cursor, cursor.iter().position(f).unwrap_or(cursor.len())) 57 | } 58 | -------------------------------------------------------------------------------- /src/timezone/mod.rs: -------------------------------------------------------------------------------- 1 | //! Types related to a time zone. 2 | 3 | mod rule; 4 | 5 | #[doc(inline)] 6 | pub use rule::{AlternateTime, Julian0WithLeap, Julian1WithoutLeap, MonthWeekDay, RuleDay, TransitionRule}; 7 | 8 | use crate::error::timezone::{LocalTimeTypeError, TimeZoneError}; 9 | use crate::error::TzError; 10 | use crate::utils::{binary_search_leap_seconds, binary_search_transitions}; 11 | 12 | #[cfg(feature = "alloc")] 13 | use crate::{ 14 | error::parse::TzStringError, 15 | parse::{parse_posix_tz, parse_tz_file}, 16 | }; 17 | 18 | use core::fmt; 19 | use core::str; 20 | 21 | #[cfg(feature = "alloc")] 22 | use alloc::{boxed::Box, format, vec, vec::Vec}; 23 | 24 | /// Transition of a TZif file 25 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 26 | pub struct Transition { 27 | /// Unix leap time 28 | unix_leap_time: i64, 29 | /// Index specifying the local time type of the transition 30 | local_time_type_index: usize, 31 | } 32 | 33 | impl Transition { 34 | /// Construct a TZif file transition 35 | #[inline] 36 | pub const fn new(unix_leap_time: i64, local_time_type_index: usize) -> Self { 37 | Self { unix_leap_time, local_time_type_index } 38 | } 39 | 40 | /// Returns Unix leap time 41 | #[inline] 42 | pub const fn unix_leap_time(&self) -> i64 { 43 | self.unix_leap_time 44 | } 45 | 46 | /// Returns local time type index 47 | #[inline] 48 | pub const fn local_time_type_index(&self) -> usize { 49 | self.local_time_type_index 50 | } 51 | } 52 | 53 | /// Leap second of a TZif file 54 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 55 | pub struct LeapSecond { 56 | /// Unix leap time 57 | unix_leap_time: i64, 58 | /// Leap second correction 59 | correction: i32, 60 | } 61 | 62 | impl LeapSecond { 63 | /// Construct a TZif file leap second 64 | #[inline] 65 | pub const fn new(unix_leap_time: i64, correction: i32) -> Self { 66 | Self { unix_leap_time, correction } 67 | } 68 | 69 | /// Returns Unix leap time 70 | #[inline] 71 | pub const fn unix_leap_time(&self) -> i64 { 72 | self.unix_leap_time 73 | } 74 | 75 | /// Returns leap second correction 76 | #[inline] 77 | pub const fn correction(&self) -> i32 { 78 | self.correction 79 | } 80 | } 81 | 82 | /// ASCII-encoded fixed-capacity string, used for storing time zone designations 83 | #[derive(Copy, Clone, Eq, PartialEq)] 84 | struct TzAsciiStr { 85 | /// Length-prefixed string buffer 86 | bytes: [u8; 8], 87 | } 88 | 89 | impl TzAsciiStr { 90 | /// Construct a time zone designation string 91 | const fn new(input: &[u8]) -> Result { 92 | let len = input.len(); 93 | 94 | if !(3 <= len && len <= 7) { 95 | return Err(LocalTimeTypeError::InvalidTimeZoneDesignationLength); 96 | } 97 | 98 | let mut bytes = [0; 8]; 99 | bytes[0] = input.len() as u8; 100 | 101 | let mut i = 0; 102 | while i < len { 103 | let b = input[i]; 104 | 105 | if !matches!(b, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'+' | b'-') { 106 | return Err(LocalTimeTypeError::InvalidTimeZoneDesignationChar); 107 | } 108 | 109 | bytes[i + 1] = b; 110 | 111 | i += 1; 112 | } 113 | 114 | Ok(Self { bytes }) 115 | } 116 | 117 | /// Returns time zone designation as a byte slice 118 | #[inline] 119 | const fn as_bytes(&self) -> &[u8] { 120 | match &self.bytes { 121 | [3, head @ .., _, _, _, _] => head, 122 | [4, head @ .., _, _, _] => head, 123 | [5, head @ .., _, _] => head, 124 | [6, head @ .., _] => head, 125 | [7, head @ ..] => head, 126 | _ => unreachable!(), 127 | } 128 | } 129 | 130 | /// Returns time zone designation as a string 131 | #[inline] 132 | const fn as_str(&self) -> &str { 133 | match str::from_utf8(self.as_bytes()) { 134 | Ok(s) => s, 135 | Err(_) => panic!("unreachable code: ASCII is valid UTF-8"), 136 | } 137 | } 138 | 139 | /// Check if two time zone designations are equal 140 | #[inline] 141 | const fn equal(&self, other: &Self) -> bool { 142 | u64::from_ne_bytes(self.bytes) == u64::from_ne_bytes(other.bytes) 143 | } 144 | } 145 | 146 | impl fmt::Debug for TzAsciiStr { 147 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 148 | self.as_str().fmt(f) 149 | } 150 | } 151 | 152 | /// Local time type associated to a time zone 153 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 154 | pub struct LocalTimeType { 155 | /// Offset from UTC in seconds 156 | ut_offset: i32, 157 | /// Daylight Saving Time indicator 158 | is_dst: bool, 159 | /// Time zone designation 160 | time_zone_designation: Option, 161 | } 162 | 163 | impl LocalTimeType { 164 | /// Construct a local time type 165 | pub const fn new(ut_offset: i32, is_dst: bool, time_zone_designation: Option<&[u8]>) -> Result { 166 | if ut_offset == i32::MIN { 167 | return Err(LocalTimeTypeError::InvalidUtcOffset); 168 | } 169 | 170 | let time_zone_designation = match time_zone_designation { 171 | None => None, 172 | Some(time_zone_designation) => match TzAsciiStr::new(time_zone_designation) { 173 | Err(error) => return Err(error), 174 | Ok(time_zone_designation) => Some(time_zone_designation), 175 | }, 176 | }; 177 | 178 | Ok(Self { ut_offset, is_dst, time_zone_designation }) 179 | } 180 | 181 | /// Construct the local time type associated to UTC 182 | #[inline] 183 | pub const fn utc() -> Self { 184 | Self { ut_offset: 0, is_dst: false, time_zone_designation: None } 185 | } 186 | 187 | /// Construct a local time type with the specified UTC offset in seconds 188 | #[inline] 189 | pub const fn with_ut_offset(ut_offset: i32) -> Result { 190 | if ut_offset == i32::MIN { 191 | return Err(LocalTimeTypeError::InvalidUtcOffset); 192 | } 193 | 194 | Ok(Self { ut_offset, is_dst: false, time_zone_designation: None }) 195 | } 196 | 197 | /// Returns offset from UTC in seconds 198 | #[inline] 199 | pub const fn ut_offset(&self) -> i32 { 200 | self.ut_offset 201 | } 202 | 203 | /// Returns daylight saving time indicator 204 | #[inline] 205 | pub const fn is_dst(&self) -> bool { 206 | self.is_dst 207 | } 208 | 209 | /// Returns time zone designation 210 | #[inline] 211 | pub const fn time_zone_designation(&self) -> &str { 212 | match &self.time_zone_designation { 213 | Some(s) => s.as_str(), 214 | None => "", 215 | } 216 | } 217 | 218 | /// Check if two local time types are equal 219 | #[inline] 220 | const fn equal(&self, other: &Self) -> bool { 221 | self.ut_offset == other.ut_offset 222 | && self.is_dst == other.is_dst 223 | && match (&self.time_zone_designation, &other.time_zone_designation) { 224 | (Some(x), Some(y)) => x.equal(y), 225 | (None, None) => true, 226 | _ => false, 227 | } 228 | } 229 | } 230 | 231 | /// Reference to a time zone 232 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 233 | pub struct TimeZoneRef<'a> { 234 | /// List of transitions 235 | transitions: &'a [Transition], 236 | /// List of local time types (cannot be empty) 237 | local_time_types: &'a [LocalTimeType], 238 | /// List of leap seconds 239 | leap_seconds: &'a [LeapSecond], 240 | /// Extra transition rule applicable after the last transition 241 | extra_rule: &'a Option, 242 | } 243 | 244 | impl<'a> TimeZoneRef<'a> { 245 | /// Construct a time zone reference 246 | pub const fn new( 247 | transitions: &'a [Transition], 248 | local_time_types: &'a [LocalTimeType], 249 | leap_seconds: &'a [LeapSecond], 250 | extra_rule: &'a Option, 251 | ) -> Result { 252 | let time_zone_ref = Self::new_unchecked(transitions, local_time_types, leap_seconds, extra_rule); 253 | 254 | if let Err(error) = time_zone_ref.check_inputs() { 255 | return Err(error); 256 | } 257 | 258 | Ok(time_zone_ref) 259 | } 260 | 261 | /// Construct the time zone reference associated to UTC 262 | #[inline] 263 | pub const fn utc() -> Self { 264 | Self { transitions: &[], local_time_types: &[const { LocalTimeType::utc() }], leap_seconds: &[], extra_rule: &None } 265 | } 266 | 267 | /// Returns list of transitions 268 | #[inline] 269 | pub const fn transitions(&self) -> &'a [Transition] { 270 | self.transitions 271 | } 272 | 273 | /// Returns list of local time types 274 | #[inline] 275 | pub const fn local_time_types(&self) -> &'a [LocalTimeType] { 276 | self.local_time_types 277 | } 278 | 279 | /// Returns list of leap seconds 280 | #[inline] 281 | pub const fn leap_seconds(&self) -> &'a [LeapSecond] { 282 | self.leap_seconds 283 | } 284 | 285 | /// Returns extra transition rule applicable after the last transition 286 | #[inline] 287 | pub const fn extra_rule(&self) -> &'a Option { 288 | self.extra_rule 289 | } 290 | 291 | /// Find the local time type associated to the time zone at the specified Unix time in seconds 292 | pub const fn find_local_time_type(&self, unix_time: i64) -> Result<&'a LocalTimeType, TzError> { 293 | let extra_rule = match self.transitions { 294 | [] => match self.extra_rule { 295 | Some(extra_rule) => extra_rule, 296 | None => return Ok(&self.local_time_types[0]), 297 | }, 298 | [.., last_transition] => { 299 | let unix_leap_time = match self.unix_time_to_unix_leap_time(unix_time) { 300 | Ok(unix_leap_time) => unix_leap_time, 301 | Err(error) => return Err(error), 302 | }; 303 | 304 | if unix_leap_time >= last_transition.unix_leap_time { 305 | match self.extra_rule { 306 | Some(extra_rule) => extra_rule, 307 | None => return Err(TzError::NoAvailableLocalTimeType), 308 | } 309 | } else { 310 | let index = match binary_search_transitions(self.transitions, unix_leap_time) { 311 | Ok(x) => x + 1, 312 | Err(x) => x, 313 | }; 314 | 315 | let local_time_type_index = if index > 0 { self.transitions[index - 1].local_time_type_index } else { 0 }; 316 | return Ok(&self.local_time_types[local_time_type_index]); 317 | } 318 | } 319 | }; 320 | 321 | extra_rule.find_local_time_type(unix_time) 322 | } 323 | 324 | /// Construct a reference to a time zone 325 | #[inline] 326 | const fn new_unchecked( 327 | transitions: &'a [Transition], 328 | local_time_types: &'a [LocalTimeType], 329 | leap_seconds: &'a [LeapSecond], 330 | extra_rule: &'a Option, 331 | ) -> Self { 332 | Self { transitions, local_time_types, leap_seconds, extra_rule } 333 | } 334 | 335 | /// Check time zone inputs 336 | const fn check_inputs(&self) -> Result<(), TzError> { 337 | use crate::constants::*; 338 | 339 | // Check local time types 340 | let local_time_types_size = self.local_time_types.len(); 341 | if local_time_types_size == 0 { 342 | return Err(TzError::TimeZone(TimeZoneError::NoLocalTimeType)); 343 | } 344 | 345 | // Check transitions 346 | let mut i_transition = 0; 347 | while i_transition < self.transitions.len() { 348 | if self.transitions[i_transition].local_time_type_index >= local_time_types_size { 349 | return Err(TzError::TimeZone(TimeZoneError::InvalidLocalTimeTypeIndex)); 350 | } 351 | 352 | if i_transition + 1 < self.transitions.len() && self.transitions[i_transition].unix_leap_time >= self.transitions[i_transition + 1].unix_leap_time { 353 | return Err(TzError::TimeZone(TimeZoneError::InvalidTransition)); 354 | } 355 | 356 | i_transition += 1; 357 | } 358 | 359 | // Check leap seconds 360 | if !(self.leap_seconds.is_empty() || self.leap_seconds[0].unix_leap_time >= 0 && self.leap_seconds[0].correction.saturating_abs() == 1) { 361 | return Err(TzError::TimeZone(TimeZoneError::InvalidLeapSecond)); 362 | } 363 | 364 | let min_interval = SECONDS_PER_28_DAYS - 1; 365 | 366 | let mut i_leap_second = 0; 367 | while i_leap_second < self.leap_seconds.len() { 368 | if i_leap_second + 1 < self.leap_seconds.len() { 369 | let x0 = &self.leap_seconds[i_leap_second]; 370 | let x1 = &self.leap_seconds[i_leap_second + 1]; 371 | 372 | let diff_unix_leap_time = x1.unix_leap_time.saturating_sub(x0.unix_leap_time); 373 | let abs_diff_correction = x1.correction.saturating_sub(x0.correction).saturating_abs(); 374 | 375 | if !(diff_unix_leap_time >= min_interval && abs_diff_correction == 1) { 376 | return Err(TzError::TimeZone(TimeZoneError::InvalidLeapSecond)); 377 | } 378 | } 379 | i_leap_second += 1; 380 | } 381 | 382 | // Check extra rule 383 | if let (Some(extra_rule), [.., last_transition]) = (&self.extra_rule, self.transitions) { 384 | let last_local_time_type = &self.local_time_types[last_transition.local_time_type_index]; 385 | 386 | let unix_time = match self.unix_leap_time_to_unix_time(last_transition.unix_leap_time) { 387 | Ok(unix_time) => unix_time, 388 | Err(error) => return Err(error), 389 | }; 390 | 391 | let rule_local_time_type = match extra_rule.find_local_time_type(unix_time) { 392 | Ok(rule_local_time_type) => rule_local_time_type, 393 | Err(error) => return Err(error), 394 | }; 395 | 396 | if !last_local_time_type.equal(rule_local_time_type) { 397 | return Err(TzError::TimeZone(TimeZoneError::InconsistentExtraRule)); 398 | } 399 | } 400 | 401 | Ok(()) 402 | } 403 | 404 | /// Convert Unix time to Unix leap time, from the list of leap seconds in a time zone 405 | pub(crate) const fn unix_time_to_unix_leap_time(&self, unix_time: i64) -> Result { 406 | let mut unix_leap_time = unix_time; 407 | 408 | let mut i = 0; 409 | while i < self.leap_seconds.len() { 410 | let leap_second = &self.leap_seconds[i]; 411 | 412 | if unix_leap_time < leap_second.unix_leap_time { 413 | break; 414 | } 415 | 416 | unix_leap_time = match unix_time.checked_add(leap_second.correction as i64) { 417 | Some(unix_leap_time) => unix_leap_time, 418 | None => return Err(TzError::OutOfRange), 419 | }; 420 | 421 | i += 1; 422 | } 423 | 424 | Ok(unix_leap_time) 425 | } 426 | 427 | /// Convert Unix leap time to Unix time, from the list of leap seconds in a time zone 428 | pub(crate) const fn unix_leap_time_to_unix_time(&self, unix_leap_time: i64) -> Result { 429 | if unix_leap_time == i64::MIN { 430 | return Err(TzError::OutOfRange); 431 | } 432 | 433 | let index = match binary_search_leap_seconds(self.leap_seconds, unix_leap_time - 1) { 434 | Ok(x) => x + 1, 435 | Err(x) => x, 436 | }; 437 | 438 | let correction = if index > 0 { self.leap_seconds[index - 1].correction } else { 0 }; 439 | 440 | match unix_leap_time.checked_sub(correction as i64) { 441 | Some(unix_time) => Ok(unix_time), 442 | None => Err(TzError::OutOfRange), 443 | } 444 | } 445 | } 446 | 447 | /// Time zone 448 | #[cfg(feature = "alloc")] 449 | #[derive(Debug, Clone, Eq, PartialEq)] 450 | pub struct TimeZone { 451 | /// List of transitions 452 | transitions: Vec, 453 | /// List of local time types (cannot be empty) 454 | local_time_types: Vec, 455 | /// List of leap seconds 456 | leap_seconds: Vec, 457 | /// Extra transition rule applicable after the last transition 458 | extra_rule: Option, 459 | } 460 | 461 | #[cfg(feature = "alloc")] 462 | impl TimeZone { 463 | /// Construct a time zone 464 | pub fn new( 465 | transitions: Vec, 466 | local_time_types: Vec, 467 | leap_seconds: Vec, 468 | extra_rule: Option, 469 | ) -> Result { 470 | TimeZoneRef::new_unchecked(&transitions, &local_time_types, &leap_seconds, &extra_rule).check_inputs()?; 471 | Ok(Self { transitions, local_time_types, leap_seconds, extra_rule }) 472 | } 473 | 474 | /// Returns a reference to the time zone 475 | #[inline] 476 | pub fn as_ref(&self) -> TimeZoneRef<'_> { 477 | TimeZoneRef::new_unchecked(&self.transitions, &self.local_time_types, &self.leap_seconds, &self.extra_rule) 478 | } 479 | 480 | /// Construct the time zone associated to UTC 481 | #[inline] 482 | pub fn utc() -> Self { 483 | Self { transitions: Vec::new(), local_time_types: vec![LocalTimeType::utc()], leap_seconds: Vec::new(), extra_rule: None } 484 | } 485 | 486 | /// Construct a time zone with the specified UTC offset in seconds 487 | #[inline] 488 | pub fn fixed(ut_offset: i32) -> Result { 489 | Ok(Self { transitions: Vec::new(), local_time_types: vec![LocalTimeType::with_ut_offset(ut_offset)?], leap_seconds: Vec::new(), extra_rule: None }) 490 | } 491 | 492 | /// Find the local time type associated to the time zone at the specified Unix time in seconds 493 | pub fn find_local_time_type(&self, unix_time: i64) -> Result<&LocalTimeType, TzError> { 494 | self.as_ref().find_local_time_type(unix_time) 495 | } 496 | 497 | /// Construct a time zone from the contents of a time zone file 498 | pub fn from_tz_data(bytes: &[u8]) -> Result { 499 | parse_tz_file(bytes) 500 | } 501 | 502 | /// Returns local time zone. 503 | /// 504 | /// This method in not supported on non-UNIX platforms, and returns the UTC time zone instead. 505 | /// 506 | #[cfg(feature = "std")] 507 | pub fn local() -> Result { 508 | TimeZoneSettings::DEFAULT.parse_local() 509 | } 510 | 511 | /// Construct a time zone from a POSIX TZ string, as described in [the POSIX documentation of the `TZ` environment variable](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html). 512 | #[cfg(feature = "std")] 513 | pub fn from_posix_tz(tz_string: &str) -> Result { 514 | TimeZoneSettings::DEFAULT.parse_posix_tz(tz_string) 515 | } 516 | 517 | /// Find the current local time type associated to the time zone 518 | #[cfg(feature = "std")] 519 | pub fn find_current_local_time_type(&self) -> Result<&LocalTimeType, TzError> { 520 | self.find_local_time_type(crate::utils::current_unix_time()) 521 | } 522 | } 523 | 524 | /// Read file function type alias 525 | #[cfg(feature = "alloc")] 526 | type ReadFileFn = fn(path: &str) -> Result, Box>; 527 | 528 | /// Time zone settings 529 | #[cfg(feature = "alloc")] 530 | #[derive(Debug)] 531 | pub struct TimeZoneSettings<'a> { 532 | /// Possible system timezone directories 533 | directories: &'a [&'a str], 534 | /// Read file function 535 | read_file_fn: ReadFileFn, 536 | } 537 | 538 | #[cfg(feature = "alloc")] 539 | impl<'a> TimeZoneSettings<'a> { 540 | /// Default possible system timezone directories 541 | pub const DEFAULT_DIRECTORIES: &'static [&'static str] = &["/usr/share/zoneinfo", "/share/zoneinfo", "/etc/zoneinfo"]; 542 | 543 | /// Default read file function 544 | #[cfg(feature = "std")] 545 | pub const DEFAULT_READ_FILE_FN: ReadFileFn = |path| Ok(std::fs::read(path)?); 546 | 547 | /// Default time zone settings 548 | #[cfg(feature = "std")] 549 | pub const DEFAULT: TimeZoneSettings<'static> = TimeZoneSettings { directories: Self::DEFAULT_DIRECTORIES, read_file_fn: Self::DEFAULT_READ_FILE_FN }; 550 | 551 | /// Construct time zone settings 552 | pub const fn new(directories: &'a [&'a str], read_file_fn: ReadFileFn) -> TimeZoneSettings<'a> { 553 | Self { directories, read_file_fn } 554 | } 555 | 556 | /// Returns local time zone using current settings. 557 | /// 558 | /// This method in not supported on non-UNIX platforms, and returns the UTC time zone instead. 559 | /// 560 | pub fn parse_local(&self) -> Result { 561 | #[cfg(not(unix))] 562 | let local_time_zone = TimeZone::utc(); 563 | 564 | #[cfg(unix)] 565 | let local_time_zone = self.parse_posix_tz("localtime")?; 566 | 567 | Ok(local_time_zone) 568 | } 569 | 570 | /// Construct a time zone from a POSIX TZ string using current settings, 571 | /// as described in [the POSIX documentation of the `TZ` environment variable](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html). 572 | pub fn parse_posix_tz(&self, tz_string: &str) -> Result { 573 | if tz_string.is_empty() { 574 | return Err(TzStringError::Empty.into()); 575 | } 576 | 577 | if tz_string == "localtime" { 578 | return Ok(parse_tz_file(&(self.read_file_fn)("/etc/localtime").map_err(crate::Error::Io)?)?); 579 | } 580 | 581 | let mut chars = tz_string.chars(); 582 | if chars.next() == Some(':') { 583 | return Ok(parse_tz_file(&self.read_tz_file(chars.as_str())?)?); 584 | } 585 | 586 | match self.read_tz_file(tz_string) { 587 | Ok(bytes) => Ok(parse_tz_file(&bytes)?), 588 | Err(_) => { 589 | let tz_string = tz_string.trim_matches(|c: char| c.is_ascii_whitespace()); 590 | 591 | // TZ string extensions are not allowed 592 | let rule = parse_posix_tz(tz_string.as_bytes(), false)?; 593 | 594 | let local_time_types = match rule { 595 | TransitionRule::Fixed(local_time_type) => vec![local_time_type], 596 | TransitionRule::Alternate(alternate_time) => vec![*alternate_time.std(), *alternate_time.dst()], 597 | }; 598 | 599 | Ok(TimeZone::new(vec![], local_time_types, vec![], Some(rule))?) 600 | } 601 | } 602 | } 603 | 604 | /// Read the TZif file corresponding to a TZ string using current settings 605 | fn read_tz_file(&self, tz_string: &str) -> Result, crate::Error> { 606 | let read_file_fn = |path: &str| (self.read_file_fn)(path).map_err(crate::Error::Io); 607 | 608 | // Don't check system timezone directories on non-UNIX platforms 609 | #[cfg(not(unix))] 610 | return Ok(read_file_fn(tz_string)?); 611 | 612 | #[cfg(unix)] 613 | if tz_string.starts_with('/') { 614 | Ok(read_file_fn(tz_string)?) 615 | } else { 616 | self.directories 617 | .iter() 618 | .find_map(|folder| read_file_fn(&format!("{folder}/{tz_string}")).ok()) 619 | .ok_or_else(|| crate::Error::Io("file was not found".into())) 620 | } 621 | } 622 | } 623 | 624 | #[cfg(test)] 625 | mod tests { 626 | use super::*; 627 | 628 | #[test] 629 | fn test_tz_ascii_str() -> Result<(), TzError> { 630 | assert!(matches!(TzAsciiStr::new(b""), Err(LocalTimeTypeError::InvalidTimeZoneDesignationLength))); 631 | assert!(matches!(TzAsciiStr::new(b"1"), Err(LocalTimeTypeError::InvalidTimeZoneDesignationLength))); 632 | assert!(matches!(TzAsciiStr::new(b"12"), Err(LocalTimeTypeError::InvalidTimeZoneDesignationLength))); 633 | assert_eq!(TzAsciiStr::new(b"123")?.as_bytes(), b"123"); 634 | assert_eq!(TzAsciiStr::new(b"1234")?.as_bytes(), b"1234"); 635 | assert_eq!(TzAsciiStr::new(b"12345")?.as_bytes(), b"12345"); 636 | assert_eq!(TzAsciiStr::new(b"123456")?.as_bytes(), b"123456"); 637 | assert_eq!(TzAsciiStr::new(b"1234567")?.as_bytes(), b"1234567"); 638 | assert!(matches!(TzAsciiStr::new(b"12345678"), Err(LocalTimeTypeError::InvalidTimeZoneDesignationLength))); 639 | assert!(matches!(TzAsciiStr::new(b"123456789"), Err(LocalTimeTypeError::InvalidTimeZoneDesignationLength))); 640 | assert!(matches!(TzAsciiStr::new(b"1234567890"), Err(LocalTimeTypeError::InvalidTimeZoneDesignationLength))); 641 | 642 | assert!(matches!(TzAsciiStr::new(b"123\0\0\0"), Err(LocalTimeTypeError::InvalidTimeZoneDesignationChar))); 643 | 644 | Ok(()) 645 | } 646 | 647 | #[cfg(feature = "alloc")] 648 | #[test] 649 | fn test_time_zone() -> Result<(), TzError> { 650 | let utc = LocalTimeType::utc(); 651 | let cet = LocalTimeType::with_ut_offset(3600)?; 652 | 653 | let utc_local_time_types = vec![utc]; 654 | let fixed_extra_rule = TransitionRule::Fixed(cet); 655 | 656 | let time_zone_1 = TimeZone::new(vec![], utc_local_time_types.clone(), vec![], None)?; 657 | let time_zone_2 = TimeZone::new(vec![], utc_local_time_types.clone(), vec![], Some(fixed_extra_rule))?; 658 | let time_zone_3 = TimeZone::new(vec![Transition::new(0, 0)], utc_local_time_types.clone(), vec![], None)?; 659 | let time_zone_4 = TimeZone::new(vec![Transition::new(i32::MIN.into(), 0), Transition::new(0, 1)], vec![utc, cet], vec![], Some(fixed_extra_rule))?; 660 | 661 | assert_eq!(*time_zone_1.find_local_time_type(0)?, utc); 662 | assert_eq!(*time_zone_2.find_local_time_type(0)?, cet); 663 | 664 | assert_eq!(*time_zone_3.find_local_time_type(-1)?, utc); 665 | assert!(matches!(time_zone_3.find_local_time_type(0), Err(TzError::NoAvailableLocalTimeType))); 666 | 667 | assert_eq!(*time_zone_4.find_local_time_type(-1)?, utc); 668 | assert_eq!(*time_zone_4.find_local_time_type(0)?, cet); 669 | 670 | let time_zone_err = TimeZone::new(vec![Transition::new(0, 0)], utc_local_time_types, vec![], Some(fixed_extra_rule)); 671 | assert!(time_zone_err.is_err()); 672 | 673 | Ok(()) 674 | } 675 | 676 | #[cfg(feature = "std")] 677 | #[test] 678 | fn test_time_zone_from_posix_tz() -> Result<(), crate::Error> { 679 | #[cfg(unix)] 680 | { 681 | let time_zone_local = TimeZone::local()?; 682 | let time_zone_local_1 = TimeZone::from_posix_tz("localtime")?; 683 | let time_zone_local_2 = TimeZone::from_posix_tz("/etc/localtime")?; 684 | let time_zone_local_3 = TimeZone::from_posix_tz(":/etc/localtime")?; 685 | 686 | assert_eq!(time_zone_local, time_zone_local_1); 687 | assert_eq!(time_zone_local, time_zone_local_2); 688 | assert_eq!(time_zone_local, time_zone_local_3); 689 | 690 | assert!(matches!(time_zone_local.find_current_local_time_type(), Ok(_) | Err(TzError::NoAvailableLocalTimeType))); 691 | 692 | let time_zone_utc = TimeZone::from_posix_tz("UTC")?; 693 | assert_eq!(time_zone_utc.find_local_time_type(0)?.ut_offset(), 0); 694 | } 695 | 696 | assert!(TimeZone::from_posix_tz("EST5EDT,0/0,J365/25").is_err()); 697 | assert!(TimeZone::from_posix_tz("").is_err()); 698 | 699 | Ok(()) 700 | } 701 | 702 | #[cfg(feature = "alloc")] 703 | #[test] 704 | fn test_leap_seconds() -> Result<(), TzError> { 705 | let time_zone = TimeZone::new( 706 | vec![], 707 | vec![LocalTimeType::new(0, false, Some(b"UTC"))?], 708 | vec![ 709 | LeapSecond::new(78796800, 1), 710 | LeapSecond::new(94694401, 2), 711 | LeapSecond::new(126230402, 3), 712 | LeapSecond::new(157766403, 4), 713 | LeapSecond::new(189302404, 5), 714 | LeapSecond::new(220924805, 6), 715 | LeapSecond::new(252460806, 7), 716 | LeapSecond::new(283996807, 8), 717 | LeapSecond::new(315532808, 9), 718 | LeapSecond::new(362793609, 10), 719 | LeapSecond::new(394329610, 11), 720 | LeapSecond::new(425865611, 12), 721 | LeapSecond::new(489024012, 13), 722 | LeapSecond::new(567993613, 14), 723 | LeapSecond::new(631152014, 15), 724 | LeapSecond::new(662688015, 16), 725 | LeapSecond::new(709948816, 17), 726 | LeapSecond::new(741484817, 18), 727 | LeapSecond::new(773020818, 19), 728 | LeapSecond::new(820454419, 20), 729 | LeapSecond::new(867715220, 21), 730 | LeapSecond::new(915148821, 22), 731 | LeapSecond::new(1136073622, 23), 732 | LeapSecond::new(1230768023, 24), 733 | LeapSecond::new(1341100824, 25), 734 | LeapSecond::new(1435708825, 26), 735 | LeapSecond::new(1483228826, 27), 736 | ], 737 | None, 738 | )?; 739 | 740 | let time_zone_ref = time_zone.as_ref(); 741 | 742 | assert!(matches!(time_zone_ref.unix_leap_time_to_unix_time(1136073621), Ok(1136073599))); 743 | assert!(matches!(time_zone_ref.unix_leap_time_to_unix_time(1136073622), Ok(1136073600))); 744 | assert!(matches!(time_zone_ref.unix_leap_time_to_unix_time(1136073623), Ok(1136073600))); 745 | assert!(matches!(time_zone_ref.unix_leap_time_to_unix_time(1136073624), Ok(1136073601))); 746 | 747 | assert!(matches!(time_zone_ref.unix_time_to_unix_leap_time(1136073599), Ok(1136073621))); 748 | assert!(matches!(time_zone_ref.unix_time_to_unix_leap_time(1136073600), Ok(1136073623))); 749 | assert!(matches!(time_zone_ref.unix_time_to_unix_leap_time(1136073601), Ok(1136073624))); 750 | 751 | Ok(()) 752 | } 753 | 754 | #[cfg(feature = "alloc")] 755 | #[test] 756 | fn test_leap_seconds_overflow() -> Result<(), TzError> { 757 | let time_zone_err = TimeZone::new( 758 | vec![Transition::new(i64::MIN, 0)], 759 | vec![LocalTimeType::utc()], 760 | vec![LeapSecond::new(0, 1)], 761 | Some(TransitionRule::Fixed(LocalTimeType::utc())), 762 | ); 763 | assert!(time_zone_err.is_err()); 764 | 765 | let time_zone = TimeZone::new(vec![Transition::new(i64::MAX, 0)], vec![LocalTimeType::utc()], vec![LeapSecond::new(0, 1)], None)?; 766 | assert!(matches!(time_zone.find_local_time_type(i64::MAX), Err(TzError::OutOfRange))); 767 | 768 | Ok(()) 769 | } 770 | } 771 | -------------------------------------------------------------------------------- /src/timezone/rule.rs: -------------------------------------------------------------------------------- 1 | //! Types related to a time zone extra transition rule. 2 | 3 | use crate::constants::*; 4 | use crate::datetime::{days_since_unix_epoch, is_leap_year, UtcDateTime}; 5 | use crate::error::timezone::TransitionRuleError; 6 | use crate::error::TzError; 7 | use crate::timezone::LocalTimeType; 8 | use crate::utils::{binary_search_i64, cmp}; 9 | 10 | use core::cmp::Ordering; 11 | 12 | /// Informations needed for checking DST transition rules consistency, for a Julian day 13 | #[derive(Debug, PartialEq, Eq)] 14 | struct JulianDayCheckInfos { 15 | /// Offset in seconds from the start of a normal year 16 | start_normal_year_offset: i64, 17 | /// Offset in seconds from the end of a normal year 18 | end_normal_year_offset: i64, 19 | /// Offset in seconds from the start of a leap year 20 | start_leap_year_offset: i64, 21 | /// Offset in seconds from the end of a leap year 22 | end_leap_year_offset: i64, 23 | } 24 | 25 | /// Informations needed for checking DST transition rules consistency, for a day represented by a month, a month week and a week day 26 | #[derive(Debug, PartialEq, Eq)] 27 | struct MonthWeekDayCheckInfos { 28 | /// Possible offset range in seconds from the start of a normal year 29 | start_normal_year_offset_range: (i64, i64), 30 | /// Possible offset range in seconds from the end of a normal year 31 | end_normal_year_offset_range: (i64, i64), 32 | /// Possible offset range in seconds from the start of a leap year 33 | start_leap_year_offset_range: (i64, i64), 34 | /// Possible offset range in seconds from the end of a leap year 35 | end_leap_year_offset_range: (i64, i64), 36 | } 37 | 38 | /// Julian day in `[1, 365]`, without taking occasional February 29th into account, which is not referenceable 39 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 40 | pub struct Julian1WithoutLeap(u16); 41 | 42 | impl Julian1WithoutLeap { 43 | /// Construct a transition rule day represented by a Julian day in `[1, 365]`, without taking occasional February 29th into account, which is not referenceable 44 | #[inline] 45 | pub const fn new(julian_day_1: u16) -> Result { 46 | if !(1 <= julian_day_1 && julian_day_1 <= 365) { 47 | return Err(TransitionRuleError::InvalidRuleDayJulianDay); 48 | } 49 | 50 | Ok(Self(julian_day_1)) 51 | } 52 | 53 | /// Returns inner value 54 | #[inline] 55 | pub const fn get(&self) -> u16 { 56 | self.0 57 | } 58 | 59 | /// Compute transition date 60 | /// 61 | /// ## Outputs 62 | /// 63 | /// * `month`: Month in `[1, 12]` 64 | /// * `month_day`: Day of the month in `[1, 31]` 65 | /// 66 | const fn transition_date(&self) -> (usize, i64) { 67 | let year_day = self.0 as i64; 68 | 69 | let month = match binary_search_i64(&CUMUL_DAYS_IN_MONTHS_NORMAL_YEAR, year_day - 1) { 70 | Ok(x) => x + 1, 71 | Err(x) => x, 72 | }; 73 | 74 | let month_day = year_day - CUMUL_DAYS_IN_MONTHS_NORMAL_YEAR[month - 1]; 75 | 76 | (month, month_day) 77 | } 78 | 79 | /// Compute the informations needed for checking DST transition rules consistency 80 | const fn compute_check_infos(&self, utc_day_time: i64) -> JulianDayCheckInfos { 81 | let start_normal_year_offset = (self.0 as i64 - 1) * SECONDS_PER_DAY + utc_day_time; 82 | let start_leap_year_offset = if self.0 <= 59 { start_normal_year_offset } else { start_normal_year_offset + SECONDS_PER_DAY }; 83 | 84 | JulianDayCheckInfos { 85 | start_normal_year_offset, 86 | end_normal_year_offset: start_normal_year_offset - SECONDS_PER_NORMAL_YEAR, 87 | start_leap_year_offset, 88 | end_leap_year_offset: start_leap_year_offset - SECONDS_PER_LEAP_YEAR, 89 | } 90 | } 91 | } 92 | 93 | /// Zero-based Julian day in `[0, 365]`, taking occasional February 29th into account and allowing December 32nd 94 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 95 | pub struct Julian0WithLeap(u16); 96 | 97 | impl Julian0WithLeap { 98 | /// Construct a transition rule day represented by a zero-based Julian day in `[0, 365]`, taking occasional February 29th into account and allowing December 32nd 99 | #[inline] 100 | pub const fn new(julian_day_0: u16) -> Result { 101 | if julian_day_0 > 365 { 102 | return Err(TransitionRuleError::InvalidRuleDayJulianDay); 103 | } 104 | 105 | Ok(Self(julian_day_0)) 106 | } 107 | 108 | /// Returns inner value 109 | #[inline] 110 | pub const fn get(&self) -> u16 { 111 | self.0 112 | } 113 | 114 | /// Compute transition date. 115 | /// 116 | /// On a non-leap year, a value of `365` corresponds to December 32nd (which is January 1st of the next year). 117 | /// 118 | /// ## Outputs 119 | /// 120 | /// * `month`: Month in `[1, 12]` 121 | /// * `month_day`: Day of the month in `[1, 32]` 122 | /// 123 | const fn transition_date(&self, leap_year: bool) -> (usize, i64) { 124 | let cumul_day_in_months = if leap_year { &CUMUL_DAYS_IN_MONTHS_LEAP_YEAR } else { &CUMUL_DAYS_IN_MONTHS_NORMAL_YEAR }; 125 | 126 | let year_day = self.0 as i64; 127 | 128 | let month = match binary_search_i64(cumul_day_in_months, year_day) { 129 | Ok(x) => x + 1, 130 | Err(x) => x, 131 | }; 132 | 133 | let month_day = 1 + year_day - cumul_day_in_months[month - 1]; 134 | 135 | (month, month_day) 136 | } 137 | 138 | /// Compute the informations needed for checking DST transition rules consistency 139 | const fn compute_check_infos(&self, utc_day_time: i64) -> JulianDayCheckInfos { 140 | let start_year_offset = self.0 as i64 * SECONDS_PER_DAY + utc_day_time; 141 | 142 | JulianDayCheckInfos { 143 | start_normal_year_offset: start_year_offset, 144 | end_normal_year_offset: start_year_offset - SECONDS_PER_NORMAL_YEAR, 145 | start_leap_year_offset: start_year_offset, 146 | end_leap_year_offset: start_year_offset - SECONDS_PER_LEAP_YEAR, 147 | } 148 | } 149 | } 150 | 151 | /// Day represented by a month, a month week and a week day 152 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 153 | pub struct MonthWeekDay { 154 | /// Month in `[1, 12]` 155 | month: u8, 156 | /// Week of the month in `[1, 5]`, with `5` representing the last week of the month 157 | week: u8, 158 | /// Day of the week in `[0, 6]` from Sunday 159 | week_day: u8, 160 | } 161 | 162 | impl MonthWeekDay { 163 | /// Construct a transition rule day represented by a month, a month week and a week day 164 | #[inline] 165 | pub const fn new(month: u8, week: u8, week_day: u8) -> Result { 166 | if !(1 <= month && month <= 12) { 167 | return Err(TransitionRuleError::InvalidRuleDayMonth); 168 | } 169 | 170 | if !(1 <= week && week <= 5) { 171 | return Err(TransitionRuleError::InvalidRuleDayWeek); 172 | } 173 | 174 | if week_day > 6 { 175 | return Err(TransitionRuleError::InvalidRuleDayWeekDay); 176 | } 177 | 178 | Ok(Self { month, week, week_day }) 179 | } 180 | 181 | /// Returns month in `[1, 12]` 182 | #[inline] 183 | pub const fn month(&self) -> u8 { 184 | self.month 185 | } 186 | 187 | /// Returns week of the month in `[1, 5]`, with `5` representing the last week of the month 188 | #[inline] 189 | pub const fn week(&self) -> u8 { 190 | self.week 191 | } 192 | 193 | /// Returns day of the week in `[0, 6]` from Sunday 194 | #[inline] 195 | pub const fn week_day(&self) -> u8 { 196 | self.week_day 197 | } 198 | 199 | /// Compute transition date on a specific year 200 | /// 201 | /// ## Outputs 202 | /// 203 | /// * `month`: Month in `[1, 12]` 204 | /// * `month_day`: Day of the month in `[1, 31]` 205 | /// 206 | const fn transition_date(&self, year: i32) -> (usize, i64) { 207 | let month = self.month as usize; 208 | let week = self.week as i64; 209 | let week_day = self.week_day as i64; 210 | 211 | let mut days_in_month = DAYS_IN_MONTHS_NORMAL_YEAR[month - 1]; 212 | if month == 2 { 213 | days_in_month += is_leap_year(year) as i64; 214 | } 215 | 216 | let week_day_of_first_month_day = (4 + days_since_unix_epoch(year, month, 1)).rem_euclid(DAYS_PER_WEEK); 217 | let first_week_day_occurence_in_month = 1 + (week_day - week_day_of_first_month_day).rem_euclid(DAYS_PER_WEEK); 218 | 219 | let mut month_day = first_week_day_occurence_in_month + (week - 1) * DAYS_PER_WEEK; 220 | if month_day > days_in_month { 221 | month_day -= DAYS_PER_WEEK 222 | } 223 | 224 | (month, month_day) 225 | } 226 | 227 | /// Compute the informations needed for checking DST transition rules consistency 228 | const fn compute_check_infos(&self, utc_day_time: i64) -> MonthWeekDayCheckInfos { 229 | let month = self.month as usize; 230 | let week = self.week as i64; 231 | 232 | let (normal_year_month_day_range, leap_year_month_day_range) = { 233 | if week == 5 { 234 | let normal_year_days_in_month = DAYS_IN_MONTHS_NORMAL_YEAR[month - 1]; 235 | let leap_year_days_in_month = if month == 2 { normal_year_days_in_month + 1 } else { normal_year_days_in_month }; 236 | 237 | let normal_year_month_day_range = (normal_year_days_in_month - 6, normal_year_days_in_month); 238 | let leap_year_month_day_range = (leap_year_days_in_month - 6, leap_year_days_in_month); 239 | 240 | (normal_year_month_day_range, leap_year_month_day_range) 241 | } else { 242 | let month_day_range = (week * DAYS_PER_WEEK - 6, week * DAYS_PER_WEEK); 243 | (month_day_range, month_day_range) 244 | } 245 | }; 246 | 247 | let start_normal_year_offset_range = ( 248 | (CUMUL_DAYS_IN_MONTHS_NORMAL_YEAR[month - 1] + normal_year_month_day_range.0 - 1) * SECONDS_PER_DAY + utc_day_time, 249 | (CUMUL_DAYS_IN_MONTHS_NORMAL_YEAR[month - 1] + normal_year_month_day_range.1 - 1) * SECONDS_PER_DAY + utc_day_time, 250 | ); 251 | 252 | let start_leap_year_offset_range = ( 253 | (CUMUL_DAYS_IN_MONTHS_LEAP_YEAR[month - 1] + leap_year_month_day_range.0 - 1) * SECONDS_PER_DAY + utc_day_time, 254 | (CUMUL_DAYS_IN_MONTHS_LEAP_YEAR[month - 1] + leap_year_month_day_range.1 - 1) * SECONDS_PER_DAY + utc_day_time, 255 | ); 256 | 257 | MonthWeekDayCheckInfos { 258 | start_normal_year_offset_range, 259 | end_normal_year_offset_range: ( 260 | start_normal_year_offset_range.0 - SECONDS_PER_NORMAL_YEAR, 261 | start_normal_year_offset_range.1 - SECONDS_PER_NORMAL_YEAR, 262 | ), 263 | start_leap_year_offset_range, 264 | end_leap_year_offset_range: (start_leap_year_offset_range.0 - SECONDS_PER_LEAP_YEAR, start_leap_year_offset_range.1 - SECONDS_PER_LEAP_YEAR), 265 | } 266 | } 267 | } 268 | 269 | /// Transition rule day 270 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 271 | pub enum RuleDay { 272 | /// Julian day in `[1, 365]`, without taking occasional February 29th into account, which is not referenceable 273 | Julian1WithoutLeap(Julian1WithoutLeap), 274 | /// Zero-based Julian day in `[0, 365]`, taking occasional February 29th into account and allowing December 32nd 275 | Julian0WithLeap(Julian0WithLeap), 276 | /// Day represented by a month, a month week and a week day 277 | MonthWeekDay(MonthWeekDay), 278 | } 279 | 280 | impl RuleDay { 281 | /// Compute transition date for the provided year. 282 | /// 283 | /// The December 32nd date is possible, which corresponds to January 1st of the next year. 284 | /// 285 | /// ## Outputs 286 | /// 287 | /// * `month`: Month in `[1, 12]` 288 | /// * `month_day`: Day of the month in `[1, 32]` 289 | /// 290 | const fn transition_date(&self, year: i32) -> (usize, i64) { 291 | match self { 292 | Self::Julian1WithoutLeap(rule_day) => rule_day.transition_date(), 293 | Self::Julian0WithLeap(rule_day) => rule_day.transition_date(is_leap_year(year)), 294 | Self::MonthWeekDay(rule_day) => rule_day.transition_date(year), 295 | } 296 | } 297 | 298 | /// Returns the UTC Unix time in seconds associated to the transition date for the provided year 299 | pub(crate) const fn unix_time(&self, year: i32, day_time_in_utc: i64) -> i64 { 300 | let (month, month_day) = self.transition_date(year); 301 | days_since_unix_epoch(year, month, month_day) * SECONDS_PER_DAY + day_time_in_utc 302 | } 303 | } 304 | 305 | /// Transition rule representing alternate local time types 306 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 307 | pub struct AlternateTime { 308 | /// Local time type for standard time 309 | std: LocalTimeType, 310 | /// Local time type for Daylight Saving Time 311 | dst: LocalTimeType, 312 | /// Start day of Daylight Saving Time 313 | dst_start: RuleDay, 314 | /// Local start day time of Daylight Saving Time, in seconds 315 | dst_start_time: i32, 316 | /// End day of Daylight Saving Time 317 | dst_end: RuleDay, 318 | /// Local end day time of Daylight Saving Time, in seconds 319 | dst_end_time: i32, 320 | } 321 | 322 | impl AlternateTime { 323 | /// Construct a transition rule representing alternate local time types 324 | pub const fn new( 325 | std: LocalTimeType, 326 | dst: LocalTimeType, 327 | dst_start: RuleDay, 328 | dst_start_time: i32, 329 | dst_end: RuleDay, 330 | dst_end_time: i32, 331 | ) -> Result { 332 | let std_ut_offset = std.ut_offset as i64; 333 | let dst_ut_offset = dst.ut_offset as i64; 334 | 335 | // Limit UTC offset to POSIX-required range 336 | if !(-25 * SECONDS_PER_HOUR < std_ut_offset && std_ut_offset < 26 * SECONDS_PER_HOUR) { 337 | return Err(TransitionRuleError::InvalidStdUtcOffset); 338 | } 339 | 340 | if !(-25 * SECONDS_PER_HOUR < dst_ut_offset && dst_ut_offset < 26 * SECONDS_PER_HOUR) { 341 | return Err(TransitionRuleError::InvalidDstUtcOffset); 342 | } 343 | 344 | // Overflow is not possible 345 | if !((dst_start_time as i64).abs() < SECONDS_PER_WEEK && (dst_end_time as i64).abs() < SECONDS_PER_WEEK) { 346 | return Err(TransitionRuleError::InvalidDstStartEndTime); 347 | } 348 | 349 | // Check DST transition rules consistency 350 | if !check_dst_transition_rules_consistency(&std, &dst, dst_start, dst_start_time, dst_end, dst_end_time) { 351 | return Err(TransitionRuleError::InconsistentRule); 352 | } 353 | 354 | Ok(Self { std, dst, dst_start, dst_start_time, dst_end, dst_end_time }) 355 | } 356 | 357 | /// Returns local time type for standard time 358 | #[inline] 359 | pub const fn std(&self) -> &LocalTimeType { 360 | &self.std 361 | } 362 | 363 | /// Returns local time type for Daylight Saving Time 364 | #[inline] 365 | pub const fn dst(&self) -> &LocalTimeType { 366 | &self.dst 367 | } 368 | 369 | /// Returns start day of Daylight Saving Time 370 | #[inline] 371 | pub const fn dst_start(&self) -> &RuleDay { 372 | &self.dst_start 373 | } 374 | 375 | /// Returns local start day time of Daylight Saving Time, in seconds 376 | #[inline] 377 | pub const fn dst_start_time(&self) -> i32 { 378 | self.dst_start_time 379 | } 380 | 381 | /// Returns end day of Daylight Saving Time 382 | #[inline] 383 | pub const fn dst_end(&self) -> &RuleDay { 384 | &self.dst_end 385 | } 386 | 387 | /// Returns local end day time of Daylight Saving Time, in seconds 388 | #[inline] 389 | pub const fn dst_end_time(&self) -> i32 { 390 | self.dst_end_time 391 | } 392 | 393 | /// Find the local time type associated to the alternate transition rule at the specified Unix time in seconds 394 | const fn find_local_time_type(&self, unix_time: i64) -> Result<&LocalTimeType, TzError> { 395 | // Overflow is not possible 396 | let dst_start_time_in_utc = self.dst_start_time as i64 - self.std.ut_offset as i64; 397 | let dst_end_time_in_utc = self.dst_end_time as i64 - self.dst.ut_offset as i64; 398 | 399 | let current_year = match UtcDateTime::from_timespec(unix_time, 0) { 400 | Ok(utc_date_time) => utc_date_time.year(), 401 | Err(error) => return Err(error), 402 | }; 403 | 404 | // Check if the current year is valid for the following computations 405 | if !(i32::MIN + 2 <= current_year && current_year <= i32::MAX - 2) { 406 | return Err(TzError::OutOfRange); 407 | } 408 | 409 | let current_year_dst_start_unix_time = self.dst_start.unix_time(current_year, dst_start_time_in_utc); 410 | let current_year_dst_end_unix_time = self.dst_end.unix_time(current_year, dst_end_time_in_utc); 411 | 412 | // Check DST start/end Unix times for previous/current/next years to support for transition day times outside of [0h, 24h] range. 413 | // This is sufficient since the absolute value of DST start/end time in UTC is less than 2 weeks. 414 | // Moreover, inconsistent DST transition rules are not allowed, so there won't be additional transitions at the year boundary. 415 | let is_dst = match cmp(current_year_dst_start_unix_time, current_year_dst_end_unix_time) { 416 | Ordering::Less | Ordering::Equal => { 417 | if unix_time < current_year_dst_start_unix_time { 418 | let previous_year_dst_end_unix_time = self.dst_end.unix_time(current_year - 1, dst_end_time_in_utc); 419 | if unix_time < previous_year_dst_end_unix_time { 420 | let previous_year_dst_start_unix_time = self.dst_start.unix_time(current_year - 1, dst_start_time_in_utc); 421 | previous_year_dst_start_unix_time <= unix_time 422 | } else { 423 | false 424 | } 425 | } else if unix_time < current_year_dst_end_unix_time { 426 | true 427 | } else { 428 | let next_year_dst_start_unix_time = self.dst_start.unix_time(current_year + 1, dst_start_time_in_utc); 429 | if next_year_dst_start_unix_time <= unix_time { 430 | let next_year_dst_end_unix_time = self.dst_end.unix_time(current_year + 1, dst_end_time_in_utc); 431 | unix_time < next_year_dst_end_unix_time 432 | } else { 433 | false 434 | } 435 | } 436 | } 437 | Ordering::Greater => { 438 | if unix_time < current_year_dst_end_unix_time { 439 | let previous_year_dst_start_unix_time = self.dst_start.unix_time(current_year - 1, dst_start_time_in_utc); 440 | if unix_time < previous_year_dst_start_unix_time { 441 | let previous_year_dst_end_unix_time = self.dst_end.unix_time(current_year - 1, dst_end_time_in_utc); 442 | unix_time < previous_year_dst_end_unix_time 443 | } else { 444 | true 445 | } 446 | } else if unix_time < current_year_dst_start_unix_time { 447 | false 448 | } else { 449 | let next_year_dst_end_unix_time = self.dst_end.unix_time(current_year + 1, dst_end_time_in_utc); 450 | if next_year_dst_end_unix_time <= unix_time { 451 | let next_year_dst_start_unix_time = self.dst_start.unix_time(current_year + 1, dst_start_time_in_utc); 452 | next_year_dst_start_unix_time <= unix_time 453 | } else { 454 | true 455 | } 456 | } 457 | } 458 | }; 459 | 460 | if is_dst { 461 | Ok(&self.dst) 462 | } else { 463 | Ok(&self.std) 464 | } 465 | } 466 | } 467 | 468 | /// Transition rule 469 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 470 | pub enum TransitionRule { 471 | /// Fixed local time type 472 | Fixed(LocalTimeType), 473 | /// Alternate local time types 474 | Alternate(AlternateTime), 475 | } 476 | 477 | impl TransitionRule { 478 | /// Find the local time type associated to the transition rule at the specified Unix time in seconds 479 | pub(super) const fn find_local_time_type(&self, unix_time: i64) -> Result<&LocalTimeType, TzError> { 480 | match self { 481 | Self::Fixed(local_time_type) => Ok(local_time_type), 482 | Self::Alternate(alternate_time) => alternate_time.find_local_time_type(unix_time), 483 | } 484 | } 485 | } 486 | 487 | /// Check DST transition rules consistency, which ensures that the DST start and end time are always in the same order. 488 | /// 489 | /// This prevents from having an additional transition at the year boundary, when the order of DST start and end time is different on consecutive years. 490 | /// 491 | const fn check_dst_transition_rules_consistency( 492 | std: &LocalTimeType, 493 | dst: &LocalTimeType, 494 | dst_start: RuleDay, 495 | dst_start_time: i32, 496 | dst_end: RuleDay, 497 | dst_end_time: i32, 498 | ) -> bool { 499 | // Overflow is not possible 500 | let dst_start_time_in_utc = dst_start_time as i64 - std.ut_offset as i64; 501 | let dst_end_time_in_utc = dst_end_time as i64 - dst.ut_offset as i64; 502 | 503 | match (dst_start, dst_end) { 504 | (RuleDay::Julian1WithoutLeap(start_day), RuleDay::Julian1WithoutLeap(end_day)) => { 505 | check_two_julian_days(start_day.compute_check_infos(dst_start_time_in_utc), end_day.compute_check_infos(dst_end_time_in_utc)) 506 | } 507 | (RuleDay::Julian1WithoutLeap(start_day), RuleDay::Julian0WithLeap(end_day)) => { 508 | check_two_julian_days(start_day.compute_check_infos(dst_start_time_in_utc), end_day.compute_check_infos(dst_end_time_in_utc)) 509 | } 510 | (RuleDay::Julian0WithLeap(start_day), RuleDay::Julian1WithoutLeap(end_day)) => { 511 | check_two_julian_days(start_day.compute_check_infos(dst_start_time_in_utc), end_day.compute_check_infos(dst_end_time_in_utc)) 512 | } 513 | (RuleDay::Julian0WithLeap(start_day), RuleDay::Julian0WithLeap(end_day)) => { 514 | check_two_julian_days(start_day.compute_check_infos(dst_start_time_in_utc), end_day.compute_check_infos(dst_end_time_in_utc)) 515 | } 516 | (RuleDay::Julian1WithoutLeap(start_day), RuleDay::MonthWeekDay(end_day)) => { 517 | check_month_week_day_and_julian_day(end_day.compute_check_infos(dst_end_time_in_utc), start_day.compute_check_infos(dst_start_time_in_utc)) 518 | } 519 | (RuleDay::Julian0WithLeap(start_day), RuleDay::MonthWeekDay(end_day)) => { 520 | check_month_week_day_and_julian_day(end_day.compute_check_infos(dst_end_time_in_utc), start_day.compute_check_infos(dst_start_time_in_utc)) 521 | } 522 | (RuleDay::MonthWeekDay(start_day), RuleDay::Julian1WithoutLeap(end_day)) => { 523 | check_month_week_day_and_julian_day(start_day.compute_check_infos(dst_start_time_in_utc), end_day.compute_check_infos(dst_end_time_in_utc)) 524 | } 525 | (RuleDay::MonthWeekDay(start_day), RuleDay::Julian0WithLeap(end_day)) => { 526 | check_month_week_day_and_julian_day(start_day.compute_check_infos(dst_start_time_in_utc), end_day.compute_check_infos(dst_end_time_in_utc)) 527 | } 528 | (RuleDay::MonthWeekDay(start_day), RuleDay::MonthWeekDay(end_day)) => { 529 | check_two_month_week_days(start_day, dst_start_time_in_utc, end_day, dst_end_time_in_utc) 530 | } 531 | } 532 | } 533 | 534 | /// Check DST transition rules consistency for two Julian days 535 | const fn check_two_julian_days(check_infos_1: JulianDayCheckInfos, check_infos_2: JulianDayCheckInfos) -> bool { 536 | // Check in same year 537 | let (before, after) = if check_infos_1.start_normal_year_offset <= check_infos_2.start_normal_year_offset 538 | && check_infos_1.start_leap_year_offset <= check_infos_2.start_leap_year_offset 539 | { 540 | (&check_infos_1, &check_infos_2) 541 | } else if check_infos_2.start_normal_year_offset <= check_infos_1.start_normal_year_offset 542 | && check_infos_2.start_leap_year_offset <= check_infos_1.start_leap_year_offset 543 | { 544 | (&check_infos_2, &check_infos_1) 545 | } else { 546 | return false; 547 | }; 548 | 549 | // Check in consecutive years 550 | if after.end_normal_year_offset <= before.start_normal_year_offset 551 | && after.end_normal_year_offset <= before.start_leap_year_offset 552 | && after.end_leap_year_offset <= before.start_normal_year_offset 553 | { 554 | return true; 555 | } 556 | 557 | if before.start_normal_year_offset <= after.end_normal_year_offset 558 | && before.start_leap_year_offset <= after.end_normal_year_offset 559 | && before.start_normal_year_offset <= after.end_leap_year_offset 560 | { 561 | return true; 562 | } 563 | 564 | false 565 | } 566 | 567 | /// Check DST transition rules consistency for a Julian day and a day represented by a month, a month week and a week day 568 | const fn check_month_week_day_and_julian_day(check_infos_1: MonthWeekDayCheckInfos, check_infos_2: JulianDayCheckInfos) -> bool { 569 | // Check in same year, then in consecutive years 570 | if check_infos_2.start_normal_year_offset <= check_infos_1.start_normal_year_offset_range.0 571 | && check_infos_2.start_leap_year_offset <= check_infos_1.start_leap_year_offset_range.0 572 | { 573 | let (before, after) = (&check_infos_2, &check_infos_1); 574 | 575 | if after.end_normal_year_offset_range.1 <= before.start_normal_year_offset 576 | && after.end_normal_year_offset_range.1 <= before.start_leap_year_offset 577 | && after.end_leap_year_offset_range.1 <= before.start_normal_year_offset 578 | { 579 | return true; 580 | }; 581 | 582 | if before.start_normal_year_offset <= after.end_normal_year_offset_range.0 583 | && before.start_leap_year_offset <= after.end_normal_year_offset_range.0 584 | && before.start_normal_year_offset <= after.end_leap_year_offset_range.0 585 | { 586 | return true; 587 | }; 588 | 589 | return false; 590 | } 591 | 592 | if check_infos_1.start_normal_year_offset_range.1 <= check_infos_2.start_normal_year_offset 593 | && check_infos_1.start_leap_year_offset_range.1 <= check_infos_2.start_leap_year_offset 594 | { 595 | let (before, after) = (&check_infos_1, &check_infos_2); 596 | 597 | if after.end_normal_year_offset <= before.start_normal_year_offset_range.0 598 | && after.end_normal_year_offset <= before.start_leap_year_offset_range.0 599 | && after.end_leap_year_offset <= before.start_normal_year_offset_range.0 600 | { 601 | return true; 602 | } 603 | 604 | if before.start_normal_year_offset_range.1 <= after.end_normal_year_offset 605 | && before.start_leap_year_offset_range.1 <= after.end_normal_year_offset 606 | && before.start_normal_year_offset_range.1 <= after.end_leap_year_offset 607 | { 608 | return true; 609 | } 610 | 611 | return false; 612 | } 613 | 614 | false 615 | } 616 | 617 | /// Check DST transition rules consistency for two days represented by a month, a month week and a week day 618 | const fn check_two_month_week_days(month_week_day_1: MonthWeekDay, utc_day_time_1: i64, month_week_day_2: MonthWeekDay, utc_day_time_2: i64) -> bool { 619 | // Sort rule days 620 | let (month_week_day_before, utc_day_time_before, month_week_day_after, utc_day_time_after) = { 621 | let rem = (month_week_day_2.month as i64 - month_week_day_1.month as i64).rem_euclid(MONTHS_PER_YEAR); 622 | 623 | if rem == 0 { 624 | if month_week_day_1.week <= month_week_day_2.week { 625 | (month_week_day_1, utc_day_time_1, month_week_day_2, utc_day_time_2) 626 | } else { 627 | (month_week_day_2, utc_day_time_2, month_week_day_1, utc_day_time_1) 628 | } 629 | } else if rem == 1 { 630 | (month_week_day_1, utc_day_time_1, month_week_day_2, utc_day_time_2) 631 | } else if rem == MONTHS_PER_YEAR - 1 { 632 | (month_week_day_2, utc_day_time_2, month_week_day_1, utc_day_time_1) 633 | } else { 634 | // Months are not equal or consecutive, so rule days are separated by more than 3 weeks and cannot swap their order 635 | return true; 636 | } 637 | }; 638 | 639 | let month_before = month_week_day_before.month as usize; 640 | let week_before = month_week_day_before.week as i64; 641 | let week_day_before = month_week_day_before.week_day as i64; 642 | 643 | let month_after = month_week_day_after.month as usize; 644 | let week_after = month_week_day_after.week as i64; 645 | let week_day_after = month_week_day_after.week_day as i64; 646 | 647 | let (diff_days_min, diff_days_max) = if week_day_before == week_day_after { 648 | // Rule days are separated by a whole number of weeks 649 | let (diff_week_min, diff_week_max) = match (week_before, week_after) { 650 | // All months have more than 29 days on a leap year, so the 5th week is non-empty 651 | (1..=4, 5) if month_before == month_after => (4 - week_before, 5 - week_before), 652 | (1..=4, 1..=4) if month_before != month_after => (4 - week_before + week_after, 5 - week_before + week_after), 653 | _ => return true, // rule days are synchronized or separated by more than 3 weeks 654 | }; 655 | 656 | (diff_week_min * DAYS_PER_WEEK, diff_week_max * DAYS_PER_WEEK) 657 | } else { 658 | // week_day_before != week_day_after 659 | let n = (week_day_after - week_day_before).rem_euclid(DAYS_PER_WEEK); // n >= 1 660 | 661 | if month_before == month_after { 662 | match (week_before, week_after) { 663 | (5, 5) => (n - DAYS_PER_WEEK, n), 664 | (1..=4, 1..=4) => (n + DAYS_PER_WEEK * (week_after - week_before - 1), n + DAYS_PER_WEEK * (week_after - week_before)), 665 | (1..=4, 5) => { 666 | // For February month: 667 | // * On a normal year, we have n > (days_in_month % DAYS_PER_WEEK). 668 | // * On a leap year, we have n >= (days_in_month % DAYS_PER_WEEK). 669 | // 670 | // Since we want to check all possible years at the same time, checking only non-leap year is enough. 671 | let days_in_month = DAYS_IN_MONTHS_NORMAL_YEAR[month_before - 1]; 672 | 673 | match cmp(n, days_in_month % DAYS_PER_WEEK) { 674 | Ordering::Less => (n + DAYS_PER_WEEK * (4 - week_before), n + DAYS_PER_WEEK * (5 - week_before)), 675 | Ordering::Equal => return true, // rule days are synchronized 676 | Ordering::Greater => (n + DAYS_PER_WEEK * (3 - week_before), n + DAYS_PER_WEEK * (4 - week_before)), 677 | } 678 | } 679 | _ => unreachable!(), 680 | } 681 | } else { 682 | // month_before != month_after 683 | match (week_before, week_after) { 684 | (1..=4, 1..=4) => { 685 | // Same as above 686 | let days_in_month = DAYS_IN_MONTHS_NORMAL_YEAR[month_before - 1]; 687 | 688 | match cmp(n, days_in_month % DAYS_PER_WEEK) { 689 | Ordering::Less => (n + DAYS_PER_WEEK * (4 - week_before + week_after), n + DAYS_PER_WEEK * (5 - week_before + week_after)), 690 | Ordering::Equal => return true, // rule days are synchronized 691 | Ordering::Greater => (n + DAYS_PER_WEEK * (3 - week_before + week_after), n + DAYS_PER_WEEK * (4 - week_before + week_after)), 692 | } 693 | } 694 | (5, 1..=4) => (n + DAYS_PER_WEEK * (week_after - 1), n + DAYS_PER_WEEK * week_after), 695 | _ => return true, // rule days are separated by more than 3 weeks 696 | } 697 | } 698 | }; 699 | 700 | let diff_days_seconds_min = diff_days_min * SECONDS_PER_DAY; 701 | let diff_days_seconds_max = diff_days_max * SECONDS_PER_DAY; 702 | 703 | // Check possible order swap of rule days 704 | utc_day_time_before <= diff_days_seconds_min + utc_day_time_after || diff_days_seconds_max + utc_day_time_after <= utc_day_time_before 705 | } 706 | 707 | #[cfg(test)] 708 | mod tests { 709 | use super::*; 710 | use crate::TzError; 711 | 712 | #[test] 713 | fn test_compute_check_infos() -> Result<(), TzError> { 714 | let check_julian = |check_infos: JulianDayCheckInfos, start_normal, end_normal, start_leap, end_leap| { 715 | assert_eq!(check_infos.start_normal_year_offset, start_normal); 716 | assert_eq!(check_infos.end_normal_year_offset, end_normal); 717 | assert_eq!(check_infos.start_leap_year_offset, start_leap); 718 | assert_eq!(check_infos.end_leap_year_offset, end_leap); 719 | }; 720 | 721 | let check_mwd = |check_infos: MonthWeekDayCheckInfos, start_normal, end_normal, start_leap, end_leap| { 722 | assert_eq!(check_infos.start_normal_year_offset_range, start_normal); 723 | assert_eq!(check_infos.end_normal_year_offset_range, end_normal); 724 | assert_eq!(check_infos.start_leap_year_offset_range, start_leap); 725 | assert_eq!(check_infos.end_leap_year_offset_range, end_leap); 726 | }; 727 | 728 | check_julian(Julian1WithoutLeap::new(1)?.compute_check_infos(1), 1, -31535999, 1, -31622399); 729 | check_julian(Julian1WithoutLeap::new(365)?.compute_check_infos(1), 31449601, -86399, 31536001, -86399); 730 | 731 | check_julian(Julian0WithLeap::new(0)?.compute_check_infos(1), 1, -31535999, 1, -31622399); 732 | check_julian(Julian0WithLeap::new(365)?.compute_check_infos(1), 31536001, 1, 31536001, -86399); 733 | 734 | check_mwd(MonthWeekDay::new(1, 1, 0)?.compute_check_infos(1), (1, 518401), (-31535999, -31017599), (1, 518401), (-31622399, -31103999)); 735 | check_mwd(MonthWeekDay::new(1, 5, 0)?.compute_check_infos(1), (2073601, 2592001), (-29462399, -28943999), (2073601, 2592001), (-29548799, -29030399)); 736 | check_mwd(MonthWeekDay::new(2, 4, 0)?.compute_check_infos(1), (4492801, 5011201), (-27043199, -26524799), (4492801, 5011201), (-27129599, -26611199)); 737 | check_mwd(MonthWeekDay::new(2, 5, 0)?.compute_check_infos(1), (4492801, 5011201), (-27043199, -26524799), (4579201, 5097601), (-27043199, -26524799)); 738 | check_mwd(MonthWeekDay::new(3, 1, 0)?.compute_check_infos(1), (5097601, 5616001), (-26438399, -25919999), (5184001, 5702401), (-26438399, -25919999)); 739 | check_mwd(MonthWeekDay::new(3, 5, 0)?.compute_check_infos(1), (7171201, 7689601), (-24364799, -23846399), (7257601, 7776001), (-24364799, -23846399)); 740 | check_mwd(MonthWeekDay::new(12, 5, 0)?.compute_check_infos(1), (30931201, 31449601), (-604799, -86399), (31017601, 31536001), (-604799, -86399)); 741 | 742 | Ok(()) 743 | } 744 | 745 | #[test] 746 | fn test_check_dst_transition_rules_consistency() -> Result<(), TzError> { 747 | let utc = LocalTimeType::utc(); 748 | 749 | let julian_1 = |year_day| -> Result<_, TzError> { Ok(RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(year_day)?)) }; 750 | let julian_0 = |year_day| -> Result<_, TzError> { Ok(RuleDay::Julian0WithLeap(Julian0WithLeap::new(year_day)?)) }; 751 | let mwd = |month, week, week_day| -> Result<_, TzError> { Ok(RuleDay::MonthWeekDay(MonthWeekDay::new(month, week, week_day)?)) }; 752 | 753 | let check = |dst_start, dst_start_time, dst_end, dst_end_time| { 754 | let check_1 = check_dst_transition_rules_consistency(&utc, &utc, dst_start, dst_start_time, dst_end, dst_end_time); 755 | let check_2 = check_dst_transition_rules_consistency(&utc, &utc, dst_end, dst_end_time, dst_start, dst_start_time); 756 | assert_eq!(check_1, check_2); 757 | 758 | check_1 759 | }; 760 | 761 | let check_all = |dst_start, dst_start_times: &[i32], dst_end, dst_end_time, results: &[bool]| { 762 | assert_eq!(dst_start_times.len(), results.len()); 763 | 764 | for (&dst_start_time, &result) in dst_start_times.iter().zip(results) { 765 | assert_eq!(check(dst_start, dst_start_time, dst_end, dst_end_time), result); 766 | } 767 | }; 768 | 769 | const DAY_1: i32 = 86400; 770 | const DAY_2: i32 = 2 * DAY_1; 771 | const DAY_3: i32 = 3 * DAY_1; 772 | const DAY_4: i32 = 4 * DAY_1; 773 | const DAY_5: i32 = 5 * DAY_1; 774 | const DAY_6: i32 = 6 * DAY_1; 775 | 776 | check_all(julian_1(59)?, &[-1, 0, 1], julian_1(60)?, -DAY_1, &[true, true, false]); 777 | check_all(julian_1(365)?, &[-1, 0, 1], julian_1(1)?, -DAY_1, &[true, true, true]); 778 | 779 | check_all(julian_0(58)?, &[-1, 0, 1], julian_0(59)?, -DAY_1, &[true, true, true]); 780 | check_all(julian_0(364)?, &[-1, 0, 1], julian_0(0)?, -DAY_1, &[true, true, false]); 781 | check_all(julian_0(365)?, &[-1, 0, 1], julian_0(0)?, 0, &[true, true, false]); 782 | 783 | check_all(julian_1(90)?, &[-1, 0, 1], julian_0(90)?, 0, &[true, true, false]); 784 | check_all(julian_1(365)?, &[-1, 0, 1], julian_0(0)?, 0, &[true, true, true]); 785 | 786 | check_all(julian_0(89)?, &[-1, 0, 1], julian_1(90)?, 0, &[true, true, false]); 787 | check_all(julian_0(364)?, &[-1, 0, 1], julian_1(1)?, -DAY_1, &[true, true, false]); 788 | check_all(julian_0(365)?, &[-1, 0, 1], julian_1(1)?, 0, &[true, true, false]); 789 | 790 | check_all(mwd(1, 4, 0)?, &[-1, 0, 1], julian_1(28)?, 0, &[true, true, false]); 791 | check_all(mwd(2, 5, 0)?, &[-1, 0, 1], julian_1(60)?, -DAY_1, &[true, true, false]); 792 | check_all(mwd(12, 5, 0)?, &[-1, 0, 1], julian_1(1)?, -DAY_1, &[true, true, false]); 793 | check_all(mwd(12, 5, 0)?, &[DAY_3 - 1, DAY_3, DAY_3 + 1], julian_1(1)?, -DAY_4, &[false, true, true]); 794 | 795 | check_all(mwd(1, 4, 0)?, &[-1, 0, 1], julian_0(27)?, 0, &[true, true, false]); 796 | check_all(mwd(2, 5, 0)?, &[-1, 0, 1], julian_0(58)?, DAY_1, &[true, true, false]); 797 | check_all(mwd(2, 4, 0)?, &[-1, 0, 1], julian_0(59)?, -DAY_1, &[true, true, false]); 798 | check_all(mwd(2, 5, 0)?, &[-1, 0, 1], julian_0(59)?, 0, &[true, true, false]); 799 | check_all(mwd(12, 5, 0)?, &[-1, 0, 1], julian_0(0)?, -DAY_1, &[true, true, false]); 800 | check_all(mwd(12, 5, 0)?, &[DAY_3 - 1, DAY_3, DAY_3 + 1], julian_0(0)?, -DAY_4, &[false, true, true]); 801 | 802 | check_all(julian_1(1)?, &[-1, 0, 1], mwd(1, 1, 0)?, 0, &[true, true, false]); 803 | check_all(julian_1(53)?, &[-1, 0, 1], mwd(2, 5, 0)?, 0, &[true, true, false]); 804 | check_all(julian_1(365)?, &[-1, 0, 1], mwd(1, 1, 0)?, -DAY_1, &[true, true, false]); 805 | check_all(julian_1(365)?, &[DAY_3 - 1, DAY_3, DAY_3 + 1], mwd(1, 1, 0)?, -DAY_4, &[false, true, true]); 806 | 807 | check_all(julian_0(0)?, &[-1, 0, 1], mwd(1, 1, 0)?, 0, &[true, true, false]); 808 | check_all(julian_0(52)?, &[-1, 0, 1], mwd(2, 5, 0)?, 0, &[true, true, false]); 809 | check_all(julian_0(59)?, &[-1, 0, 1], mwd(3, 1, 0)?, 0, &[true, true, false]); 810 | check_all(julian_0(59)?, &[-DAY_3 - 1, -DAY_3, -DAY_3 + 1], mwd(2, 5, 0)?, DAY_4, &[true, true, false]); 811 | check_all(julian_0(364)?, &[-1, 0, 1], mwd(1, 1, 0)?, -DAY_1, &[true, true, false]); 812 | check_all(julian_0(365)?, &[-1, 0, 1], mwd(1, 1, 0)?, 0, &[true, true, false]); 813 | check_all(julian_0(364)?, &[DAY_4 - 1, DAY_4, DAY_4 + 1], mwd(1, 1, 0)?, -DAY_4, &[false, true, true]); 814 | check_all(julian_0(365)?, &[DAY_3 - 1, DAY_3, DAY_3 + 1], mwd(1, 1, 0)?, -DAY_4, &[false, true, true]); 815 | 816 | let months_per_year = MONTHS_PER_YEAR as u8; 817 | for i in 0..months_per_year - 1 { 818 | let month = i + 1; 819 | let month_1 = (i + 1) % months_per_year + 1; 820 | let month_2 = (i + 2) % months_per_year + 1; 821 | 822 | assert!(check(mwd(month, 1, 0)?, 0, mwd(month_2, 1, 0)?, 0)); 823 | assert!(check(mwd(month, 3, 0)?, DAY_4, mwd(month, 4, 0)?, -DAY_3)); 824 | 825 | check_all(mwd(month, 5, 0)?, &[-1, 0, 1], mwd(month, 5, 0)?, 0, &[true, true, true]); 826 | check_all(mwd(month, 4, 0)?, &[-1, 0, 1], mwd(month, 5, 0)?, 0, &[true, true, false]); 827 | check_all(mwd(month, 4, 0)?, &[DAY_4 - 1, DAY_4, DAY_4 + 1], mwd(month_1, 1, 0)?, -DAY_3, &[true, true, false]); 828 | check_all(mwd(month, 5, 0)?, &[DAY_4 - 1, DAY_4, DAY_4 + 1], mwd(month_1, 1, 0)?, -DAY_3, &[true, true, true]); 829 | check_all(mwd(month, 5, 0)?, &[-1, 0, 1], mwd(month_1, 5, 0)?, 0, &[true, true, true]); 830 | check_all(mwd(month, 3, 2)?, &[-1, 0, 1], mwd(month, 4, 3)?, -DAY_1, &[true, true, false]); 831 | check_all(mwd(month, 5, 2)?, &[-1, 0, 1], mwd(month, 5, 3)?, -DAY_1, &[false, true, true]); 832 | check_all(mwd(month, 5, 2)?, &[-1, 0, 1], mwd(month_1, 1, 3)?, -DAY_1, &[true, true, false]); 833 | check_all(mwd(month, 5, 2)?, &[-1, 0, 1], mwd(month_1, 5, 3)?, 0, &[true, true, true]); 834 | } 835 | 836 | check_all(mwd(2, 4, 2)?, &[-1, 0, 1], mwd(2, 5, 3)?, -DAY_1, &[false, true, true]); 837 | 838 | check_all(mwd(3, 4, 2)?, &[-1, 0, 1], mwd(3, 5, 4)?, -DAY_2, &[true, true, false]); 839 | check_all(mwd(3, 4, 2)?, &[-1, 0, 1], mwd(3, 5, 5)?, -DAY_3, &[true, true, true]); 840 | check_all(mwd(3, 4, 2)?, &[-1, 0, 1], mwd(3, 5, 6)?, -DAY_4, &[false, true, true]); 841 | 842 | check_all(mwd(4, 4, 2)?, &[-1, 0, 1], mwd(4, 5, 3)?, -DAY_1, &[true, true, false]); 843 | check_all(mwd(4, 4, 2)?, &[-1, 0, 1], mwd(4, 5, 4)?, -DAY_2, &[true, true, true]); 844 | check_all(mwd(4, 4, 2)?, &[-1, 0, 1], mwd(4, 5, 5)?, -DAY_3, &[false, true, true]); 845 | 846 | check_all(mwd(2, 4, 2)?, &[DAY_5 - 1, DAY_5, DAY_5 + 1], mwd(3, 1, 3)?, -DAY_3, &[false, true, true]); 847 | 848 | check_all(mwd(3, 4, 2)?, &[DAY_5 - 1, DAY_5, DAY_5 + 1], mwd(4, 1, 4)?, -DAY_4, &[true, true, false]); 849 | check_all(mwd(3, 4, 2)?, &[DAY_5 - 1, DAY_5, DAY_5 + 1], mwd(4, 1, 5)?, -DAY_5, &[true, true, true]); 850 | check_all(mwd(3, 4, 2)?, &[DAY_5 - 1, DAY_5, DAY_5 + 1], mwd(4, 1, 6)?, -DAY_6, &[false, true, true]); 851 | 852 | check_all(mwd(4, 4, 2)?, &[DAY_5 - 1, DAY_5, DAY_5 + 1], mwd(5, 1, 3)?, -DAY_3, &[true, true, false]); 853 | check_all(mwd(4, 4, 2)?, &[DAY_5 - 1, DAY_5, DAY_5 + 1], mwd(5, 1, 4)?, -DAY_4, &[true, true, true]); 854 | check_all(mwd(4, 4, 2)?, &[DAY_5 - 1, DAY_5, DAY_5 + 1], mwd(5, 1, 5)?, -DAY_5, &[false, true, true]); 855 | 856 | Ok(()) 857 | } 858 | 859 | #[test] 860 | fn test_rule_day() -> Result<(), TzError> { 861 | let rule_day_j1 = RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(60)?); 862 | assert_eq!(rule_day_j1.transition_date(2000), (3, 1)); 863 | assert_eq!(rule_day_j1.transition_date(2001), (3, 1)); 864 | assert_eq!(rule_day_j1.unix_time(2000, 43200), 951912000); 865 | 866 | let rule_day_j0 = RuleDay::Julian0WithLeap(Julian0WithLeap::new(59)?); 867 | assert_eq!(rule_day_j0.transition_date(2000), (2, 29)); 868 | assert_eq!(rule_day_j0.transition_date(2001), (3, 1)); 869 | assert_eq!(rule_day_j0.unix_time(2000, 43200), 951825600); 870 | 871 | let rule_day_j0_max = RuleDay::Julian0WithLeap(Julian0WithLeap::new(365)?); 872 | assert_eq!(rule_day_j0_max.transition_date(2000), (12, 31)); 873 | assert_eq!(rule_day_j0_max.transition_date(2001), (12, 32)); 874 | 875 | assert_eq!( 876 | RuleDay::Julian0WithLeap(Julian0WithLeap::new(365)?).unix_time(2000, 0), 877 | RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(365)?).unix_time(2000, 0) 878 | ); 879 | 880 | assert_eq!( 881 | RuleDay::Julian0WithLeap(Julian0WithLeap::new(365)?).unix_time(1999, 0), 882 | RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(1)?).unix_time(2000, 0), 883 | ); 884 | 885 | let rule_day_mwd = RuleDay::MonthWeekDay(MonthWeekDay::new(2, 5, 2)?); 886 | assert_eq!(rule_day_mwd.transition_date(2000), (2, 29)); 887 | assert_eq!(rule_day_mwd.transition_date(2001), (2, 27)); 888 | assert_eq!(rule_day_mwd.unix_time(2000, 43200), 951825600); 889 | assert_eq!(rule_day_mwd.unix_time(2001, 43200), 983275200); 890 | 891 | Ok(()) 892 | } 893 | 894 | #[test] 895 | fn test_transition_rule() -> Result<(), TzError> { 896 | let transition_rule_fixed = TransitionRule::Fixed(LocalTimeType::new(-36000, false, None)?); 897 | assert_eq!(transition_rule_fixed.find_local_time_type(0)?.ut_offset(), -36000); 898 | 899 | let transition_rule_dst = TransitionRule::Alternate(AlternateTime::new( 900 | LocalTimeType::new(43200, false, Some(b"NZST"))?, 901 | LocalTimeType::new(46800, true, Some(b"NZDT"))?, 902 | RuleDay::MonthWeekDay(MonthWeekDay::new(10, 1, 0)?), 903 | 7200, 904 | RuleDay::MonthWeekDay(MonthWeekDay::new(3, 3, 0)?), 905 | 7200, 906 | )?); 907 | 908 | assert_eq!(transition_rule_dst.find_local_time_type(953384399)?.ut_offset(), 46800); 909 | assert_eq!(transition_rule_dst.find_local_time_type(953384400)?.ut_offset(), 43200); 910 | assert_eq!(transition_rule_dst.find_local_time_type(970322399)?.ut_offset(), 43200); 911 | assert_eq!(transition_rule_dst.find_local_time_type(970322400)?.ut_offset(), 46800); 912 | 913 | let transition_rule_negative_dst = TransitionRule::Alternate(AlternateTime::new( 914 | LocalTimeType::new(3600, false, Some(b"IST"))?, 915 | LocalTimeType::new(0, true, Some(b"GMT"))?, 916 | RuleDay::MonthWeekDay(MonthWeekDay::new(10, 5, 0)?), 917 | 7200, 918 | RuleDay::MonthWeekDay(MonthWeekDay::new(3, 5, 0)?), 919 | 3600, 920 | )?); 921 | 922 | assert_eq!(transition_rule_negative_dst.find_local_time_type(954032399)?.ut_offset(), 0); 923 | assert_eq!(transition_rule_negative_dst.find_local_time_type(954032400)?.ut_offset(), 3600); 924 | assert_eq!(transition_rule_negative_dst.find_local_time_type(972781199)?.ut_offset(), 3600); 925 | assert_eq!(transition_rule_negative_dst.find_local_time_type(972781200)?.ut_offset(), 0); 926 | 927 | let transition_rule_negative_time_1 = TransitionRule::Alternate(AlternateTime::new( 928 | LocalTimeType::new(0, false, None)?, 929 | LocalTimeType::new(0, true, None)?, 930 | RuleDay::Julian0WithLeap(Julian0WithLeap::new(100)?), 931 | 0, 932 | RuleDay::Julian0WithLeap(Julian0WithLeap::new(101)?), 933 | -86500, 934 | )?); 935 | 936 | assert!(transition_rule_negative_time_1.find_local_time_type(8639899)?.is_dst()); 937 | assert!(!transition_rule_negative_time_1.find_local_time_type(8639900)?.is_dst()); 938 | assert!(!transition_rule_negative_time_1.find_local_time_type(8639999)?.is_dst()); 939 | assert!(transition_rule_negative_time_1.find_local_time_type(8640000)?.is_dst()); 940 | 941 | let transition_rule_negative_time_2 = TransitionRule::Alternate(AlternateTime::new( 942 | LocalTimeType::new(-10800, false, Some(b"-03"))?, 943 | LocalTimeType::new(-7200, true, Some(b"-02"))?, 944 | RuleDay::MonthWeekDay(MonthWeekDay::new(3, 5, 0)?), 945 | -7200, 946 | RuleDay::MonthWeekDay(MonthWeekDay::new(10, 5, 0)?), 947 | -3600, 948 | )?); 949 | 950 | assert_eq!(transition_rule_negative_time_2.find_local_time_type(954032399)?.ut_offset(), -10800); 951 | assert_eq!(transition_rule_negative_time_2.find_local_time_type(954032400)?.ut_offset(), -7200); 952 | assert_eq!(transition_rule_negative_time_2.find_local_time_type(972781199)?.ut_offset(), -7200); 953 | assert_eq!(transition_rule_negative_time_2.find_local_time_type(972781200)?.ut_offset(), -10800); 954 | 955 | let transition_rule_all_year_dst = TransitionRule::Alternate(AlternateTime::new( 956 | LocalTimeType::new(-18000, false, Some(b"EST"))?, 957 | LocalTimeType::new(-14400, true, Some(b"EDT"))?, 958 | RuleDay::Julian0WithLeap(Julian0WithLeap::new(0)?), 959 | 0, 960 | RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(365)?), 961 | 90000, 962 | )?); 963 | 964 | assert_eq!(transition_rule_all_year_dst.find_local_time_type(946702799)?.ut_offset(), -14400); 965 | assert_eq!(transition_rule_all_year_dst.find_local_time_type(946702800)?.ut_offset(), -14400); 966 | 967 | Ok(()) 968 | } 969 | 970 | #[test] 971 | fn test_transition_rule_overflow() -> Result<(), TzError> { 972 | let transition_rule_1 = TransitionRule::Alternate(AlternateTime::new( 973 | LocalTimeType::new(-1, false, None)?, 974 | LocalTimeType::new(-1, true, None)?, 975 | RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(365)?), 976 | 0, 977 | RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(1)?), 978 | 0, 979 | )?); 980 | 981 | let transition_rule_2 = TransitionRule::Alternate(AlternateTime::new( 982 | LocalTimeType::new(1, false, None)?, 983 | LocalTimeType::new(1, true, None)?, 984 | RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(365)?), 985 | 0, 986 | RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(1)?), 987 | 0, 988 | )?); 989 | 990 | assert!(matches!(transition_rule_1.find_local_time_type(i64::MIN), Err(TzError::OutOfRange))); 991 | assert!(matches!(transition_rule_2.find_local_time_type(i64::MAX), Err(TzError::OutOfRange))); 992 | 993 | Ok(()) 994 | } 995 | } 996 | -------------------------------------------------------------------------------- /src/utils/const_fns.rs: -------------------------------------------------------------------------------- 1 | //! Some useful constant functions. 2 | 3 | use crate::error::TzError; 4 | use crate::timezone::{LeapSecond, Transition}; 5 | 6 | use core::cmp::Ordering; 7 | 8 | /// Compare two values 9 | pub(crate) const fn cmp(a: i64, b: i64) -> Ordering { 10 | if a < b { 11 | Ordering::Less 12 | } else if a == b { 13 | Ordering::Equal 14 | } else { 15 | Ordering::Greater 16 | } 17 | } 18 | 19 | /// Returns the minimum of two values 20 | pub(crate) const fn min(a: i64, b: i64) -> i64 { 21 | match cmp(a, b) { 22 | Ordering::Less | Ordering::Equal => a, 23 | Ordering::Greater => b, 24 | } 25 | } 26 | 27 | /// Macro for implementing integer conversion 28 | macro_rules! impl_try_into_integer { 29 | ($from_type:ty, $to_type:ty, $value:expr) => {{ 30 | let min = <$to_type>::MIN as $from_type; 31 | let max = <$to_type>::MAX as $from_type; 32 | 33 | if min <= $value && $value <= max { 34 | Ok($value as $to_type) 35 | } else { 36 | Err(TzError::OutOfRange) 37 | } 38 | }}; 39 | } 40 | 41 | /// Convert a `i64` value to a `i32` value 42 | pub(crate) const fn try_into_i32(value: i64) -> Result { 43 | impl_try_into_integer!(i64, i32, value) 44 | } 45 | 46 | /// Convert a `i128` value to a `i64` value 47 | pub(crate) const fn try_into_i64(value: i128) -> Result { 48 | impl_try_into_integer!(i128, i64, value) 49 | } 50 | 51 | /// Macro for implementing binary search 52 | macro_rules! impl_binary_search { 53 | ($slice:expr, $f:expr, $x:expr) => {{ 54 | let mut size = $slice.len(); 55 | let mut left = 0; 56 | let mut right = size; 57 | while left < right { 58 | let mid = left + size / 2; 59 | 60 | let v = $f(&$slice[mid]); 61 | if v < $x { 62 | left = mid + 1; 63 | } else if v > $x { 64 | right = mid; 65 | } else { 66 | return Ok(mid); 67 | } 68 | 69 | size = right - left; 70 | } 71 | Err(left) 72 | }}; 73 | } 74 | 75 | /// Copy the input value 76 | const fn copied(x: &i64) -> i64 { 77 | *x 78 | } 79 | 80 | /// Binary searches a sorted `i64` slice for the given element 81 | pub(crate) const fn binary_search_i64(slice: &[i64], x: i64) -> Result { 82 | impl_binary_search!(slice, copied, x) 83 | } 84 | 85 | /// Binary searches a sorted `Transition` slice for the given element 86 | pub(crate) const fn binary_search_transitions(slice: &[Transition], x: i64) -> Result { 87 | impl_binary_search!(slice, Transition::unix_leap_time, x) 88 | } 89 | 90 | /// Binary searches a sorted `LeapSecond` slice for the given element 91 | pub(crate) const fn binary_search_leap_seconds(slice: &[LeapSecond], x: i64) -> Result { 92 | impl_binary_search!(slice, LeapSecond::unix_leap_time, x) 93 | } 94 | -------------------------------------------------------------------------------- /src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | //! Some useful utilities. 2 | 3 | mod const_fns; 4 | 5 | #[cfg(feature = "std")] 6 | mod system_time; 7 | 8 | pub(crate) use const_fns::{binary_search_i64, binary_search_leap_seconds, binary_search_transitions, cmp, min, try_into_i32, try_into_i64}; 9 | 10 | #[cfg(feature = "std")] 11 | pub(crate) use system_time::{current_total_nanoseconds, current_unix_time}; 12 | -------------------------------------------------------------------------------- /src/utils/system_time.rs: -------------------------------------------------------------------------------- 1 | //! Some useful system time functions. 2 | 3 | use std::time::{Duration, SystemTime}; 4 | 5 | /// Returns the duration between Unix epoch (`1970-01-01T00:00:00Z`) and now. 6 | /// 7 | /// The `Ok` variant corresponds to a positive duration, and the `Err` variant to a negative duration. 8 | fn current_duration_since_epoch() -> Result { 9 | SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).map_err(|e| e.duration()) 10 | } 11 | 12 | /// Returns the current Unix time in seconds 13 | pub(crate) fn current_unix_time() -> i64 { 14 | match current_duration_since_epoch() { 15 | Ok(duration) => 0i64.saturating_add_unsigned(duration.as_secs()), 16 | Err(duration) => 0i64.saturating_sub_unsigned(duration.as_secs()), 17 | } 18 | } 19 | 20 | /// Returns the total nanoseconds between Unix epoch (`1970-01-01T00:00:00Z`) and now 21 | pub(crate) fn current_total_nanoseconds() -> i128 { 22 | match current_duration_since_epoch() { 23 | Ok(duration) => 0i128.saturating_add_unsigned(duration.as_nanos()), 24 | Err(duration) => 0i128.saturating_sub_unsigned(duration.as_nanos()), 25 | } 26 | } 27 | --------------------------------------------------------------------------------