tz-rs-0.6.14/.cargo_vcs_info.json0000644000000001360000000000100122030ustar { "git": { "sha1": "2ebd88d3af227b45a089141d9afa6ca1627d9eca" }, "path_in_vcs": "" }tz-rs-0.6.14/.github/dependabot.yml000064400000000000000000000003161046102023000151630ustar 00000000000000version: 2 updates: - package-ecosystem: "cargo" directory: "/" schedule: interval: "daily" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" tz-rs-0.6.14/.github/workflows/audit.yml000064400000000000000000000007141046102023000162230ustar 00000000000000name: Audit on: schedule: - cron: "0 0 * * 0" jobs: audit: name: "Audit" runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 - name: Install Rust id: actions-rs uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true - name: Audit run: | cargo install cargo-audit cargo audit tz-rs-0.6.14/.github/workflows/ci.yml000064400000000000000000000033521046102023000155110ustar 00000000000000name: CI on: push: branches: ["master"] pull_request: branches: ["master"] schedule: - cron: "0 0 * * 0" jobs: check: name: "Check" runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 - name: Install Rust id: actions-rs uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: stable override: true components: rustfmt, clippy - name: Format run: cargo fmt --all -- --check - name: Lint run: cargo clippy -- -D warnings doc: name: "Doc" runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 - name: Install Rust id: actions-rs uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: nightly override: true - name: Doc run: RUSTDOCFLAGS="-D warnings --cfg docsrs" cargo doc --all-features --no-deps test: strategy: matrix: rust: [1.45, 1.55, stable, nightly] features: ["", alloc, std] include: - const: const - rust: 1.45 const: "" name: "Test/${{ matrix.rust }}/Features=${{ matrix.const }},${{ matrix.features }}" runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v3 - name: Install Rust id: actions-rs uses: actions-rs/toolchain@v1 with: profile: minimal toolchain: ${{ matrix.rust }} override: true - name: Test run: cargo test --no-default-features --features=${{ matrix.const }},${{ matrix.features }} env: CARGO_NET_GIT_FETCH_WITH_CLI: true tz-rs-0.6.14/.gitignore000064400000000000000000000000321046102023000127560ustar 00000000000000.vscode Cargo.lock target tz-rs-0.6.14/Cargo.lock0000644000000005660000000000100101650ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "const_fn" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbdcdcb6d86f71c5e97409ad45898af11cbc995b4ee8112d59095a28d376c935" [[package]] name = "tz-rs" version = "0.6.14" dependencies = [ "const_fn", ] tz-rs-0.6.14/Cargo.toml0000644000000022130000000000100101770ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2018" name = "tz-rs" version = "0.6.14" authors = ["x-hgg-x"] description = "A pure Rust reimplementation of libc functions localtime, gmtime and mktime." readme = "README.md" keywords = [ "date", "time", "timezone", "zone", "calendar", ] categories = [ "date-and-time", "parser-implementations", ] license = "MIT OR Apache-2.0" repository = "https://github.com/x-hgg-x/tz-rs" [package.metadata.docs.rs] all-features = true rustdoc-args = [ "--cfg", "docsrs", ] [lib] name = "tz" [dependencies.const_fn] version = "0.4.4" optional = true [features] alloc = [] const = ["const_fn"] default = [ "std", "const", ] std = ["alloc"] tz-rs-0.6.14/Cargo.toml.orig000064400000000000000000000012071046102023000136620ustar 00000000000000[package] name = "tz-rs" version = "0.6.14" edition = "2018" authors = ["x-hgg-x"] repository = "https://github.com/x-hgg-x/tz-rs" description = "A pure Rust reimplementation of libc functions localtime, gmtime and mktime." license = "MIT OR Apache-2.0" keywords = ["date", "time", "timezone", "zone", "calendar"] categories = ["date-and-time", "parser-implementations"] readme = "README.md" [lib] name = "tz" [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] [dependencies] const_fn = { version = "0.4.4", optional = true } [features] default = ["std", "const"] std = ["alloc"] alloc = [] const = ["const_fn"] tz-rs-0.6.14/LICENSE-Apache000064400000000000000000000261351046102023000131660ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. tz-rs-0.6.14/LICENSE-MIT000064400000000000000000000020501046102023000124240ustar 00000000000000MIT License Copyright (c) 2022 x-hgg-x Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. tz-rs-0.6.14/README.md000064400000000000000000000073661046102023000122660ustar 00000000000000# tz-rs [![version](https://img.shields.io/crates/v/tz-rs?color=blue&style=flat-square)](https://crates.io/crates/tz-rs) ![Minimum supported Rust version](https://img.shields.io/badge/rustc-1.45+-important?logo=rust "Minimum Supported Rust Version") [![Documentation](https://docs.rs/tz-rs/badge.svg)](https://docs.rs/tz-rs) 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). This crate allows to convert between a [Unix timestamp](https://en.wikipedia.org/wiki/Unix_time) and a calendar time exprimed in the [proleptic gregorian calendar](https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar) with a provided time zone. 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. Two formats are currently accepted for the `TZ` string: * `std offset[dst[offset][,start[/time],end[/time]]]` providing a time zone description, * `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. 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). ## Context 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. 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. ## Documentation Documentation is hosted on [docs.rs](https://docs.rs/tz-rs/latest/tz/). ## Platform support This crate is mainly intended for UNIX platforms. 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. The database files can then be read by specifying an absolute path in the `TZ` string: ```rust TimeZone::from_posix_tz(format!("{local_database_dir}/usr/share/zoneinfo/Pacific/Auckland"))?; ``` Note that the determination of the local time zone with this crate is not supported on non-UNIX platforms. 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. ## Date time formatting (equivalent of libc `strftime` function) 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. ## Compiler support Requires `rustc 1.45+` when building with no default features, or `rustc 1.55+` otherwise. ## License This project is licensed under either of - [Apache License, Version 2.0](https://github.com/x-hgg-x/tz-rs/blob/master/LICENSE-Apache) - [MIT license](https://github.com/x-hgg-x/tz-rs/blob/master/LICENSE-MIT) at your option. Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. tz-rs-0.6.14/check_release.sh000075500000000000000000000013101046102023000141020ustar 00000000000000#!/bin/sh cargo +1.45 test --no-default-features cargo +1.45 test --no-default-features --features=alloc cargo +1.45 test --no-default-features --features=std cargo +1.55 test --no-default-features --features=const cargo +1.55 test --no-default-features --features=const,alloc cargo +1.55 test --no-default-features --features=const,std cargo +stable test --no-default-features --features=const cargo +stable test --no-default-features --features=const,alloc cargo +stable test --no-default-features --features=const,std cargo +nightly test --no-default-features --features=const cargo +nightly test --no-default-features --features=const,alloc cargo +nightly test --no-default-features --features=const,std tz-rs-0.6.14/examples/statics.rs000064400000000000000000000062151046102023000146350ustar 00000000000000fn main() -> tz::Result<()> { #[cfg(feature = "const")] { use tz::datetime::*; use tz::timezone::*; macro_rules! unwrap { ($x:expr) => { match $x { Ok(x) => x, Err(_) => [][0], } }; } macro_rules! to_const { ($type:ty, $x:expr) => {{ const TMP: $type = $x; TMP }}; } const TIME_ZONE_REF: TimeZoneRef = unwrap!(TimeZoneRef::new( &[ Transition::new(-2334101314, 1), Transition::new(-1157283000, 2), Transition::new(-1155436200, 1), Transition::new(-880198200, 3), Transition::new(-769395600, 4), Transition::new(-765376200, 1), Transition::new(-712150200, 5), ], to_const!( &[LocalTimeType], &[ unwrap!(LocalTimeType::new(-37886, false, Some(b"LMT"))), unwrap!(LocalTimeType::new(-37800, false, Some(b"HST"))), unwrap!(LocalTimeType::new(-34200, true, Some(b"HDT"))), unwrap!(LocalTimeType::new(-34200, true, Some(b"HWT"))), unwrap!(LocalTimeType::new(-34200, true, Some(b"HPT"))), unwrap!(LocalTimeType::new(-36000, false, Some(b"HST"))), ] ), &[ LeapSecond::new(78796800, 1), LeapSecond::new(94694401, 2), LeapSecond::new(126230402, 3), LeapSecond::new(157766403, 4), LeapSecond::new(189302404, 5), LeapSecond::new(220924805, 6), ], to_const!( &Option, &Some(TransitionRule::Alternate(unwrap!(AlternateTime::new( unwrap!(LocalTimeType::new(-36000, false, Some(b"HST"))), unwrap!(LocalTimeType::new(-34200, true, Some(b"HPT"))), RuleDay::MonthWeekDay(unwrap!(MonthWeekDay::new(10, 5, 0))), 93600, RuleDay::MonthWeekDay(unwrap!(MonthWeekDay::new(3, 4, 4))), 7200, )))) ), )); const UTC: TimeZoneRef = TimeZoneRef::utc(); const UNIX_EPOCH: UtcDateTime = unwrap!(UtcDateTime::from_timespec(0, 0)); const UTC_DATE_TIME: UtcDateTime = unwrap!(UtcDateTime::new(2000, 1, 1, 0, 0, 0, 1000)); const DATE_TIME: DateTime = unwrap!(DateTime::new(2000, 1, 1, 1, 0, 0, 1000, unwrap!(LocalTimeType::with_ut_offset(3600)))); const DATE_TIME_1: DateTime = unwrap!(UTC_DATE_TIME.project(TIME_ZONE_REF)); const DATE_TIME_2: DateTime = unwrap!(DATE_TIME_1.project(UTC)); println!("{:#?}", TIME_ZONE_REF); println!("{:?}", TIME_ZONE_REF.find_local_time_type(0)); println!("{:?}", UNIX_EPOCH); println!("{:?}", UTC_DATE_TIME); println!("{:#?}", DATE_TIME); println!("{:#?}", DATE_TIME_1); println!("{:#?}", DATE_TIME_2); } Ok(()) } tz-rs-0.6.14/examples/time.rs000064400000000000000000000123761046102023000141260ustar 00000000000000use tz::*; fn main() -> Result<()> { #[cfg(feature = "std")] { // // TimeZone // // 2000-01-01T00:00:00Z let unix_time = 946684800; // Get UTC time zone let time_zone_utc = TimeZone::utc(); println!("{:?}", time_zone_utc.find_local_time_type(unix_time)?); // Get fixed time zone at GMT-1 let time_zone_fixed = TimeZone::fixed(-3600)?; println!("{:?}", time_zone_fixed.find_local_time_type(unix_time)?.ut_offset()); // Get local time zone (UNIX only) let time_zone_local = TimeZone::local()?; println!("{:?}", time_zone_local.find_local_time_type(unix_time)?.ut_offset()); // Get the current local time type let current_local_time_type = time_zone_local.find_current_local_time_type()?; println!("{:?}", current_local_time_type); // Get time zone from a TZ string: // From an absolute file let _ = TimeZone::from_posix_tz("/usr/share/zoneinfo/Pacific/Auckland"); // From a file relative to the system timezone directory let _ = TimeZone::from_posix_tz("Pacific/Auckland"); // From a time zone description TimeZone::from_posix_tz("HST10")?; TimeZone::from_posix_tz("<-03>3")?; TimeZone::from_posix_tz("NZST-12:00:00NZDT-13:00:00,M10.1.0,M3.3.0")?; // Use a leading colon to force searching for a corresponding file let _ = TimeZone::from_posix_tz(":UTC"); // // DateTime // // Get the current UTC date time let current_utc_date_time = UtcDateTime::now()?; println!("{:?}", current_utc_date_time); // Create a new UTC date time (2000-01-01T00:00:00.123456789Z) let utc_date_time = UtcDateTime::new(2000, 1, 1, 0, 0, 0, 123_456_789)?; println!("{}", utc_date_time); println!("{:?}", utc_date_time); // Create a new UTC date time from a Unix time with nanoseconds (2000-01-01T00:00:00.123456789Z) let other_utc_date_time = UtcDateTime::from_timespec(946684800, 123_456_789)?; println!("{}", other_utc_date_time); println!("{:?}", other_utc_date_time); // Project the UTC date time to a time zone let date_time = utc_date_time.project(TimeZone::fixed(-3600)?.as_ref())?; println!("{}", date_time); println!("{:#?}", date_time); // Project the date time to another time zone let other_date_time = date_time.project(TimeZone::fixed(3600)?.as_ref())?; println!("{}", other_date_time); println!("{:#?}", other_date_time); // Create a new date time from a Unix time with nanoseconds and a time zone (2000-01-01T00:00:00.123456789Z) let another_date_time = DateTime::from_timespec(946684800, 123_456_789, TimeZone::fixed(86400)?.as_ref())?; println!("{}", another_date_time); println!("{:#?}", another_date_time); // Get the corresponding UTC Unix times with nanoseconds println!("{:?}", (utc_date_time.unix_time(), utc_date_time.nanoseconds())); println!("{:?}", (other_utc_date_time.unix_time(), other_utc_date_time.nanoseconds())); println!("{:?}", (date_time.unix_time(), date_time.nanoseconds())); println!("{:?}", (other_date_time.unix_time(), other_date_time.nanoseconds())); // Nanoseconds are always added towards the future let neg_utc_date_time = UtcDateTime::from_timespec(-1, 123_456_789)?; println!("{}", neg_utc_date_time); println!("{}", neg_utc_date_time.total_nanoseconds()); // Get the current date time at the local time zone (UNIX only) let time_zone_local = TimeZone::local()?; let date_time = DateTime::now(time_zone_local.as_ref())?; println!("{:#?}", date_time); // Create a new date time with an UTC offset (2000-01-01T01:00:00.123456789+01:00) let date_time = DateTime::new(2000, 1, 1, 1, 0, 0, 123_456_789, LocalTimeType::with_ut_offset(3600)?)?; println!("{:#?}", date_time); // // Find the possible date times corresponding to a date, a time and a time zone // let time_zone = TimeZone::from_posix_tz("CET-1CEST,M3.5.0,M10.5.0/3")?; // Found date time is unique let found_date_times = DateTime::find(2000, 1, 1, 0, 0, 0, 0, time_zone.as_ref())?; println!("{:#?}", found_date_times); println!("{:#?}", found_date_times.unique()); println!("{:#?}", found_date_times.earliest()); println!("{:#?}", found_date_times.latest()); // Found date time was skipped by a forward transition let found_date_times = DateTime::find(2000, 3, 26, 2, 30, 0, 0, time_zone.as_ref())?; println!("{:#?}", found_date_times); println!("{:#?}", found_date_times.unique()); println!("{:#?}", found_date_times.earliest()); println!("{:#?}", found_date_times.latest()); // Found date time is ambiguous because of a backward transition let found_date_times = DateTime::find(2000, 10, 29, 2, 30, 0, 0, time_zone.as_ref())?; println!("{:#?}", found_date_times); println!("{:#?}", found_date_times.unique()); println!("{:#?}", found_date_times.earliest()); println!("{:#?}", found_date_times.latest()); } Ok(()) } tz-rs-0.6.14/rustfmt.toml000064400000000000000000000000551046102023000133740ustar 00000000000000max_width = 160 use_small_heuristics = "Max" tz-rs-0.6.14/src/constants/mod.rs000064400000000000000000000043351046102023000147300ustar 00000000000000//! Some useful constants. /// Number of nanoseconds in one second pub const NANOSECONDS_PER_SECOND: u32 = 1_000_000_000; /// Number of seconds in one minute pub const SECONDS_PER_MINUTE: i64 = 60; /// Number of minutes in one hour pub const MINUTES_PER_HOUR: i64 = 60; /// Number of hours in one day pub const HOURS_PER_DAY: i64 = 24; /// Number of seconds in one hour pub const SECONDS_PER_HOUR: i64 = 3600; /// Number of seconds in one day pub const SECONDS_PER_DAY: i64 = SECONDS_PER_HOUR * HOURS_PER_DAY; /// Number of days in one week pub const DAYS_PER_WEEK: i64 = 7; /// Number of seconds in one week pub const SECONDS_PER_WEEK: i64 = SECONDS_PER_DAY * DAYS_PER_WEEK; /// Number of seconds in 28 days pub const SECONDS_PER_28_DAYS: i64 = SECONDS_PER_DAY * 28; /// Number of months in one year pub const MONTHS_PER_YEAR: i64 = 12; /// Number of days in a normal year pub const DAYS_PER_NORMAL_YEAR: i64 = 365; /// Number of seconds in a normal year pub const SECONDS_PER_NORMAL_YEAR: i64 = DAYS_PER_NORMAL_YEAR * SECONDS_PER_DAY; /// Number of seconds in a leap year pub const SECONDS_PER_LEAP_YEAR: i64 = (DAYS_PER_NORMAL_YEAR + 1) * SECONDS_PER_DAY; /// Number of days in 4 years (including 1 leap year) pub const DAYS_PER_4_YEARS: i64 = DAYS_PER_NORMAL_YEAR * 4 + 1; /// Number of days in 100 years (including 24 leap years) pub const DAYS_PER_100_YEARS: i64 = DAYS_PER_NORMAL_YEAR * 100 + 24; /// Number of days in 400 years (including 97 leap years) pub const DAYS_PER_400_YEARS: i64 = DAYS_PER_NORMAL_YEAR * 400 + 97; /// Month days in a normal year pub const DAYS_IN_MONTHS_NORMAL_YEAR: [i64; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; /// Cumulated month days in a normal year pub const CUMUL_DAYS_IN_MONTHS_NORMAL_YEAR: [i64; 12] = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]; /// Cumulated month days in a leap year pub const CUMUL_DAYS_IN_MONTHS_LEAP_YEAR: [i64; 12] = [0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335]; /// Unix time at `2000-03-01T00:00:00Z` (Wednesday) pub const UNIX_OFFSET_SECS: i64 = 951868800; /// Offset year pub const OFFSET_YEAR: i64 = 2000; /// Month days in a leap year from March pub const DAY_IN_MONTHS_LEAP_YEAR_FROM_MARCH: [i64; 12] = [31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 31, 29]; tz-rs-0.6.14/src/datetime/find.rs000064400000000000000000001004531046102023000146470ustar 00000000000000//! Types related to the [`DateTime::find`] method. use crate::datetime::*; use crate::timezone::TransitionRule; use crate::Result; #[cfg(feature = "alloc")] use alloc::vec::Vec; /// Type of a found date time created by the [`DateTime::find`] method #[derive(Debug, Copy, Clone, PartialEq)] pub enum FoundDateTimeKind { /// Found date time is valid Normal(DateTime), /// Found date time is invalid because it was skipped by a forward transition. /// /// This variant gives the two [`DateTime`] corresponding to the transition instant, just before and just after the transition. /// /// This is different from the `mktime` behavior, which allows invalid date times when no DST information is available (by specifying `tm_isdst = -1`). Skipped { /// Date time just before the forward transition before_transition: DateTime, /// Date time just after the forward transition after_transition: DateTime, }, } /// List containing the found date times created by the [`DateTime::find`] method. /// /// It can be empty if no local time type was found for the provided date, time and time zone. /// #[cfg(feature = "alloc")] #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] #[derive(Debug, Default, Clone, PartialEq)] pub struct FoundDateTimeList(Vec); #[cfg(feature = "alloc")] impl FoundDateTimeList { /// Returns the found date time if existing and unique pub fn unique(&self) -> Option { match *self.0.as_slice() { [FoundDateTimeKind::Normal(date_time)] => Some(date_time), _ => None, } } /// Returns the earliest found date time if existing pub fn earliest(&self) -> Option { // Found date times are computed in ascending order of Unix times match *self.0.first()? { FoundDateTimeKind::Normal(date_time) => Some(date_time), FoundDateTimeKind::Skipped { before_transition, .. } => Some(before_transition), } } /// Returns the latest found date time if existing pub fn latest(&self) -> Option { // Found date times are computed in ascending order of Unix times match *self.0.last()? { FoundDateTimeKind::Normal(date_time) => Some(date_time), FoundDateTimeKind::Skipped { after_transition, .. } => Some(after_transition), } } /// Extracts and returns the inner list of found date times pub fn into_inner(self) -> Vec { self.0 } } /// Wrapper reference type with methods for extracting the found date times, created by the [`DateTime::find_n`] method #[derive(Debug, PartialEq)] pub struct FoundDateTimeListRefMut<'a> { /// Preallocated buffer buf: &'a mut [Option], /// Current index current_index: usize, /// Total count of found date times count: usize, } impl<'a> FoundDateTimeListRefMut<'a> { /// Construct a new [`FoundDateTimeListRefMut`] value pub fn new(buf: &'a mut [Option]) -> Self { Self { buf, current_index: 0, count: 0 } } /// Returns the found date time if existing and unique pub fn unique(&self) -> Option { let mut iter = self.data().iter().flatten(); let first = iter.next(); let second = iter.next(); match (first, second) { (Some(FoundDateTimeKind::Normal(date_time)), None) => Some(*date_time), _ => None, } } /// Returns the earliest found date time if existing pub fn earliest(&self) -> Option { // Found date times are computed in ascending order of Unix times match *self.data().iter().flatten().next()? { FoundDateTimeKind::Normal(date_time) => Some(date_time), FoundDateTimeKind::Skipped { before_transition, .. } => Some(before_transition), } } /// Returns the latest found date time if existing pub fn latest(&self) -> Option { // Found date times are computed in ascending order of Unix times match *self.data().iter().flatten().next_back()? { FoundDateTimeKind::Normal(date_time) => Some(date_time), FoundDateTimeKind::Skipped { after_transition, .. } => Some(after_transition), } } /// Returns the subslice of written data pub fn data(&self) -> &[Option] { &self.buf[..self.current_index] } /// Returns the count of found date times pub fn count(&self) -> usize { self.count } /// Returns `true` if all found date times have been written in the buffer pub fn is_exhaustive(&self) -> bool { self.current_index == self.count } } /// Trait representing a list of found date times pub(super) trait DateTimeList { /// Appends a found date time to the list fn push(&mut self, found_date_time: FoundDateTimeKind); } #[cfg(feature = "alloc")] impl DateTimeList for FoundDateTimeList { fn push(&mut self, found_date_time: FoundDateTimeKind) { self.0.push(found_date_time); } } impl<'a> DateTimeList for FoundDateTimeListRefMut<'a> { fn push(&mut self, found_date_time: FoundDateTimeKind) { if let Some(x) = self.buf.get_mut(self.current_index) { *x = Some(found_date_time); self.current_index += 1 } self.count += 1; } } /// Find the possible date times corresponding to a date, a time and a time zone /// /// ## Inputs /// /// * `found_date_time_list`: Buffer containing found date times /// * `year`: Year /// * `month`: Month in `[1, 12]` /// * `month_day`: Day of the month in `[1, 31]` /// * `hour`: Hours since midnight in `[0, 23]` /// * `minute`: Minutes in `[0, 59]` /// * `second`: Seconds in `[0, 60]`, with a possible leap second /// * `nanoseconds`: Nanoseconds in `[0, 999_999_999]` /// * `time_zone_ref`: Reference to a time zone /// #[allow(clippy::too_many_arguments)] pub(super) fn find_date_time( found_date_time_list: &mut impl DateTimeList, year: i32, month: u8, month_day: u8, hour: u8, minute: u8, second: u8, nanoseconds: u32, time_zone_ref: TimeZoneRef, ) -> Result<()> { let transitions = time_zone_ref.transitions(); let local_time_types = time_zone_ref.local_time_types(); let extra_rule = time_zone_ref.extra_rule(); if transitions.is_empty() && extra_rule.is_none() { let date_time = DateTime::new(year, month, month_day, hour, minute, second, nanoseconds, local_time_types[0])?; found_date_time_list.push(FoundDateTimeKind::Normal(date_time)); return Ok(()); } let new_datetime = |local_time_type, unix_time| DateTime { year, month, month_day, hour, minute, second, local_time_type, unix_time, nanoseconds }; check_date_time_inputs(year, month, month_day, hour, minute, second, nanoseconds)?; let utc_unix_time = unix_time(year, month, month_day, hour, minute, second); // Process transitions if !transitions.is_empty() { let mut last_cached_time = None; let mut get_time = |local_time_type_index: usize| { match last_cached_time { Some((index, value)) if index == local_time_type_index => Result::Ok(value), _ => { // Overflow is not possible let unix_time = utc_unix_time - local_time_types[local_time_type_index].ut_offset() as i64; let unix_leap_time = time_zone_ref.unix_time_to_unix_leap_time(unix_time)?; last_cached_time = Some((local_time_type_index, (unix_time, unix_leap_time))); Result::Ok((unix_time, unix_leap_time)) } } }; let mut previous_transition_unix_leap_time = i64::MIN; let mut previous_local_time_type_index = 0; // Check transitions in order for (index, transition) in transitions.iter().enumerate() { let local_time_type_before = local_time_types[previous_local_time_type_index]; let (unix_time_before, unix_leap_time_before) = get_time(previous_local_time_type_index)?; if previous_transition_unix_leap_time <= unix_leap_time_before && unix_leap_time_before < transition.unix_leap_time() { UtcDateTime::check_unix_time(unix_time_before)?; found_date_time_list.push(FoundDateTimeKind::Normal(new_datetime(local_time_type_before, unix_time_before))); } else { // The last transition is ignored if no extra rules are defined if index < transitions.len() - 1 || extra_rule.is_some() { let local_time_type_after = local_time_types[transition.local_time_type_index()]; let (_, unix_leap_time_after) = get_time(transition.local_time_type_index())?; // Check for a forward transition if unix_leap_time_before >= transition.unix_leap_time() && unix_leap_time_after < transition.unix_leap_time() { let transition_unix_time = time_zone_ref.unix_leap_time_to_unix_time(transition.unix_leap_time())?; found_date_time_list.push(FoundDateTimeKind::Skipped { before_transition: DateTime::from_timespec_and_local(transition_unix_time, nanoseconds, local_time_type_before)?, after_transition: DateTime::from_timespec_and_local(transition_unix_time, nanoseconds, local_time_type_after)?, }); } } } previous_transition_unix_leap_time = transition.unix_leap_time(); previous_local_time_type_index = transition.local_time_type_index(); } } // Process extra rule match extra_rule { None => {} Some(TransitionRule::Fixed(local_time_type)) => { // Overflow is not possible let unix_time = utc_unix_time - local_time_type.ut_offset() as i64; let condition = match transitions.last() { Some(last_transition) => unix_time >= time_zone_ref.unix_leap_time_to_unix_time(last_transition.unix_leap_time())?, None => true, }; if condition { UtcDateTime::check_unix_time(unix_time)?; found_date_time_list.push(FoundDateTimeKind::Normal(new_datetime(*local_time_type, unix_time))); } } Some(TransitionRule::Alternate(alternate_time)) => { let std_ut_offset = alternate_time.std().ut_offset() as i64; let dst_ut_offset = alternate_time.dst().ut_offset() as i64; // Overflow is not possible let unix_time_std = utc_unix_time - std_ut_offset; let unix_time_dst = utc_unix_time - dst_ut_offset; let dst_start_time_in_utc = alternate_time.dst_start_time() as i64 - std_ut_offset; let dst_end_time_in_utc = alternate_time.dst_end_time() as i64 - dst_ut_offset; // Check if the associated UTC date times are valid UtcDateTime::check_unix_time(unix_time_std)?; UtcDateTime::check_unix_time(unix_time_dst)?; // Check if the year is valid for the following computations if !(i32::MIN + 2..=i32::MAX - 2).contains(&year) { return Err(OutOfRangeError("out of range date time").into()); } // Check DST start/end Unix times for previous/current/next years to support for transition day times outside of [0h, 24h] range. // This is sufficient since the absolute value of DST start/end time in UTC is less than 2 weeks. // Moreover, inconsistent DST transition rules are not allowed, so there won't be additional transitions at the year boundary. let mut additional_transition_times = [ alternate_time.dst_start().unix_time(year - 1, dst_start_time_in_utc), alternate_time.dst_end().unix_time(year - 1, dst_end_time_in_utc), alternate_time.dst_start().unix_time(year, dst_start_time_in_utc), alternate_time.dst_end().unix_time(year, dst_end_time_in_utc), alternate_time.dst_start().unix_time(year + 1, dst_start_time_in_utc), alternate_time.dst_end().unix_time(year + 1, dst_end_time_in_utc), i64::MAX, ]; // Sort transitions let sorted = additional_transition_times.windows(2).all(|x| x[0] <= x[1]); if !sorted { for chunk in additional_transition_times.chunks_exact_mut(2) { chunk.swap(0, 1); } }; let transition_start = (alternate_time.std(), alternate_time.dst(), unix_time_std, unix_time_dst); let transition_end = (alternate_time.dst(), alternate_time.std(), unix_time_dst, unix_time_std); let additional_transitions = if sorted { [&transition_start, &transition_end, &transition_start, &transition_end, &transition_start, &transition_end, &transition_start] } else { [&transition_end, &transition_start, &transition_end, &transition_start, &transition_end, &transition_start, &transition_end] }; let mut previous_transition_unix_time = match transitions.last() { Some(last_transition) => time_zone_ref.unix_leap_time_to_unix_time(last_transition.unix_leap_time())?, None => i64::MIN, }; // Check transitions in order if let Some(first_valid) = additional_transition_times.iter().position(|&unix_time| previous_transition_unix_time < unix_time) { let valid_transition_times = &additional_transition_times[first_valid..]; let valid_transitions = &additional_transitions[first_valid..]; let valid_iter = valid_transition_times.iter().copied().zip(valid_transitions.iter().copied()); for (transition_unix_time, &(&local_time_type_before, &local_time_type_after, unix_time_before, unix_time_after)) in valid_iter { if previous_transition_unix_time <= unix_time_before && unix_time_before < transition_unix_time { found_date_time_list.push(FoundDateTimeKind::Normal(new_datetime(local_time_type_before, unix_time_before))); } else { // Check for a forward transition if unix_time_before >= transition_unix_time && unix_time_after < transition_unix_time { found_date_time_list.push(FoundDateTimeKind::Skipped { before_transition: DateTime::from_timespec_and_local(transition_unix_time, nanoseconds, local_time_type_before)?, after_transition: DateTime::from_timespec_and_local(transition_unix_time, nanoseconds, local_time_type_after)?, }); } } previous_transition_unix_time = transition_unix_time; } } } } Ok(()) } #[cfg(feature = "alloc")] #[cfg(test)] mod test { use super::*; use crate::datetime::test::check_equal_date_time; use crate::timezone::*; use alloc::vec; fn check_equal_option_date_time(x: &Option, y: &Option) { match (x, y) { (None, None) => (), (Some(x), Some(y)) => check_equal_date_time(x, y), _ => panic!("not equal"), } } enum Check { Normal([i32; 1]), Skipped([(i32, u8, u8, u8, u8, u8, i32); 2]), } fn check( time_zone_ref: TimeZoneRef, posssible_date_time_results: &[Check], searched: (i32, u8, u8, u8, u8, u8), result_indices: &[usize], unique: Option<[usize; 2]>, earlier: Option<[usize; 2]>, later: Option<[usize; 2]>, ) -> Result<()> { let new_date_time = |(year, month, month_day, hour, minute, second, ut_offset)| { Result::Ok(DateTime::new(year, month, month_day, hour, minute, second, 0, LocalTimeType::with_ut_offset(ut_offset)?)?) }; let (year, month, month_day, hour, minute, second) = searched; let mut found_date_times = FoundDateTimeList::default(); find_date_time(&mut found_date_times, year, month, month_day, hour, minute, second, 0, time_zone_ref)?; let mut buf = vec![None; result_indices.len()]; let mut found_date_time_list = FoundDateTimeListRefMut::new(&mut buf); find_date_time(&mut found_date_time_list, year, month, month_day, hour, minute, second, 0, time_zone_ref)?; let indexed_date_time = |[index_1, index_2]: [usize; 2]| match posssible_date_time_results[index_1] { Check::Normal(arr) => new_date_time((year, month, month_day, hour, minute, second, arr[index_2])), Check::Skipped(arr) => new_date_time(arr[index_2]), }; check_equal_option_date_time(&found_date_times.unique(), &unique.map(indexed_date_time).transpose()?); check_equal_option_date_time(&found_date_times.earliest(), &earlier.map(indexed_date_time).transpose()?); check_equal_option_date_time(&found_date_times.latest(), &later.map(indexed_date_time).transpose()?); let found_date_times_inner = found_date_times.into_inner(); assert_eq!(found_date_times_inner.len(), result_indices.len()); assert!(found_date_time_list.is_exhaustive()); assert_eq!(found_date_times_inner, buf.iter().copied().flatten().collect::>()); for (found_date_time, &result_index) in found_date_times_inner.iter().zip(result_indices) { match posssible_date_time_results[result_index] { Check::Normal([ut_offset]) => { assert_eq!(*found_date_time, FoundDateTimeKind::Normal(new_date_time((year, month, month_day, hour, minute, second, ut_offset))?)); } Check::Skipped([before, after]) => { let skipped = FoundDateTimeKind::Skipped { before_transition: new_date_time(before)?, after_transition: new_date_time(after)? }; assert_eq!(*found_date_time, skipped); } }; } Ok(()) } #[test] fn test_find_date_time_fixed() -> Result<()> { let local_time_type = LocalTimeType::with_ut_offset(3600)?; let results = &[Check::Normal([3600])]; let time_zone_1 = TimeZone::new(vec![], vec![local_time_type], vec![], None)?; let time_zone_2 = TimeZone::new(vec![], vec![local_time_type], vec![], Some(TransitionRule::Fixed(local_time_type)))?; check(time_zone_1.as_ref(), results, (2000, 1, 1, 0, 0, 0), &[0], Some([0, 0]), Some([0, 0]), Some([0, 0]))?; check(time_zone_2.as_ref(), results, (2000, 1, 1, 0, 0, 0), &[0], Some([0, 0]), Some([0, 0]), Some([0, 0]))?; let time_zone_3 = TimeZone::new(vec![Transition::new(0, 0)], vec![local_time_type], vec![], Some(TransitionRule::Fixed(local_time_type)))?; check(time_zone_3.as_ref(), results, (1960, 1, 1, 0, 0, 0), &[0], Some([0, 0]), Some([0, 0]), Some([0, 0]))?; check(time_zone_3.as_ref(), results, (1980, 1, 1, 0, 0, 0), &[0], Some([0, 0]), Some([0, 0]), Some([0, 0]))?; Ok(()) } #[test] fn test_find_date_time_no_offset() -> Result<()> { let local_time_types = [ LocalTimeType::new(0, false, Some(b"STD1"))?, LocalTimeType::new(0, true, Some(b"DST1"))?, LocalTimeType::new(0, false, Some(b"STD2"))?, LocalTimeType::new(0, true, Some(b"DST2"))?, ]; let time_zone = TimeZone::new( vec![Transition::new(3600, 1), Transition::new(7200, 2)], local_time_types.to_vec(), vec![], Some(TransitionRule::Alternate(AlternateTime::new( local_time_types[2], local_time_types[3], RuleDay::Julian0WithLeap(Julian0WithLeap::new(0)?), 10800, RuleDay::Julian0WithLeap(Julian0WithLeap::new(0)?), 14400, )?)), )?; let time_zone_ref = time_zone.as_ref(); let find_unique_local_time_type = |year, month, month_day, hour, minute, second, nanoseconds| { let mut found_date_time_list = FoundDateTimeList::default(); find_date_time(&mut found_date_time_list, year, month, month_day, hour, minute, second, nanoseconds, time_zone_ref)?; let mut buf = [None; 1]; let mut found_date_time_list_ref_mut = FoundDateTimeListRefMut::new(&mut buf); find_date_time(&mut found_date_time_list_ref_mut, year, month, month_day, hour, minute, second, 0, time_zone_ref)?; assert!(found_date_time_list_ref_mut.is_exhaustive()); let datetime_1 = found_date_time_list.unique().unwrap(); let datetime_2 = found_date_time_list_ref_mut.unique().unwrap(); assert_eq!(datetime_1, datetime_2); Result::Ok(*datetime_1.local_time_type()) }; assert_eq!(local_time_types[0], find_unique_local_time_type(1970, 1, 1, 0, 30, 0, 0)?); assert_eq!(local_time_types[1], find_unique_local_time_type(1970, 1, 1, 1, 30, 0, 0)?); assert_eq!(local_time_types[2], find_unique_local_time_type(1970, 1, 1, 2, 30, 0, 0)?); assert_eq!(local_time_types[3], find_unique_local_time_type(1970, 1, 1, 3, 30, 0, 0)?); assert_eq!(local_time_types[2], find_unique_local_time_type(1970, 1, 1, 4, 30, 0, 0)?); Ok(()) } #[test] fn test_find_date_time_extra_rule_only() -> Result<()> { let time_zone = TimeZone::new( vec![], vec![LocalTimeType::utc(), LocalTimeType::with_ut_offset(3600)?], vec![], Some(TransitionRule::Alternate(AlternateTime::new( LocalTimeType::utc(), LocalTimeType::with_ut_offset(3600)?, RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(1)?), 7200, RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(1)?), 12600, )?)), )?; let time_zone_ref = time_zone.as_ref(); let results = &[ Check::Normal([0]), Check::Normal([3600]), Check::Skipped([(2000, 1, 1, 2, 0, 0, 0), (2000, 1, 1, 3, 0, 0, 3600)]), Check::Skipped([(2010, 1, 1, 2, 0, 0, 0), (2010, 1, 1, 3, 0, 0, 3600)]), ]; check(time_zone_ref, results, (2000, 1, 1, 1, 45, 0), &[0], Some([0, 0]), Some([0, 0]), Some([0, 0]))?; check(time_zone_ref, results, (2000, 1, 1, 2, 15, 0), &[2], None, Some([2, 0]), Some([2, 1]))?; check(time_zone_ref, results, (2000, 1, 1, 2, 45, 0), &[2, 0], None, Some([2, 0]), Some([0, 0]))?; check(time_zone_ref, results, (2000, 1, 1, 3, 15, 0), &[1, 0], None, Some([1, 0]), Some([0, 0]))?; check(time_zone_ref, results, (2000, 1, 1, 3, 45, 0), &[0], Some([0, 0]), Some([0, 0]), Some([0, 0]))?; check(time_zone_ref, results, (2010, 1, 1, 1, 45, 0), &[0], Some([0, 0]), Some([0, 0]), Some([0, 0]))?; check(time_zone_ref, results, (2010, 1, 1, 2, 15, 0), &[3], None, Some([3, 0]), Some([3, 1]))?; check(time_zone_ref, results, (2010, 1, 1, 2, 45, 0), &[3, 0], None, Some([3, 0]), Some([0, 0]))?; check(time_zone_ref, results, (2010, 1, 1, 3, 15, 0), &[1, 0], None, Some([1, 0]), Some([0, 0]))?; check(time_zone_ref, results, (2010, 1, 1, 3, 45, 0), &[0], Some([0, 0]), Some([0, 0]), Some([0, 0]))?; Ok(()) } #[test] fn test_find_date_time_transitions_only() -> Result<()> { let time_zone = TimeZone::new( vec![ Transition::new(0, 0), Transition::new(7200, 1), Transition::new(14400, 2), Transition::new(25200, 3), Transition::new(28800, 4), Transition::new(32400, 0), ], vec![ LocalTimeType::new(0, false, None)?, LocalTimeType::new(3600, false, None)?, LocalTimeType::new(-10800, false, None)?, LocalTimeType::new(-19800, false, None)?, LocalTimeType::new(-16200, false, None)?, ], vec![], None, )?; let time_zone_ref = time_zone.as_ref(); let results = &[ Check::Normal([0]), Check::Normal([3600]), Check::Normal([-10800]), Check::Normal([-19800]), Check::Normal([-16200]), Check::Skipped([(1970, 1, 1, 2, 0, 0, 0), (1970, 1, 1, 3, 0, 0, 3600)]), Check::Skipped([(1970, 1, 1, 2, 30, 0, -19800), (1970, 1, 1, 3, 30, 0, -16200)]), ]; check(time_zone_ref, results, (1970, 1, 1, 0, 0, 0), &[0], Some([0, 0]), Some([0, 0]), Some([0, 0]))?; check(time_zone_ref, results, (1970, 1, 1, 1, 0, 0), &[0, 2], None, Some([0, 0]), Some([2, 0]))?; check(time_zone_ref, results, (1970, 1, 1, 1, 15, 0), &[0, 2], None, Some([0, 0]), Some([2, 0]))?; check(time_zone_ref, results, (1970, 1, 1, 1, 30, 0), &[0, 2, 3], None, Some([0, 0]), Some([3, 0]))?; check(time_zone_ref, results, (1970, 1, 1, 1, 45, 0), &[0, 2, 3], None, Some([0, 0]), Some([3, 0]))?; check(time_zone_ref, results, (1970, 1, 1, 2, 0, 0), &[5, 2, 3], None, Some([5, 0]), Some([3, 0]))?; check(time_zone_ref, results, (1970, 1, 1, 2, 15, 0), &[5, 2, 3], None, Some([5, 0]), Some([3, 0]))?; check(time_zone_ref, results, (1970, 1, 1, 2, 30, 0), &[5, 2, 6], None, Some([5, 0]), Some([6, 1]))?; check(time_zone_ref, results, (1970, 1, 1, 2, 45, 0), &[5, 2, 6], None, Some([5, 0]), Some([6, 1]))?; check(time_zone_ref, results, (1970, 1, 1, 3, 0, 0), &[1, 2, 6], None, Some([1, 0]), Some([6, 1]))?; check(time_zone_ref, results, (1970, 1, 1, 3, 15, 0), &[1, 2, 6], None, Some([1, 0]), Some([6, 1]))?; check(time_zone_ref, results, (1970, 1, 1, 3, 30, 0), &[1, 2, 4], None, Some([1, 0]), Some([4, 0]))?; check(time_zone_ref, results, (1970, 1, 1, 3, 45, 0), &[1, 2, 4], None, Some([1, 0]), Some([4, 0]))?; check(time_zone_ref, results, (1970, 1, 1, 4, 0, 0), &[1, 4], None, Some([1, 0]), Some([4, 0]))?; check(time_zone_ref, results, (1970, 1, 1, 4, 15, 0), &[1, 4], None, Some([1, 0]), Some([4, 0]))?; check(time_zone_ref, results, (1970, 1, 1, 4, 30, 0), &[1], Some([1, 0]), Some([1, 0]), Some([1, 0]))?; check(time_zone_ref, results, (1970, 1, 1, 4, 45, 0), &[1], Some([1, 0]), Some([1, 0]), Some([1, 0]))?; check(time_zone_ref, results, (1970, 1, 1, 5, 0, 0), &[], None, None, None)?; Ok(()) } #[test] fn test_find_date_time_transitions_with_extra_rule() -> Result<()> { let time_zone = TimeZone::new( vec![Transition::new(0, 0), Transition::new(3600, 1), Transition::new(7200, 0), Transition::new(10800, 2)], vec![LocalTimeType::utc(), LocalTimeType::with_ut_offset(i32::MAX)?, LocalTimeType::with_ut_offset(3600)?], vec![], Some(TransitionRule::Alternate(AlternateTime::new( LocalTimeType::utc(), LocalTimeType::with_ut_offset(3600)?, RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(300)?), 0, RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(90)?), 3600, )?)), )?; let time_zone_ref = time_zone.as_ref(); let results = &[ Check::Normal([0]), Check::Normal([3600]), Check::Normal([i32::MAX]), Check::Skipped([(1970, 1, 1, 1, 0, 0, 0), (2038, 1, 19, 4, 14, 7, i32::MAX)]), Check::Skipped([(1970, 1, 1, 3, 0, 0, 0), (1970, 1, 1, 4, 0, 0, 3600)]), Check::Skipped([(1970, 10, 27, 0, 0, 0, 0), (1970, 10, 27, 1, 0, 0, 3600)]), Check::Skipped([(2000, 10, 27, 0, 0, 0, 0), (2000, 10, 27, 1, 0, 0, 3600)]), Check::Skipped([(2030, 10, 27, 0, 0, 0, 0), (2030, 10, 27, 1, 0, 0, 3600)]), Check::Skipped([(2038, 10, 27, 0, 0, 0, 0), (2038, 10, 27, 1, 0, 0, 3600)]), ]; check(time_zone_ref, results, (1970, 1, 1, 0, 30, 0), &[0], Some([0, 0]), Some([0, 0]), Some([0, 0]))?; check(time_zone_ref, results, (1970, 1, 1, 1, 30, 0), &[3], None, Some([3, 0]), Some([3, 1]))?; check(time_zone_ref, results, (1970, 1, 1, 2, 30, 0), &[3, 0], None, Some([3, 0]), Some([0, 0]))?; check(time_zone_ref, results, (1970, 1, 1, 3, 30, 0), &[3, 4], None, Some([3, 0]), Some([4, 1]))?; check(time_zone_ref, results, (1970, 1, 1, 4, 30, 0), &[3, 1], None, Some([3, 0]), Some([1, 0]))?; check(time_zone_ref, results, (1970, 2, 1, 0, 0, 0), &[3, 1], None, Some([3, 0]), Some([1, 0]))?; check(time_zone_ref, results, (1970, 3, 31, 0, 30, 0), &[3, 1, 0], None, Some([3, 0]), Some([0, 0]))?; check(time_zone_ref, results, (1970, 6, 1, 0, 0, 0), &[3, 0], None, Some([3, 0]), Some([0, 0]))?; check(time_zone_ref, results, (1970, 10, 27, 0, 30, 0), &[3, 5], None, Some([3, 0]), Some([5, 1]))?; check(time_zone_ref, results, (1970, 11, 1, 0, 0, 0), &[3, 1], None, Some([3, 0]), Some([1, 0]))?; check(time_zone_ref, results, (2000, 2, 1, 0, 0, 0), &[3, 1], None, Some([3, 0]), Some([1, 0]))?; check(time_zone_ref, results, (2000, 3, 31, 0, 30, 0), &[3, 1, 0], None, Some([3, 0]), Some([0, 0]))?; check(time_zone_ref, results, (2000, 6, 1, 0, 0, 0), &[3, 0], None, Some([3, 0]), Some([0, 0]))?; check(time_zone_ref, results, (2000, 10, 27, 0, 30, 0), &[3, 6], None, Some([3, 0]), Some([6, 1]))?; check(time_zone_ref, results, (2000, 11, 1, 0, 0, 0), &[3, 1], None, Some([3, 0]), Some([1, 0]))?; check(time_zone_ref, results, (2030, 2, 1, 0, 0, 0), &[3, 1], None, Some([3, 0]), Some([1, 0]))?; check(time_zone_ref, results, (2030, 3, 31, 0, 30, 0), &[3, 1, 0], None, Some([3, 0]), Some([0, 0]))?; check(time_zone_ref, results, (2030, 6, 1, 0, 0, 0), &[3, 0], None, Some([3, 0]), Some([0, 0]))?; check(time_zone_ref, results, (2030, 10, 27, 0, 30, 0), &[3, 7], None, Some([3, 0]), Some([7, 1]))?; check(time_zone_ref, results, (2030, 11, 1, 0, 0, 0), &[3, 1], None, Some([3, 0]), Some([1, 0]))?; check(time_zone_ref, results, (2038, 1, 19, 5, 0, 0), &[2, 1], None, Some([2, 0]), Some([1, 0]))?; check(time_zone_ref, results, (2038, 2, 1, 0, 0, 0), &[1], Some([1, 0]), Some([1, 0]), Some([1, 0]))?; check(time_zone_ref, results, (2038, 3, 31, 0, 30, 0), &[1, 0], None, Some([1, 0]), Some([0, 0]))?; check(time_zone_ref, results, (2038, 6, 1, 0, 0, 0), &[0], Some([0, 0]), Some([0, 0]), Some([0, 0]))?; check(time_zone_ref, results, (2038, 10, 27, 0, 30, 0), &[8], None, Some([8, 0]), Some([8, 1]))?; check(time_zone_ref, results, (2038, 11, 1, 0, 0, 0), &[1], Some([1, 0]), Some([1, 0]), Some([1, 0]))?; Ok(()) } #[test] fn test_find_date_time_ref_mut() -> Result<()> { let transitions = &[Transition::new(3600, 1), Transition::new(86400, 0), Transition::new(i64::MAX, 0)]; let local_time_types = &[LocalTimeType::new(0, false, Some(b"STD"))?, LocalTimeType::new(3600, true, Some(b"DST"))?]; let time_zone_ref = TimeZoneRef::new(transitions, local_time_types, &[], &None)?; let mut small_buf = [None; 1]; let mut found_date_time_small_list = FoundDateTimeListRefMut::new(&mut small_buf); find_date_time(&mut found_date_time_small_list, 1970, 1, 2, 0, 30, 0, 0, time_zone_ref)?; assert!(!found_date_time_small_list.is_exhaustive()); let mut buf = [None; 2]; let mut found_date_time_list_1 = FoundDateTimeListRefMut::new(&mut buf); find_date_time(&mut found_date_time_list_1, 1970, 1, 2, 0, 30, 0, 0, time_zone_ref)?; let data = found_date_time_list_1.data(); assert!(found_date_time_list_1.is_exhaustive()); assert_eq!(found_date_time_list_1.count(), 2); assert!(matches!(data, [Some(FoundDateTimeKind::Normal(..)), Some(FoundDateTimeKind::Normal(..))])); let mut found_date_time_list_2 = FoundDateTimeListRefMut::new(&mut buf); find_date_time(&mut found_date_time_list_2, 1970, 1, 1, 1, 30, 0, 0, time_zone_ref)?; let data = found_date_time_list_2.data(); assert!(found_date_time_list_2.is_exhaustive()); assert_eq!(found_date_time_list_2.count(), 1); assert!(found_date_time_list_2.unique().is_none()); assert!(matches!(data, &[Some(FoundDateTimeKind::Skipped { .. })])); Ok(()) } } tz-rs-0.6.14/src/datetime/mod.rs000064400000000000000000001606441046102023000145160ustar 00000000000000//! Types related to a date time. mod find; pub use find::*; use crate::constants::*; use crate::error::*; use crate::timezone::{LocalTimeType, TimeZoneRef}; use crate::utils::*; use core::cmp::Ordering; use core::fmt; /// UTC date time exprimed in the [proleptic gregorian calendar](https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar) #[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd)] pub struct UtcDateTime { /// Year year: i32, /// Month in `[1, 12]` month: u8, /// Day of the month in `[1, 31]` month_day: u8, /// Hours since midnight in `[0, 23]` hour: u8, /// Minutes in `[0, 59]` minute: u8, /// Seconds in `[0, 60]`, with a possible leap second second: u8, /// Nanoseconds in `[0, 999_999_999]` nanoseconds: u32, } impl fmt::Display for UtcDateTime { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { format_date_time(f, self.year, self.month, self.month_day, self.hour, self.minute, self.second, self.nanoseconds, 0) } } impl UtcDateTime { /// Minimum allowed Unix time in seconds const MIN_UNIX_TIME: i64 = -67768100567971200; /// Maximum allowed Unix time in seconds const MAX_UNIX_TIME: i64 = 67767976233532799; /// Check if the UTC date time associated to a Unix time in seconds is valid #[cfg_attr(feature = "const", const_fn::const_fn)] fn check_unix_time(unix_time: i64) -> Result<(), DateTimeError> { if Self::MIN_UNIX_TIME <= unix_time && unix_time <= Self::MAX_UNIX_TIME { Ok(()) } else { Err(DateTimeError("out of range date time")) } } /// Construct a UTC date time /// /// ## Inputs /// /// * `year`: Year /// * `month`: Month in `[1, 12]` /// * `month_day`: Day of the month in `[1, 31]` /// * `hour`: Hours since midnight in `[0, 23]` /// * `minute`: Minutes in `[0, 59]` /// * `second`: Seconds in `[0, 60]`, with a possible leap second /// * `nanoseconds`: Nanoseconds in `[0, 999_999_999]` /// #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn new(year: i32, month: u8, month_day: u8, hour: u8, minute: u8, second: u8, nanoseconds: u32) -> Result { // Exclude the maximum possible UTC date time with a leap second if year == i32::MAX && month == 12 && month_day == 31 && hour == 23 && minute == 59 && second == 60 { return Err(DateTimeError("out of range date time")); } if let Err(error) = check_date_time_inputs(year, month, month_day, hour, minute, second, nanoseconds) { return Err(error); } Ok(Self { year, month, month_day, hour, minute, second, nanoseconds }) } /// Construct a UTC date time from a Unix time in seconds and nanoseconds #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn from_timespec(unix_time: i64, nanoseconds: u32) -> Result { let seconds = match unix_time.checked_sub(UNIX_OFFSET_SECS) { Some(seconds) => seconds, None => return Err(OutOfRangeError("out of range operation")), }; let mut remaining_days = seconds / SECONDS_PER_DAY; let mut remaining_seconds = seconds % SECONDS_PER_DAY; if remaining_seconds < 0 { remaining_seconds += SECONDS_PER_DAY; remaining_days -= 1; } let mut cycles_400_years = remaining_days / DAYS_PER_400_YEARS; remaining_days %= DAYS_PER_400_YEARS; if remaining_days < 0 { remaining_days += DAYS_PER_400_YEARS; cycles_400_years -= 1; } let cycles_100_years = min(remaining_days / DAYS_PER_100_YEARS, 3); remaining_days -= cycles_100_years * DAYS_PER_100_YEARS; let cycles_4_years = min(remaining_days / DAYS_PER_4_YEARS, 24); remaining_days -= cycles_4_years * DAYS_PER_4_YEARS; let remaining_years = min(remaining_days / DAYS_PER_NORMAL_YEAR, 3); remaining_days -= remaining_years * DAYS_PER_NORMAL_YEAR; let mut year = OFFSET_YEAR + remaining_years + cycles_4_years * 4 + cycles_100_years * 100 + cycles_400_years * 400; let mut month = 0; while month < DAY_IN_MONTHS_LEAP_YEAR_FROM_MARCH.len() { let days = DAY_IN_MONTHS_LEAP_YEAR_FROM_MARCH[month]; if remaining_days < days { break; } remaining_days -= days; month += 1; } month += 2; if month >= MONTHS_PER_YEAR as usize { month -= MONTHS_PER_YEAR as usize; year += 1; } month += 1; let month_day = 1 + remaining_days; let hour = remaining_seconds / SECONDS_PER_HOUR; let minute = (remaining_seconds / SECONDS_PER_MINUTE) % MINUTES_PER_HOUR; let second = remaining_seconds % SECONDS_PER_MINUTE; let year = match try_into_i32(year) { Ok(year) => year, Err(error) => return Err(error), }; Ok(Self { year, month: month as u8, month_day: month_day as u8, hour: hour as u8, minute: minute as u8, second: second as u8, nanoseconds }) } /// Construct a UTC date time from total nanoseconds since Unix epoch (`1970-01-01T00:00:00Z`) #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn from_total_nanoseconds(total_nanoseconds: i128) -> Result { match total_nanoseconds_to_timespec(total_nanoseconds) { Ok((unix_time, nanoseconds)) => Self::from_timespec(unix_time, nanoseconds), Err(error) => Err(error), } } /// Returns the current UTC date time #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] pub fn now() -> Result { use core::convert::TryInto; use std::time::SystemTime; let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?; Ok(Self::from_timespec(now.as_secs().try_into()?, now.subsec_nanos())?) } /// Returns the Unix time in seconds associated to the UTC date time #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn unix_time(&self) -> i64 { unix_time(self.year, self.month, self.month_day, self.hour, self.minute, self.second) } /// Project the UTC date time into a time zone. /// /// Leap seconds are not preserved. /// #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn project(&self, time_zone_ref: TimeZoneRef) -> Result { DateTime::from_timespec(self.unix_time(), self.nanoseconds, time_zone_ref) } } /// Date time associated to a local time type, exprimed in the [proleptic gregorian calendar](https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar) #[derive(Debug, Copy, Clone)] pub struct DateTime { /// Year year: i32, /// Month in `[1, 12]` month: u8, /// Day of the month in `[1, 31]` month_day: u8, /// Hours since midnight in `[0, 23]` hour: u8, /// Minutes in `[0, 59]` minute: u8, /// Seconds in `[0, 60]`, with a possible leap second second: u8, /// Local time type local_time_type: LocalTimeType, /// UTC Unix time in seconds unix_time: i64, /// Nanoseconds in `[0, 999_999_999]` nanoseconds: u32, } impl PartialEq for DateTime { fn eq(&self, other: &Self) -> bool { (self.unix_time, self.nanoseconds) == (other.unix_time, other.nanoseconds) } } impl PartialOrd for DateTime { fn partial_cmp(&self, other: &Self) -> Option { (self.unix_time, self.nanoseconds).partial_cmp(&(other.unix_time, other.nanoseconds)) } } impl fmt::Display for DateTime { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let ut_offset = self.local_time_type().ut_offset(); format_date_time(f, self.year, self.month, self.month_day, self.hour, self.minute, self.second, self.nanoseconds, ut_offset) } } impl DateTime { /// Construct a date time /// /// ## Inputs /// /// * `year`: Year /// * `month`: Month in `[1, 12]` /// * `month_day`: Day of the month in `[1, 31]` /// * `hour`: Hours since midnight in `[0, 23]` /// * `minute`: Minutes in `[0, 59]` /// * `second`: Seconds in `[0, 60]`, with a possible leap second /// * `nanoseconds`: Nanoseconds in `[0, 999_999_999]` /// * `local_time_type`: Local time type associated to a time zone /// #[allow(clippy::too_many_arguments)] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn new( year: i32, month: u8, month_day: u8, hour: u8, minute: u8, second: u8, nanoseconds: u32, local_time_type: LocalTimeType, ) -> Result { if let Err(error) = check_date_time_inputs(year, month, month_day, hour, minute, second, nanoseconds) { return Err(error); } // Overflow is not possible let unix_time = unix_time(year, month, month_day, hour, minute, second) - local_time_type.ut_offset() as i64; // Check if the associated UTC date time is valid if let Err(error) = UtcDateTime::check_unix_time(unix_time) { return Err(error); } Ok(Self { year, month, month_day, hour, minute, second, local_time_type, unix_time, nanoseconds }) } /// Find the possible date times corresponding to a date, a time and a time zone /// /// ## Inputs /// /// * `year`: Year /// * `month`: Month in `[1, 12]` /// * `month_day`: Day of the month in `[1, 31]` /// * `hour`: Hours since midnight in `[0, 23]` /// * `minute`: Minutes in `[0, 59]` /// * `second`: Seconds in `[0, 60]`, with a possible leap second /// * `nanoseconds`: Nanoseconds in `[0, 999_999_999]` /// * `time_zone_ref`: Reference to a time zone /// #[allow(clippy::too_many_arguments)] #[cfg(feature = "alloc")] #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] pub fn find( year: i32, month: u8, month_day: u8, hour: u8, minute: u8, second: u8, nanoseconds: u32, time_zone_ref: TimeZoneRef, ) -> Result { let mut found_date_time_list = FoundDateTimeList::default(); find_date_time(&mut found_date_time_list, year, month, month_day, hour, minute, second, nanoseconds, time_zone_ref)?; Ok(found_date_time_list) } /// Find the possible date times corresponding to a date, a time and a time zone. /// /// This method doesn't allocate, and instead takes a preallocated buffer as an input. /// It returns a [`FoundDateTimeListRefMut`] wrapper which has additional methods. /// /// ## Inputs /// /// * `buf`: Preallocated buffer /// * `year`: Year /// * `month`: Month in `[1, 12]` /// * `month_day`: Day of the month in `[1, 31]` /// * `hour`: Hours since midnight in `[0, 23]` /// * `minute`: Minutes in `[0, 59]` /// * `second`: Seconds in `[0, 60]`, with a possible leap second /// * `nanoseconds`: Nanoseconds in `[0, 999_999_999]` /// * `time_zone_ref`: Reference to a time zone /// /// ## Usage /// /// ```rust /// # fn main() -> Result<(), tz::TzError> { /// use tz::datetime::{DateTime, FoundDateTimeKind}; /// use tz::timezone::{LocalTimeType, TimeZoneRef, Transition}; /// /// let transitions = &[Transition::new(3600, 1), Transition::new(86400, 0), Transition::new(i64::MAX, 0)]; /// let local_time_types = &[LocalTimeType::new(0, false, Some(b"STD"))?, LocalTimeType::new(3600, true, Some(b"DST"))?]; /// let time_zone_ref = TimeZoneRef::new(transitions, local_time_types, &[], &None)?; /// /// // Buffer is too small, so the results are non exhaustive /// let mut small_buf = [None; 1]; /// assert!(!DateTime::find_n(&mut small_buf, 1970, 1, 2, 0, 30, 0, 0, time_zone_ref)?.is_exhaustive()); /// /// // Fill buffer /// let mut buf = [None; 2]; /// let found_date_time_list = DateTime::find_n(&mut buf, 1970, 1, 2, 0, 30, 0, 0, time_zone_ref)?; /// let data = found_date_time_list.data(); /// assert!(found_date_time_list.is_exhaustive()); /// assert_eq!(found_date_time_list.count(), 2); /// assert!(matches!(data, [Some(FoundDateTimeKind::Normal(..)), Some(FoundDateTimeKind::Normal(..))])); /// /// // We can reuse the buffer /// let found_date_time_list = DateTime::find_n(&mut buf, 1970, 1, 1, 1, 30, 0, 0, time_zone_ref)?; /// let data = found_date_time_list.data(); /// assert!(found_date_time_list.is_exhaustive()); /// assert_eq!(found_date_time_list.count(), 1); /// assert!(found_date_time_list.unique().is_none()); // FoundDateTimeKind::Skipped /// assert!(matches!(data, &[Some(FoundDateTimeKind::Skipped { .. })])); /// # Ok(()) /// # } /// ``` /// #[allow(clippy::too_many_arguments)] pub fn find_n<'a>( buf: &'a mut [Option], year: i32, month: u8, month_day: u8, hour: u8, minute: u8, second: u8, nanoseconds: u32, time_zone_ref: TimeZoneRef, ) -> Result, TzError> { let mut found_date_time_list = FoundDateTimeListRefMut::new(buf); find_date_time(&mut found_date_time_list, year, month, month_day, hour, minute, second, nanoseconds, time_zone_ref)?; Ok(found_date_time_list) } /// Construct a date time from a Unix time in seconds with nanoseconds and a local time type #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn from_timespec_and_local(unix_time: i64, nanoseconds: u32, local_time_type: LocalTimeType) -> Result { let unix_time_with_offset = match unix_time.checked_add(local_time_type.ut_offset() as i64) { Some(unix_time_with_offset) => unix_time_with_offset, None => return Err(ProjectDateTimeError("out of range date time")), }; let utc_date_time_with_offset = match UtcDateTime::from_timespec(unix_time_with_offset, nanoseconds) { Ok(utc_date_time_with_offset) => utc_date_time_with_offset, Err(OutOfRangeError(error)) => return Err(ProjectDateTimeError(error)), }; let UtcDateTime { year, month, month_day, hour, minute, second, nanoseconds } = utc_date_time_with_offset; Ok(Self { year, month, month_day, hour, minute, second, local_time_type, unix_time, nanoseconds }) } /// Construct a date time from a Unix time in seconds with nanoseconds and a time zone #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn from_timespec(unix_time: i64, nanoseconds: u32, time_zone_ref: TimeZoneRef) -> Result { let local_time_type = match time_zone_ref.find_local_time_type(unix_time) { Ok(&local_time_type) => local_time_type, Err(FindLocalTimeTypeError(error)) => return Err(ProjectDateTimeError(error)), }; Self::from_timespec_and_local(unix_time, nanoseconds, local_time_type) } /// Construct a date time from total nanoseconds since Unix epoch (`1970-01-01T00:00:00Z`) and a local time type #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn from_total_nanoseconds_and_local(total_nanoseconds: i128, local_time_type: LocalTimeType) -> Result { match total_nanoseconds_to_timespec(total_nanoseconds) { Ok((unix_time, nanoseconds)) => Self::from_timespec_and_local(unix_time, nanoseconds, local_time_type), Err(OutOfRangeError(error)) => Err(ProjectDateTimeError(error)), } } /// Construct a date time from total nanoseconds since Unix epoch (`1970-01-01T00:00:00Z`) and a time zone #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn from_total_nanoseconds(total_nanoseconds: i128, time_zone_ref: TimeZoneRef) -> Result { match total_nanoseconds_to_timespec(total_nanoseconds) { Ok((unix_time, nanoseconds)) => Self::from_timespec(unix_time, nanoseconds, time_zone_ref), Err(OutOfRangeError(error)) => Err(ProjectDateTimeError(error)), } } /// Returns the current date time associated to the specified time zone #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] pub fn now(time_zone_ref: TimeZoneRef) -> Result { use core::convert::TryInto; use std::time::SystemTime; let now = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?; Ok(Self::from_timespec(now.as_secs().try_into()?, now.subsec_nanos(), time_zone_ref)?) } /// Project the date time into another time zone. /// /// Leap seconds are not preserved. /// #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn project(&self, time_zone_ref: TimeZoneRef) -> Result { Self::from_timespec(self.unix_time, self.nanoseconds, time_zone_ref) } } /// Macro for implementing date time getters macro_rules! impl_datetime { () => { /// Returns year #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn year(&self) -> i32 { self.year } /// Returns month in `[1, 12]` #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn month(&self) -> u8 { self.month } /// Returns day of the month in `[1, 31]` #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn month_day(&self) -> u8 { self.month_day } /// Returns hours since midnight in `[0, 23]` #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn hour(&self) -> u8 { self.hour } /// Returns minutes in `[0, 59]` #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn minute(&self) -> u8 { self.minute } /// Returns seconds in `[0, 60]`, with a possible leap second #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn second(&self) -> u8 { self.second } /// Returns nanoseconds in `[0, 999_999_999]` #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn nanoseconds(&self) -> u32 { self.nanoseconds } /// Returns days since Sunday in `[0, 6]` #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn week_day(&self) -> u8 { week_day(self.year, self.month as usize, self.month_day as i64) } /// Returns days since January 1 in `[0, 365]` #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn year_day(&self) -> u16 { year_day(self.year, self.month as usize, self.month_day as i64) } /// Returns total nanoseconds since Unix epoch (`1970-01-01T00:00:00Z`) #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn total_nanoseconds(&self) -> i128 { nanoseconds_since_unix_epoch(self.unix_time(), self.nanoseconds) } }; } impl UtcDateTime { impl_datetime!(); } impl DateTime { impl_datetime!(); /// Returns local time type #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn local_time_type(&self) -> &LocalTimeType { &self.local_time_type } /// Returns UTC Unix time in seconds #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn unix_time(&self) -> i64 { self.unix_time } } /// Compute the number of days since Sunday in `[0, 6]` /// /// ## Inputs /// /// * `year`: Year /// * `month`: Month in `[1, 12]` /// * `month_day`: Day of the month in `[1, 31]` /// #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] fn week_day(year: i32, month: usize, month_day: i64) -> u8 { let days_since_unix_epoch = days_since_unix_epoch(year, month, month_day); (4 + days_since_unix_epoch).rem_euclid(DAYS_PER_WEEK) as u8 } /// Compute the number of days since January 1 in `[0, 365]` /// /// ## Inputs /// /// * `year`: Year /// * `month`: Month in `[1, 12]` /// * `month_day`: Day of the month in `[1, 31]` /// #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] fn year_day(year: i32, month: usize, month_day: i64) -> u16 { let leap = (month >= 3 && is_leap_year(year)) as i64; (CUMUL_DAYS_IN_MONTHS_NORMAL_YEAR[month - 1] + leap + month_day - 1) as u16 } /// Check if a year is a leap year #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub(crate) fn is_leap_year(year: i32) -> bool { year % 400 == 0 || (year % 4 == 0 && year % 100 != 0) } /// Compute the number of days since Unix epoch (`1970-01-01T00:00:00Z`). /// /// The December 32nd date is possible, which corresponds to January 1st of the next year. /// /// ## Inputs /// /// * `year`: Year /// * `month`: Month in `[1, 12]` /// * `month_day`: Day of the month in `[1, 32]` /// #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub(crate) fn days_since_unix_epoch(year: i32, month: usize, month_day: i64) -> i64 { let is_leap_year = is_leap_year(year); let year = year as i64; let mut result = (year - 1970) * 365; if year >= 1970 { result += (year - 1968) / 4; result -= (year - 1900) / 100; result += (year - 1600) / 400; if is_leap_year && month < 3 { result -= 1; } } else { result += (year - 1972) / 4; result -= (year - 2000) / 100; result += (year - 2000) / 400; if is_leap_year && month >= 3 { result += 1; } } result += CUMUL_DAYS_IN_MONTHS_NORMAL_YEAR[month - 1] + month_day - 1; result } /// Compute Unix time in seconds /// /// ## Inputs /// /// * `year`: Year /// * `month`: Month in `[1, 12]` /// * `month_day`: Day of the month in `[1, 31]` /// * `hour`: Hours since midnight in `[0, 23]` /// * `minute`: Minutes in `[0, 59]` /// * `second`: Seconds in `[0, 60]`, with a possible leap second /// #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] fn unix_time(year: i32, month: u8, month_day: u8, hour: u8, minute: u8, second: u8) -> i64 { let mut result = days_since_unix_epoch(year, month as usize, month_day as i64); result *= HOURS_PER_DAY; result += hour as i64; result *= MINUTES_PER_HOUR; result += minute as i64; result *= SECONDS_PER_MINUTE; result += second as i64; result } /// Compute the number of nanoseconds since Unix epoch (`1970-01-01T00:00:00Z`) #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] fn nanoseconds_since_unix_epoch(unix_time: i64, nanoseconds: u32) -> i128 { // Overflow is not possible unix_time as i128 * NANOSECONDS_PER_SECOND as i128 + nanoseconds as i128 } /// Compute Unix time in seconds with nanoseconds from total nanoseconds since Unix epoch (`1970-01-01T00:00:00Z`) /// /// ## Outputs /// /// * `unix_time`: Unix time in seconds /// * `nanoseconds`: Nanoseconds in `[0, 999_999_999]` /// #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] fn total_nanoseconds_to_timespec(total_nanoseconds: i128) -> Result<(i64, u32), OutOfRangeError> { let unix_time = match try_into_i64(total_nanoseconds.div_euclid(NANOSECONDS_PER_SECOND as i128)) { Ok(unix_time) => unix_time, Err(error) => return Err(error), }; let nanoseconds = total_nanoseconds.rem_euclid(NANOSECONDS_PER_SECOND as i128) as u32; Ok((unix_time, nanoseconds)) } /// Check date time inputs /// /// ## Inputs /// /// * `year`: Year /// * `month`: Month in `[1, 12]` /// * `month_day`: Day of the month in `[1, 31]` /// * `hour`: Hours since midnight in `[0, 23]` /// * `minute`: Minutes in `[0, 59]` /// * `second`: Seconds in `[0, 60]`, with a possible leap second /// * `nanoseconds`: Nanoseconds in `[0, 999_999_999]` /// #[cfg_attr(feature = "const", const_fn::const_fn)] fn check_date_time_inputs(year: i32, month: u8, month_day: u8, hour: u8, minute: u8, second: u8, nanoseconds: u32) -> Result<(), DateTimeError> { if !(1 <= month && month <= 12) { return Err(DateTimeError("invalid month")); } if !(1 <= month_day && month_day <= 31) { return Err(DateTimeError("invalid month day")); } if hour > 23 { return Err(DateTimeError("invalid hour")); } if minute > 59 { return Err(DateTimeError("invalid minute")); } if second > 60 { return Err(DateTimeError("invalid second")); } if nanoseconds >= NANOSECONDS_PER_SECOND { return Err(DateTimeError("invalid nanoseconds")); } let leap = is_leap_year(year) as i64; let mut days_in_month = DAYS_IN_MONTHS_NORMAL_YEAR[month as usize - 1]; if month == 2 { days_in_month += leap; } if month_day as i64 > days_in_month { return Err(DateTimeError("invalid month day")); } Ok(()) } /// Format a date time /// /// ## Inputs /// /// * `f`: Formatter /// * `year`: Year /// * `month`: Month in `[1, 12]` /// * `month_day`: Day of the month in `[1, 31]` /// * `hour`: Hours since midnight in `[0, 23]` /// * `minute`: Minutes in `[0, 59]` /// * `second`: Seconds in `[0, 60]`, with a possible leap second /// * `nanoseconds`: Nanoseconds in `[0, 999_999_999]` /// * `ut_offset`: Offset from UTC in seconds /// #[allow(clippy::too_many_arguments)] fn format_date_time( f: &mut fmt::Formatter, year: i32, month: u8, month_day: u8, hour: u8, minute: u8, second: u8, nanoseconds: u32, ut_offset: i32, ) -> fmt::Result { write!(f, "{}-{:02}-{:02}T{:02}:{:02}:{:02}.{:09}", year, month, month_day, hour, minute, second, nanoseconds)?; if ut_offset != 0 { let ut_offset = ut_offset as i64; let ut_offset_abs = ut_offset.abs(); let sign = if ut_offset < 0 { '-' } else { '+' }; let offset_hour = ut_offset_abs / SECONDS_PER_HOUR; let offset_minute = (ut_offset_abs / SECONDS_PER_MINUTE) % MINUTES_PER_HOUR; let offset_second = ut_offset_abs % SECONDS_PER_MINUTE; write!(f, "{}{:02}:{:02}", sign, offset_hour, offset_minute)?; if offset_second != 0 { write!(f, ":{:02}", offset_second)?; } } else { write!(f, "Z")?; } Ok(()) } #[cfg(test)] mod test { use super::*; use crate::Result; #[cfg(feature = "alloc")] use crate::timezone::TimeZone; #[cfg(feature = "alloc")] pub(super) fn check_equal_date_time(x: &DateTime, y: &DateTime) { assert_eq!(x.year(), y.year()); assert_eq!(x.month(), y.month()); assert_eq!(x.month_day(), y.month_day()); assert_eq!(x.hour(), y.hour()); assert_eq!(x.minute(), y.minute()); assert_eq!(x.second(), y.second()); assert_eq!(x.local_time_type(), y.local_time_type()); assert_eq!(x.unix_time(), y.unix_time()); assert_eq!(x.nanoseconds(), y.nanoseconds()); } #[cfg(feature = "alloc")] #[test] fn test_date_time() -> Result<()> { let time_zone_utc = TimeZone::utc(); let utc = LocalTimeType::utc(); let time_zone_cet = TimeZone::fixed(3600)?; let cet = LocalTimeType::with_ut_offset(3600)?; let time_zone_eet = TimeZone::fixed(7200)?; let eet = LocalTimeType::with_ut_offset(7200)?; #[cfg(feature = "std")] { assert_eq!(DateTime::now(time_zone_utc.as_ref())?.local_time_type().ut_offset(), 0); assert_eq!(DateTime::now(time_zone_cet.as_ref())?.local_time_type().ut_offset(), 3600); assert_eq!(DateTime::now(time_zone_eet.as_ref())?.local_time_type().ut_offset(), 7200); } let unix_times = &[ -93750523134, -11670955134, -11670868734, -8515195134, -8483659134, -8389051134, -8388964734, 951825666, 951912066, 983448066, 1078056066, 1078142466, 4107585666, 32540356866, ]; let nanoseconds_list = &[10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]; #[rustfmt::skip] let date_times_utc = &[ DateTime { year: -1001, month: 3, month_day: 1, hour: 12, minute: 1, second: 6, local_time_type: utc, unix_time: -93750523134, nanoseconds: 10 }, DateTime { year: 1600, month: 2, month_day: 29, hour: 12, minute: 1, second: 6, local_time_type: utc, unix_time: -11670955134, nanoseconds: 11 }, DateTime { year: 1600, month: 3, month_day: 1, hour: 12, minute: 1, second: 6, local_time_type: utc, unix_time: -11670868734, nanoseconds: 12 }, DateTime { year: 1700, month: 3, month_day: 1, hour: 12, minute: 1, second: 6, local_time_type: utc, unix_time: -8515195134, nanoseconds: 13 }, DateTime { year: 1701, month: 3, month_day: 1, hour: 12, minute: 1, second: 6, local_time_type: utc, unix_time: -8483659134, nanoseconds: 14 }, DateTime { year: 1704, month: 2, month_day: 29, hour: 12, minute: 1, second: 6, local_time_type: utc, unix_time: -8389051134, nanoseconds: 15 }, DateTime { year: 1704, month: 3, month_day: 1, hour: 12, minute: 1, second: 6, local_time_type: utc, unix_time: -8388964734, nanoseconds: 16 }, DateTime { year: 2000, month: 2, month_day: 29, hour: 12, minute: 1, second: 6, local_time_type: utc, unix_time: 951825666, nanoseconds: 17 }, DateTime { year: 2000, month: 3, month_day: 1, hour: 12, minute: 1, second: 6, local_time_type: utc, unix_time: 951912066, nanoseconds: 18 }, DateTime { year: 2001, month: 3, month_day: 1, hour: 12, minute: 1, second: 6, local_time_type: utc, unix_time: 983448066, nanoseconds: 19 }, DateTime { year: 2004, month: 2, month_day: 29, hour: 12, minute: 1, second: 6, local_time_type: utc, unix_time: 1078056066, nanoseconds: 20 }, DateTime { year: 2004, month: 3, month_day: 1, hour: 12, minute: 1, second: 6, local_time_type: utc, unix_time: 1078142466, nanoseconds: 21 }, DateTime { year: 2100, month: 3, month_day: 1, hour: 12, minute: 1, second: 6, local_time_type: utc, unix_time: 4107585666, nanoseconds: 22 }, DateTime { year: 3001, month: 3, month_day: 1, hour: 12, minute: 1, second: 6, local_time_type: utc, unix_time: 32540356866, nanoseconds: 23 }, ]; #[rustfmt::skip] let date_times_cet = &[ DateTime { year: -1001, month: 3, month_day: 1, hour: 13, minute: 1, second: 6, local_time_type: cet, unix_time: -93750523134, nanoseconds: 10 }, DateTime { year: 1600, month: 2, month_day: 29, hour: 13, minute: 1, second: 6, local_time_type: cet, unix_time: -11670955134, nanoseconds: 11 }, DateTime { year: 1600, month: 3, month_day: 1, hour: 13, minute: 1, second: 6, local_time_type: cet, unix_time: -11670868734, nanoseconds: 12 }, DateTime { year: 1700, month: 3, month_day: 1, hour: 13, minute: 1, second: 6, local_time_type: cet, unix_time: -8515195134, nanoseconds: 13 }, DateTime { year: 1701, month: 3, month_day: 1, hour: 13, minute: 1, second: 6, local_time_type: cet, unix_time: -8483659134, nanoseconds: 14 }, DateTime { year: 1704, month: 2, month_day: 29, hour: 13, minute: 1, second: 6, local_time_type: cet, unix_time: -8389051134, nanoseconds: 15 }, DateTime { year: 1704, month: 3, month_day: 1, hour: 13, minute: 1, second: 6, local_time_type: cet, unix_time: -8388964734, nanoseconds: 16 }, DateTime { year: 2000, month: 2, month_day: 29, hour: 13, minute: 1, second: 6, local_time_type: cet, unix_time: 951825666, nanoseconds: 17 }, DateTime { year: 2000, month: 3, month_day: 1, hour: 13, minute: 1, second: 6, local_time_type: cet, unix_time: 951912066, nanoseconds: 18 }, DateTime { year: 2001, month: 3, month_day: 1, hour: 13, minute: 1, second: 6, local_time_type: cet, unix_time: 983448066, nanoseconds: 19 }, DateTime { year: 2004, month: 2, month_day: 29, hour: 13, minute: 1, second: 6, local_time_type: cet, unix_time: 1078056066, nanoseconds: 20 }, DateTime { year: 2004, month: 3, month_day: 1, hour: 13, minute: 1, second: 6, local_time_type: cet, unix_time: 1078142466, nanoseconds: 21 }, DateTime { year: 2100, month: 3, month_day: 1, hour: 13, minute: 1, second: 6, local_time_type: cet, unix_time: 4107585666, nanoseconds: 22 }, DateTime { year: 3001, month: 3, month_day: 1, hour: 13, minute: 1, second: 6, local_time_type: cet, unix_time: 32540356866, nanoseconds: 23 }, ]; #[rustfmt::skip] let date_times_eet = &[ DateTime { year: -1001, month: 3, month_day: 1, hour: 14, minute: 1, second: 6, local_time_type: eet, unix_time: -93750523134, nanoseconds: 10 }, DateTime { year: 1600, month: 2, month_day: 29, hour: 14, minute: 1, second: 6, local_time_type: eet, unix_time: -11670955134, nanoseconds: 11 }, DateTime { year: 1600, month: 3, month_day: 1, hour: 14, minute: 1, second: 6, local_time_type: eet, unix_time: -11670868734, nanoseconds: 12 }, DateTime { year: 1700, month: 3, month_day: 1, hour: 14, minute: 1, second: 6, local_time_type: eet, unix_time: -8515195134, nanoseconds: 13 }, DateTime { year: 1701, month: 3, month_day: 1, hour: 14, minute: 1, second: 6, local_time_type: eet, unix_time: -8483659134, nanoseconds: 14 }, DateTime { year: 1704, month: 2, month_day: 29, hour: 14, minute: 1, second: 6, local_time_type: eet, unix_time: -8389051134, nanoseconds: 15 }, DateTime { year: 1704, month: 3, month_day: 1, hour: 14, minute: 1, second: 6, local_time_type: eet, unix_time: -8388964734, nanoseconds: 16 }, DateTime { year: 2000, month: 2, month_day: 29, hour: 14, minute: 1, second: 6, local_time_type: eet, unix_time: 951825666, nanoseconds: 17 }, DateTime { year: 2000, month: 3, month_day: 1, hour: 14, minute: 1, second: 6, local_time_type: eet, unix_time: 951912066, nanoseconds: 18 }, DateTime { year: 2001, month: 3, month_day: 1, hour: 14, minute: 1, second: 6, local_time_type: eet, unix_time: 983448066, nanoseconds: 19 }, DateTime { year: 2004, month: 2, month_day: 29, hour: 14, minute: 1, second: 6, local_time_type: eet, unix_time: 1078056066, nanoseconds: 20 }, DateTime { year: 2004, month: 3, month_day: 1, hour: 14, minute: 1, second: 6, local_time_type: eet, unix_time: 1078142466, nanoseconds: 21 }, DateTime { year: 2100, month: 3, month_day: 1, hour: 14, minute: 1, second: 6, local_time_type: eet, unix_time: 4107585666, nanoseconds: 22 }, DateTime { year: 3001, month: 3, month_day: 1, hour: 14, minute: 1, second: 6, local_time_type: eet, unix_time: 32540356866, nanoseconds: 23 }, ]; for ((((&unix_time, &nanoseconds), date_time_utc), date_time_cet), date_time_eet) in unix_times.iter().zip(nanoseconds_list).zip(date_times_utc).zip(date_times_cet).zip(date_times_eet) { let utc_date_time = UtcDateTime::from_timespec(unix_time, nanoseconds)?; assert_eq!(UtcDateTime::from_timespec(utc_date_time.unix_time(), nanoseconds)?, utc_date_time); assert_eq!(utc_date_time.year(), date_time_utc.year()); assert_eq!(utc_date_time.month(), date_time_utc.month()); assert_eq!(utc_date_time.month_day(), date_time_utc.month_day()); assert_eq!(utc_date_time.hour(), date_time_utc.hour()); assert_eq!(utc_date_time.minute(), date_time_utc.minute()); assert_eq!(utc_date_time.second(), date_time_utc.second()); assert_eq!(utc_date_time.nanoseconds(), date_time_utc.nanoseconds()); assert_eq!(utc_date_time.unix_time(), unix_time); assert_eq!(date_time_utc.unix_time(), unix_time); assert_eq!(date_time_cet.unix_time(), unix_time); assert_eq!(date_time_eet.unix_time(), unix_time); assert_eq!(date_time_utc, date_time_cet); assert_eq!(date_time_utc, date_time_eet); check_equal_date_time(&utc_date_time.project(time_zone_utc.as_ref())?, date_time_utc); check_equal_date_time(&utc_date_time.project(time_zone_cet.as_ref())?, date_time_cet); check_equal_date_time(&utc_date_time.project(time_zone_eet.as_ref())?, date_time_eet); check_equal_date_time(&date_time_utc.project(time_zone_utc.as_ref())?, date_time_utc); check_equal_date_time(&date_time_cet.project(time_zone_utc.as_ref())?, date_time_utc); check_equal_date_time(&date_time_eet.project(time_zone_utc.as_ref())?, date_time_utc); check_equal_date_time(&date_time_utc.project(time_zone_cet.as_ref())?, date_time_cet); check_equal_date_time(&date_time_cet.project(time_zone_cet.as_ref())?, date_time_cet); check_equal_date_time(&date_time_eet.project(time_zone_cet.as_ref())?, date_time_cet); check_equal_date_time(&date_time_utc.project(time_zone_eet.as_ref())?, date_time_eet); check_equal_date_time(&date_time_cet.project(time_zone_eet.as_ref())?, date_time_eet); check_equal_date_time(&date_time_eet.project(time_zone_eet.as_ref())?, date_time_eet); } Ok(()) } #[cfg(feature = "alloc")] #[test] fn test_date_time_leap_seconds() -> Result<()> { let utc_date_time = UtcDateTime::new(1972, 6, 30, 23, 59, 60, 1000)?; assert_eq!(UtcDateTime::from_timespec(utc_date_time.unix_time(), 1000)?, UtcDateTime::new(1972, 7, 1, 0, 0, 0, 1000)?); let date_time = utc_date_time.project(TimeZone::fixed(-3600)?.as_ref())?; let date_time_result = DateTime { year: 1972, month: 6, month_day: 30, hour: 23, minute: 00, second: 00, local_time_type: LocalTimeType::with_ut_offset(-3600)?, unix_time: 78796800, nanoseconds: 1000, }; check_equal_date_time(&date_time, &date_time_result); Ok(()) } #[cfg(feature = "alloc")] #[test] fn test_date_time_partial_eq_partial_ord() -> Result<()> { let time_zone_utc = TimeZone::utc(); let time_zone_cet = TimeZone::fixed(3600)?; let time_zone_eet = TimeZone::fixed(7200)?; let utc_date_time_1 = UtcDateTime::from_timespec(1, 1)?; let utc_date_time_2 = UtcDateTime::from_timespec(2, 1)?; let utc_date_time_3 = UtcDateTime::from_timespec(3, 1)?; let utc_date_time_4 = UtcDateTime::from_timespec(3, 1000)?; let date_time_utc_1 = utc_date_time_1.project(time_zone_utc.as_ref())?; let date_time_utc_2 = utc_date_time_2.project(time_zone_utc.as_ref())?; let date_time_utc_3 = utc_date_time_3.project(time_zone_utc.as_ref())?; let date_time_utc_4 = utc_date_time_4.project(time_zone_utc.as_ref())?; let date_time_cet_1 = utc_date_time_1.project(time_zone_cet.as_ref())?; let date_time_cet_2 = utc_date_time_2.project(time_zone_cet.as_ref())?; let date_time_cet_3 = utc_date_time_3.project(time_zone_cet.as_ref())?; let date_time_cet_4 = utc_date_time_4.project(time_zone_cet.as_ref())?; let date_time_eet_1 = utc_date_time_1.project(time_zone_eet.as_ref())?; let date_time_eet_2 = utc_date_time_2.project(time_zone_eet.as_ref())?; let date_time_eet_3 = utc_date_time_3.project(time_zone_eet.as_ref())?; let date_time_eet_4 = utc_date_time_4.project(time_zone_eet.as_ref())?; assert_eq!(date_time_utc_1, date_time_cet_1); assert_eq!(date_time_utc_1, date_time_eet_1); assert_eq!(date_time_utc_2, date_time_cet_2); assert_eq!(date_time_utc_2, date_time_eet_2); assert_eq!(date_time_utc_3, date_time_cet_3); assert_eq!(date_time_utc_3, date_time_eet_3); assert_eq!(date_time_utc_4, date_time_cet_4); assert_eq!(date_time_utc_4, date_time_eet_4); assert_ne!(date_time_utc_1, date_time_utc_2); assert_ne!(date_time_utc_1, date_time_utc_3); assert_ne!(date_time_utc_1, date_time_utc_4); assert_eq!(date_time_utc_1.partial_cmp(&date_time_cet_1), Some(Ordering::Equal)); assert_eq!(date_time_utc_1.partial_cmp(&date_time_eet_1), Some(Ordering::Equal)); assert_eq!(date_time_utc_2.partial_cmp(&date_time_cet_2), Some(Ordering::Equal)); assert_eq!(date_time_utc_2.partial_cmp(&date_time_eet_2), Some(Ordering::Equal)); assert_eq!(date_time_utc_3.partial_cmp(&date_time_cet_3), Some(Ordering::Equal)); assert_eq!(date_time_utc_3.partial_cmp(&date_time_eet_3), Some(Ordering::Equal)); assert_eq!(date_time_utc_4.partial_cmp(&date_time_cet_4), Some(Ordering::Equal)); assert_eq!(date_time_utc_4.partial_cmp(&date_time_eet_4), Some(Ordering::Equal)); assert_eq!(date_time_utc_1.partial_cmp(&date_time_utc_2), Some(Ordering::Less)); assert_eq!(date_time_utc_2.partial_cmp(&date_time_utc_3), Some(Ordering::Less)); assert_eq!(date_time_utc_3.partial_cmp(&date_time_utc_4), Some(Ordering::Less)); Ok(()) } #[test] fn test_date_time_sync_and_send() { trait AssertSyncSendStatic: Sync + Send + 'static {} impl AssertSyncSendStatic for DateTime {} } #[test] fn test_utc_date_time_ord() -> Result<()> { let utc_date_time_1 = UtcDateTime::new(1972, 6, 30, 23, 59, 59, 1000)?; let utc_date_time_2 = UtcDateTime::new(1972, 6, 30, 23, 59, 60, 1000)?; let utc_date_time_3 = UtcDateTime::new(1972, 7, 1, 0, 0, 0, 1000)?; let utc_date_time_4 = UtcDateTime::new(1972, 7, 1, 0, 0, 0, 1001)?; assert_eq!(utc_date_time_1.cmp(&utc_date_time_1), Ordering::Equal); assert_eq!(utc_date_time_1.cmp(&utc_date_time_2), Ordering::Less); assert_eq!(utc_date_time_1.cmp(&utc_date_time_3), Ordering::Less); assert_eq!(utc_date_time_1.cmp(&utc_date_time_4), Ordering::Less); assert_eq!(utc_date_time_2.cmp(&utc_date_time_1), Ordering::Greater); assert_eq!(utc_date_time_2.cmp(&utc_date_time_2), Ordering::Equal); assert_eq!(utc_date_time_2.cmp(&utc_date_time_3), Ordering::Less); assert_eq!(utc_date_time_2.cmp(&utc_date_time_4), Ordering::Less); assert_eq!(utc_date_time_3.cmp(&utc_date_time_1), Ordering::Greater); assert_eq!(utc_date_time_3.cmp(&utc_date_time_2), Ordering::Greater); assert_eq!(utc_date_time_3.cmp(&utc_date_time_3), Ordering::Equal); assert_eq!(utc_date_time_3.cmp(&utc_date_time_4), Ordering::Less); assert_eq!(utc_date_time_4.cmp(&utc_date_time_1), Ordering::Greater); assert_eq!(utc_date_time_4.cmp(&utc_date_time_2), Ordering::Greater); assert_eq!(utc_date_time_4.cmp(&utc_date_time_3), Ordering::Greater); assert_eq!(utc_date_time_4.cmp(&utc_date_time_4), Ordering::Equal); assert_eq!(utc_date_time_1.cmp(&utc_date_time_1), utc_date_time_1.unix_time().cmp(&utc_date_time_1.unix_time())); assert_eq!(utc_date_time_1.cmp(&utc_date_time_2), utc_date_time_1.unix_time().cmp(&utc_date_time_2.unix_time())); assert_eq!(utc_date_time_1.cmp(&utc_date_time_3), utc_date_time_1.unix_time().cmp(&utc_date_time_3.unix_time())); assert_eq!(utc_date_time_1.cmp(&utc_date_time_4), utc_date_time_1.unix_time().cmp(&utc_date_time_4.unix_time())); assert_eq!(utc_date_time_2.cmp(&utc_date_time_1), utc_date_time_2.unix_time().cmp(&utc_date_time_1.unix_time())); assert_eq!(utc_date_time_2.cmp(&utc_date_time_2), utc_date_time_2.unix_time().cmp(&utc_date_time_2.unix_time())); assert_eq!(utc_date_time_3.cmp(&utc_date_time_1), utc_date_time_3.unix_time().cmp(&utc_date_time_1.unix_time())); assert_eq!(utc_date_time_3.cmp(&utc_date_time_3), utc_date_time_3.unix_time().cmp(&utc_date_time_3.unix_time())); assert_eq!(utc_date_time_4.cmp(&utc_date_time_1), utc_date_time_4.unix_time().cmp(&utc_date_time_1.unix_time())); assert_eq!(utc_date_time_4.cmp(&utc_date_time_4), utc_date_time_4.unix_time().cmp(&utc_date_time_4.unix_time())); Ok(()) } #[cfg(feature = "alloc")] #[test] fn test_date_time_format() -> Result<()> { use alloc::string::ToString; let time_zones = [ TimeZone::fixed(-49550)?, TimeZone::fixed(-5400)?, TimeZone::fixed(-3600)?, TimeZone::fixed(-1800)?, TimeZone::fixed(0)?, TimeZone::fixed(1800)?, TimeZone::fixed(3600)?, TimeZone::fixed(5400)?, TimeZone::fixed(49550)?, ]; let utc_date_times = &[UtcDateTime::new(2000, 1, 2, 3, 4, 5, 0)?, UtcDateTime::new(2000, 1, 2, 3, 4, 5, 123_456_789)?]; let utc_date_time_strings = &["2000-01-02T03:04:05.000000000Z", "2000-01-02T03:04:05.123456789Z"]; let date_time_strings_list = &[ &[ "2000-01-01T13:18:15.000000000-13:45:50", "2000-01-02T01:34:05.000000000-01:30", "2000-01-02T02:04:05.000000000-01:00", "2000-01-02T02:34:05.000000000-00:30", "2000-01-02T03:04:05.000000000Z", "2000-01-02T03:34:05.000000000+00:30", "2000-01-02T04:04:05.000000000+01:00", "2000-01-02T04:34:05.000000000+01:30", "2000-01-02T16:49:55.000000000+13:45:50", ], &[ "2000-01-01T13:18:15.123456789-13:45:50", "2000-01-02T01:34:05.123456789-01:30", "2000-01-02T02:04:05.123456789-01:00", "2000-01-02T02:34:05.123456789-00:30", "2000-01-02T03:04:05.123456789Z", "2000-01-02T03:34:05.123456789+00:30", "2000-01-02T04:04:05.123456789+01:00", "2000-01-02T04:34:05.123456789+01:30", "2000-01-02T16:49:55.123456789+13:45:50", ], ]; for ((utc_date_time, &utc_date_time_string), &date_time_strings) in utc_date_times.iter().zip(utc_date_time_strings).zip(date_time_strings_list) { for (time_zone, &date_time_string) in time_zones.iter().zip(date_time_strings) { assert_eq!(utc_date_time.to_string(), utc_date_time_string); assert_eq!(utc_date_time.project(time_zone.as_ref())?.to_string(), date_time_string); } } Ok(()) } #[cfg(feature = "alloc")] #[test] fn test_date_time_overflow() -> Result<()> { assert!(UtcDateTime::new(i32::MIN, 1, 1, 0, 0, 0, 0).is_ok()); assert!(UtcDateTime::new(i32::MAX, 12, 31, 23, 59, 59, 0).is_ok()); assert!(DateTime::new(i32::MIN, 1, 1, 0, 0, 0, 0, LocalTimeType::utc()).is_ok()); assert!(DateTime::new(i32::MAX, 12, 31, 23, 59, 59, 0, LocalTimeType::utc()).is_ok()); assert!(matches!(DateTime::new(i32::MIN, 1, 1, 0, 0, 0, 0, LocalTimeType::with_ut_offset(1)?), Err(DateTimeError(_)))); assert!(matches!(DateTime::new(i32::MAX, 12, 31, 23, 59, 59, 0, LocalTimeType::with_ut_offset(-1)?), Err(DateTimeError(_)))); assert!(matches!(UtcDateTime::new(i32::MAX, 12, 31, 23, 59, 60, 0), Err(DateTimeError(_)))); assert!(matches!(DateTime::new(i32::MAX, 12, 31, 23, 59, 60, 0, LocalTimeType::utc()), Err(DateTimeError(_)))); assert!(DateTime::new(i32::MAX, 12, 31, 23, 59, 60, 0, LocalTimeType::with_ut_offset(1)?).is_ok()); assert!(UtcDateTime::from_timespec(UtcDateTime::MIN_UNIX_TIME, 0).is_ok()); assert!(UtcDateTime::from_timespec(UtcDateTime::MAX_UNIX_TIME, 0).is_ok()); assert!(matches!(UtcDateTime::from_timespec(UtcDateTime::MIN_UNIX_TIME - 1, 0), Err(OutOfRangeError(_)))); assert!(matches!(UtcDateTime::from_timespec(UtcDateTime::MAX_UNIX_TIME + 1, 0), Err(OutOfRangeError(_)))); assert!(matches!(UtcDateTime::from_timespec(UtcDateTime::MIN_UNIX_TIME, 0)?.project(TimeZone::fixed(-1)?.as_ref()), Err(ProjectDateTimeError(_)))); assert!(matches!(UtcDateTime::from_timespec(UtcDateTime::MAX_UNIX_TIME, 0)?.project(TimeZone::fixed(1)?.as_ref()), Err(ProjectDateTimeError(_)))); assert!(matches!(UtcDateTime::from_timespec(i64::MIN, 0), Err(OutOfRangeError(_)))); assert!(matches!(UtcDateTime::from_timespec(i64::MAX, 0), Err(OutOfRangeError(_)))); assert!(DateTime::from_timespec(UtcDateTime::MIN_UNIX_TIME, 0, TimeZone::fixed(0)?.as_ref()).is_ok()); assert!(DateTime::from_timespec(UtcDateTime::MAX_UNIX_TIME, 0, TimeZone::fixed(0)?.as_ref()).is_ok()); assert!(matches!(DateTime::from_timespec(i64::MIN, 0, TimeZone::fixed(-1)?.as_ref()), Err(ProjectDateTimeError(_)))); assert!(matches!(DateTime::from_timespec(i64::MAX, 0, TimeZone::fixed(1)?.as_ref()), Err(ProjectDateTimeError(_)))); Ok(()) } #[test] fn test_week_day() { assert_eq!(week_day(1970, 1, 1), 4); assert_eq!(week_day(2000, 1, 1), 6); assert_eq!(week_day(2000, 2, 28), 1); assert_eq!(week_day(2000, 2, 29), 2); assert_eq!(week_day(2000, 3, 1), 3); assert_eq!(week_day(2000, 12, 31), 0); assert_eq!(week_day(2001, 1, 1), 1); assert_eq!(week_day(2001, 2, 28), 3); assert_eq!(week_day(2001, 3, 1), 4); assert_eq!(week_day(2001, 12, 31), 1); } #[test] fn test_year_day() { assert_eq!(year_day(2000, 1, 1), 0); assert_eq!(year_day(2000, 2, 28), 58); assert_eq!(year_day(2000, 2, 29), 59); assert_eq!(year_day(2000, 3, 1), 60); assert_eq!(year_day(2000, 12, 31), 365); assert_eq!(year_day(2001, 1, 1), 0); assert_eq!(year_day(2001, 2, 28), 58); assert_eq!(year_day(2001, 3, 1), 59); assert_eq!(year_day(2001, 12, 31), 364); } #[test] fn test_is_leap_year() { assert!(is_leap_year(2000)); assert!(!is_leap_year(2001)); assert!(is_leap_year(2004)); assert!(!is_leap_year(2100)); assert!(!is_leap_year(2200)); assert!(!is_leap_year(2300)); assert!(is_leap_year(2400)); } #[test] fn test_days_since_unix_epoch() { assert_eq!(days_since_unix_epoch(-1001, 3, 1), -1085076); assert_eq!(days_since_unix_epoch(1600, 2, 29), -135081); assert_eq!(days_since_unix_epoch(1600, 3, 1), -135080); assert_eq!(days_since_unix_epoch(1700, 3, 1), -98556); assert_eq!(days_since_unix_epoch(1701, 3, 1), -98191); assert_eq!(days_since_unix_epoch(1704, 2, 29), -97096); assert_eq!(days_since_unix_epoch(2000, 2, 29), 11016); assert_eq!(days_since_unix_epoch(2000, 3, 1), 11017); assert_eq!(days_since_unix_epoch(2001, 3, 1), 11382); assert_eq!(days_since_unix_epoch(2004, 2, 29), 12477); assert_eq!(days_since_unix_epoch(2100, 3, 1), 47541); assert_eq!(days_since_unix_epoch(3001, 3, 1), 376624); } #[test] fn test_nanoseconds_since_unix_epoch() { assert_eq!(nanoseconds_since_unix_epoch(1, 1000), 1_000_001_000); assert_eq!(nanoseconds_since_unix_epoch(0, 1000), 1000); assert_eq!(nanoseconds_since_unix_epoch(-1, 1000), -999_999_000); assert_eq!(nanoseconds_since_unix_epoch(-2, 1000), -1_999_999_000); } #[test] fn test_total_nanoseconds_to_timespec() -> Result<()> { assert!(matches!(total_nanoseconds_to_timespec(1_000_001_000), Ok((1, 1000)))); assert!(matches!(total_nanoseconds_to_timespec(1000), Ok((0, 1000)))); assert!(matches!(total_nanoseconds_to_timespec(-999_999_000), Ok((-1, 1000)))); assert!(matches!(total_nanoseconds_to_timespec(-1_999_999_000), Ok((-2, 1000)))); assert!(matches!(total_nanoseconds_to_timespec(i128::MAX), Err(OutOfRangeError(_)))); assert!(matches!(total_nanoseconds_to_timespec(i128::MIN), Err(OutOfRangeError(_)))); let min_total_nanoseconds = -9223372036854775808000000000; let max_total_nanoseconds = 9223372036854775807999999999; assert!(matches!(total_nanoseconds_to_timespec(min_total_nanoseconds), Ok((i64::MIN, 0)))); assert!(matches!(total_nanoseconds_to_timespec(max_total_nanoseconds), Ok((i64::MAX, 999999999)))); assert!(matches!(total_nanoseconds_to_timespec(min_total_nanoseconds - 1), Err(OutOfRangeError(_)))); assert!(matches!(total_nanoseconds_to_timespec(max_total_nanoseconds + 1), Err(OutOfRangeError(_)))); Ok(()) } #[test] #[cfg(feature = "const")] fn test_const() -> Result<()> { use crate::timezone::*; macro_rules! unwrap { ($x:expr) => { match $x { Ok(x) => x, Err(_) => const_panic!(), } }; } macro_rules! to_const { ($type:ty, $x:expr) => {{ const TMP: $type = $x; TMP }}; } const TIME_ZONE_REF: TimeZoneRef = unwrap!(TimeZoneRef::new( &[ Transition::new(-2334101314, 1), Transition::new(-1157283000, 2), Transition::new(-1155436200, 1), Transition::new(-880198200, 3), Transition::new(-769395600, 4), Transition::new(-765376200, 1), Transition::new(-712150200, 5), ], to_const!( &[LocalTimeType], &[ unwrap!(LocalTimeType::new(-37886, false, Some(b"LMT"))), unwrap!(LocalTimeType::new(-37800, false, Some(b"HST"))), unwrap!(LocalTimeType::new(-34200, true, Some(b"HDT"))), unwrap!(LocalTimeType::new(-34200, true, Some(b"HWT"))), unwrap!(LocalTimeType::new(-34200, true, Some(b"HPT"))), unwrap!(LocalTimeType::new(-36000, false, Some(b"HST"))), ] ), &[ LeapSecond::new(78796800, 1), LeapSecond::new(94694401, 2), LeapSecond::new(126230402, 3), LeapSecond::new(157766403, 4), LeapSecond::new(189302404, 5), LeapSecond::new(220924805, 6), ], to_const!( &Option, &Some(TransitionRule::Alternate(unwrap!(AlternateTime::new( unwrap!(LocalTimeType::new(-36000, false, Some(b"HST"))), unwrap!(LocalTimeType::new(-34200, true, Some(b"HPT"))), RuleDay::MonthWeekDay(unwrap!(MonthWeekDay::new(10, 5, 0))), 93600, RuleDay::MonthWeekDay(unwrap!(MonthWeekDay::new(3, 4, 4))), 7200, )))) ), )); const UTC: TimeZoneRef = TimeZoneRef::utc(); const UNIX_EPOCH: UtcDateTime = unwrap!(UtcDateTime::from_timespec(0, 0)); const UTC_DATE_TIME: UtcDateTime = unwrap!(UtcDateTime::new(2000, 1, 1, 0, 0, 0, 1000)); const DATE_TIME: DateTime = unwrap!(DateTime::new(2000, 1, 1, 1, 0, 0, 1000, unwrap!(LocalTimeType::with_ut_offset(3600)))); const DATE_TIME_1: DateTime = unwrap!(UTC_DATE_TIME.project(TIME_ZONE_REF)); const DATE_TIME_2: DateTime = unwrap!(DATE_TIME_1.project(UTC)); const LOCAL_TIME_TYPE_1: &LocalTimeType = DATE_TIME_1.local_time_type(); const LOCAL_TIME_TYPE_2: &LocalTimeType = DATE_TIME_2.local_time_type(); assert_eq!(UNIX_EPOCH.unix_time(), 0); assert_eq!(DATE_TIME.unix_time(), UTC_DATE_TIME.unix_time()); assert_eq!(DATE_TIME_2.unix_time(), UTC_DATE_TIME.unix_time()); assert_eq!(DATE_TIME_2.nanoseconds(), UTC_DATE_TIME.nanoseconds()); let date_time = UTC_DATE_TIME.project(TIME_ZONE_REF)?; assert_eq!(date_time.local_time_type().time_zone_designation(), LOCAL_TIME_TYPE_1.time_zone_designation()); let date_time_1 = DateTime::from_timespec(UTC_DATE_TIME.unix_time(), 1000, TIME_ZONE_REF)?; let date_time_2 = date_time_1.project(UTC)?; assert_eq!(date_time, DATE_TIME_1); assert_eq!(date_time_1, DATE_TIME_1); assert_eq!(date_time_2, DATE_TIME_2); let local_time_type_1 = date_time_1.local_time_type(); let local_time_type_2 = date_time_2.local_time_type(); assert_eq!(local_time_type_1.ut_offset(), LOCAL_TIME_TYPE_1.ut_offset()); assert_eq!(local_time_type_1.is_dst(), LOCAL_TIME_TYPE_1.is_dst()); assert_eq!(local_time_type_1.time_zone_designation(), LOCAL_TIME_TYPE_1.time_zone_designation()); assert_eq!(local_time_type_2.ut_offset(), LOCAL_TIME_TYPE_2.ut_offset()); assert_eq!(local_time_type_2.is_dst(), LOCAL_TIME_TYPE_2.is_dst()); assert_eq!(local_time_type_2.time_zone_designation(), LOCAL_TIME_TYPE_2.time_zone_designation()); Ok(()) } } tz-rs-0.6.14/src/error/mod.rs000064400000000000000000000224411046102023000140430ustar 00000000000000//! Error types. use core::array::TryFromSliceError; use core::fmt; use core::num::TryFromIntError; use core::str::Utf8Error; #[cfg(feature = "std")] mod parse { use super::*; use core::num::ParseIntError; /// Unified error type for parsing a TZ string #[cfg_attr(docsrs, doc(cfg(feature = "std")))] #[non_exhaustive] #[derive(Debug)] pub enum TzStringError { /// UTF-8 error Utf8Error(Utf8Error), /// Integer parsing error ParseIntError(ParseIntError), /// I/O error IoError(std::io::Error), /// Invalid TZ string InvalidTzString(&'static str), /// Unsupported TZ string UnsupportedTzString(&'static str), } impl fmt::Display for TzStringError { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { match self { Self::Utf8Error(error) => error.fmt(f), Self::ParseIntError(error) => error.fmt(f), Self::IoError(error) => error.fmt(f), Self::InvalidTzString(error) => write!(f, "invalid TZ string: {}", error), Self::UnsupportedTzString(error) => write!(f, "unsupported TZ string: {}", error), } } } impl std::error::Error for TzStringError {} impl From for TzStringError { fn from(error: Utf8Error) -> Self { Self::Utf8Error(error) } } impl From for TzStringError { fn from(error: ParseIntError) -> Self { Self::ParseIntError(error) } } impl From for TzStringError { fn from(error: std::io::Error) -> Self { Self::IoError(error) } } /// Unified error type for parsing a TZif file #[cfg_attr(docsrs, doc(cfg(feature = "std")))] #[non_exhaustive] #[derive(Debug)] pub enum TzFileError { /// Conversion from slice to array error TryFromSliceError(TryFromSliceError), /// I/O error IoError(std::io::Error), /// Unified error for parsing a TZ string TzStringError(TzStringError), /// Invalid TZif file InvalidTzFile(&'static str), /// Unsupported TZif file UnsupportedTzFile(&'static str), } impl fmt::Display for TzFileError { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { match self { Self::TryFromSliceError(error) => error.fmt(f), Self::IoError(error) => error.fmt(f), Self::TzStringError(error) => error.fmt(f), Self::InvalidTzFile(error) => write!(f, "invalid TZ file: {}", error), Self::UnsupportedTzFile(error) => write!(f, "unsupported TZ file: {}", error), } } } impl std::error::Error for TzFileError {} impl From for TzFileError { fn from(error: TryFromSliceError) -> Self { Self::TryFromSliceError(error) } } impl From for TzFileError { fn from(error: std::io::Error) -> Self { Self::IoError(error) } } impl From for TzFileError { fn from(error: TzStringError) -> Self { Self::TzStringError(error) } } } #[cfg(feature = "std")] pub use parse::{TzFileError, TzStringError}; macro_rules! create_error { (#[$doc:meta], $name:ident) => { #[$doc] #[derive(Debug)] pub struct $name( /// Error description pub &'static str, ); impl fmt::Display for $name { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { self.0.fmt(f) } } #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] impl std::error::Error for $name {} }; } create_error!(#[doc = "Out of range error"], OutOfRangeError); create_error!(#[doc = "Local time type error"], LocalTimeTypeError); create_error!(#[doc = "Transition rule error"], TransitionRuleError); create_error!(#[doc = "Time zone error"], TimeZoneError); create_error!(#[doc = "Date time error"], DateTimeError); create_error!(#[doc = "Local time type search error"], FindLocalTimeTypeError); create_error!(#[doc = "Date time projection error"], ProjectDateTimeError); impl From for ProjectDateTimeError { fn from(error: OutOfRangeError) -> Self { Self(error.0) } } impl From for ProjectDateTimeError { fn from(error: FindLocalTimeTypeError) -> Self { Self(error.0) } } /// Unified error type for everything in the crate #[non_exhaustive] #[derive(Debug)] pub enum TzError { /// UTF-8 error Utf8Error(Utf8Error), /// Conversion from slice to array error TryFromSliceError(TryFromSliceError), /// I/O error #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] IoError(std::io::Error), /// System time error #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] SystemTimeError(std::time::SystemTimeError), /// Unified error for parsing a TZif file #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] TzFileError(TzFileError), /// Unified error for parsing a TZ string #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] TzStringError(TzStringError), /// Out of range error OutOfRangeError(OutOfRangeError), /// Local time type error LocalTimeTypeError(LocalTimeTypeError), /// Transition rule error TransitionRuleError(TransitionRuleError), /// Time zone error TimeZoneError(TimeZoneError), /// Date time error DateTimeError(DateTimeError), /// Local time type search error FindLocalTimeTypeError(FindLocalTimeTypeError), /// Date time projection error ProjectDateTimeError(ProjectDateTimeError), } impl fmt::Display for TzError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Self::Utf8Error(error) => error.fmt(f), Self::TryFromSliceError(error) => error.fmt(f), #[cfg(feature = "std")] Self::IoError(error) => error.fmt(f), #[cfg(feature = "std")] Self::SystemTimeError(error) => error.fmt(f), #[cfg(feature = "std")] Self::TzFileError(error) => error.fmt(f), #[cfg(feature = "std")] Self::TzStringError(error) => error.fmt(f), Self::OutOfRangeError(error) => error.fmt(f), Self::LocalTimeTypeError(error) => write!(f, "invalid local time type: {}", error), Self::TransitionRuleError(error) => write!(f, "invalid transition rule: {}", error), Self::TimeZoneError(error) => write!(f, "invalid time zone: {}", error), Self::DateTimeError(error) => write!(f, "invalid date time: {}", error), Self::FindLocalTimeTypeError(error) => error.fmt(f), Self::ProjectDateTimeError(error) => error.fmt(f), } } } #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] impl std::error::Error for TzError {} impl From for TzError { fn from(error: Utf8Error) -> Self { Self::Utf8Error(error) } } impl From for TzError { fn from(error: TryFromSliceError) -> Self { Self::TryFromSliceError(error) } } #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] impl From for TzError { fn from(error: std::io::Error) -> Self { Self::IoError(error) } } #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] impl From for TzError { fn from(error: std::time::SystemTimeError) -> Self { Self::SystemTimeError(error) } } #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] impl From for TzError { fn from(error: TzFileError) -> Self { Self::TzFileError(error) } } #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] impl From for TzError { fn from(error: TzStringError) -> Self { Self::TzStringError(error) } } impl From for TzError { fn from(error: OutOfRangeError) -> Self { Self::OutOfRangeError(error) } } impl From for TzError { fn from(_: TryFromIntError) -> Self { Self::OutOfRangeError(OutOfRangeError("out of range integer conversion")) } } impl From for TzError { fn from(error: LocalTimeTypeError) -> Self { Self::LocalTimeTypeError(error) } } impl From for TzError { fn from(error: TransitionRuleError) -> Self { Self::TransitionRuleError(error) } } impl From for TzError { fn from(error: TimeZoneError) -> Self { Self::TimeZoneError(error) } } impl From for TzError { fn from(error: DateTimeError) -> Self { Self::DateTimeError(error) } } impl From for TzError { fn from(error: FindLocalTimeTypeError) -> Self { Self::FindLocalTimeTypeError(error) } } impl From for TzError { fn from(error: ProjectDateTimeError) -> Self { Self::ProjectDateTimeError(error) } } tz-rs-0.6.14/src/lib.rs000064400000000000000000000217111046102023000127000ustar 00000000000000#![deny(missing_docs)] #![cfg_attr(not(feature = "std"), no_std)] #![cfg_attr(docsrs, feature(doc_cfg))] //! This crate provides the [`TimeZone`] and [`DateTime`] types, which can be used to determine local time on a given time zone. //! //! This allows to convert between an [Unix timestamp](https://en.wikipedia.org/wiki/Unix_time) and a calendar time exprimed in the [proleptic gregorian calendar](https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar) with a provided time zone. //! //! 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. //! //! Two formats are currently accepted for the `TZ` string: //! * `std offset[dst[offset][,start[/time],end[/time]]]` providing a time zone description, //! * `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. //! //! 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). //! //! # Usage //! //! ## Time zone //! //! ```rust //! # fn main() -> Result<(), tz::TzError> { //! # #[cfg(feature = "std")] { //! use tz::TimeZone; //! //! // 2000-01-01T00:00:00Z //! let unix_time = 946684800; //! //! // Get UTC time zone //! let time_zone_utc = TimeZone::utc(); //! assert_eq!(time_zone_utc.find_local_time_type(unix_time)?.ut_offset(), 0); //! //! // Get fixed time zone at GMT-1 //! let time_zone_fixed = TimeZone::fixed(-3600)?; //! assert_eq!(time_zone_fixed.find_local_time_type(unix_time)?.ut_offset(), -3600); //! //! // Get local time zone (UNIX only) //! let time_zone_local = TimeZone::local()?; //! // Get the current local time type //! let _current_local_time_type = time_zone_local.find_current_local_time_type()?; //! //! // Get time zone from a TZ string: //! // From an absolute file //! let _ = TimeZone::from_posix_tz("/usr/share/zoneinfo/Pacific/Auckland"); //! // From a file relative to the system timezone directory //! let _ = TimeZone::from_posix_tz("Pacific/Auckland"); //! // From a time zone description //! TimeZone::from_posix_tz("HST10")?; //! TimeZone::from_posix_tz("<-03>3")?; //! TimeZone::from_posix_tz("NZST-12:00:00NZDT-13:00:00,M10.1.0,M3.3.0")?; //! // Use a leading colon to force searching for a corresponding file //! let _ = TimeZone::from_posix_tz(":UTC"); //! # } //! # Ok(()) //! # } //! ``` //! //! ## Date time //! //! ```rust //! # fn main() -> Result<(), tz::TzError> { //! # #[cfg(feature = "std")] { //! use tz::{DateTime, LocalTimeType, TimeZone, UtcDateTime}; //! //! // Get the current UTC date time //! let _current_utc_date_time = UtcDateTime::now()?; //! //! // Create a new UTC date time (2000-01-01T00:00:00.123456789Z) //! let utc_date_time = UtcDateTime::new(2000, 1, 1, 0, 0, 0, 123_456_789)?; //! assert_eq!(utc_date_time.year(), 2000); //! assert_eq!(utc_date_time.month(), 1); //! assert_eq!(utc_date_time.month_day(), 1); //! assert_eq!(utc_date_time.hour(), 0); //! assert_eq!(utc_date_time.minute(), 0); //! assert_eq!(utc_date_time.second(), 0); //! assert_eq!(utc_date_time.week_day(), 6); //! assert_eq!(utc_date_time.year_day(), 0); //! assert_eq!(utc_date_time.unix_time(), 946684800); //! assert_eq!(utc_date_time.nanoseconds(), 123_456_789); //! assert_eq!(utc_date_time.to_string(), "2000-01-01T00:00:00.123456789Z"); //! //! // Create a new UTC date time from a Unix time with nanoseconds (2000-01-01T00:00:00.123456789Z) //! let other_utc_date_time = UtcDateTime::from_timespec(946684800, 123_456_789)?; //! assert_eq!(other_utc_date_time, utc_date_time); //! //! // Project the UTC date time to a time zone //! let date_time = utc_date_time.project(TimeZone::fixed(-3600)?.as_ref())?; //! assert_eq!(date_time.year(), 1999); //! assert_eq!(date_time.month(), 12); //! assert_eq!(date_time.month_day(), 31); //! assert_eq!(date_time.hour(), 23); //! assert_eq!(date_time.minute(), 0); //! assert_eq!(date_time.second(), 0); //! assert_eq!(date_time.week_day(), 5); //! assert_eq!(date_time.year_day(), 364); //! assert_eq!(date_time.local_time_type().ut_offset(), -3600); //! assert_eq!(date_time.unix_time(), 946684800); //! assert_eq!(date_time.nanoseconds(), 123_456_789); //! assert_eq!(date_time.to_string(), "1999-12-31T23:00:00.123456789-01:00"); //! //! // Project the date time to another time zone //! let other_date_time = date_time.project(TimeZone::fixed(3600)?.as_ref())?; //! assert_eq!(other_date_time.year(), 2000); //! assert_eq!(other_date_time.month(), 1); //! assert_eq!(other_date_time.month_day(), 1); //! assert_eq!(other_date_time.hour(), 1); //! assert_eq!(other_date_time.minute(), 0); //! assert_eq!(other_date_time.second(), 0); //! assert_eq!(other_date_time.week_day(), 6); //! assert_eq!(other_date_time.year_day(), 0); //! assert_eq!(other_date_time.local_time_type().ut_offset(), 3600); //! assert_eq!(other_date_time.unix_time(), 946684800); //! assert_eq!(other_date_time.nanoseconds(), 123_456_789); //! assert_eq!(other_date_time.to_string(), "2000-01-01T01:00:00.123456789+01:00"); //! //! // Create a new date time from a Unix time with nanoseconds and a time zone (2000-01-01T00:00:00.123456789Z) //! let another_date_time = DateTime::from_timespec(946684800, 123_456_789, TimeZone::fixed(86400)?.as_ref())?; //! //! // DateTime objects are compared by their Unix time and nanoseconds //! assert_eq!(another_date_time, other_date_time); //! //! // Get the current date time at the local time zone (UNIX only) //! let time_zone_local = TimeZone::local()?; //! let _date_time = DateTime::now(time_zone_local.as_ref())?; //! //! // Create a new date time with an UTC offset (2000-01-01T01:00:00.123456789+01:00) //! let date_time = DateTime::new(2000, 1, 1, 1, 0, 0, 123_456_789, LocalTimeType::with_ut_offset(3600)?)?; //! assert_eq!(date_time.year(), 2000); //! assert_eq!(date_time.month(), 1); //! assert_eq!(date_time.month_day(), 1); //! assert_eq!(date_time.hour(), 1); //! assert_eq!(date_time.minute(), 0); //! assert_eq!(date_time.second(), 0); //! assert_eq!(date_time.week_day(), 6); //! assert_eq!(date_time.year_day(), 0); //! assert_eq!(date_time.unix_time(), 946684800); //! assert_eq!(date_time.nanoseconds(), 123_456_789); //! assert_eq!(date_time.to_string(), "2000-01-01T01:00:00.123456789+01:00"); //! //! // //! // Find the possible date times corresponding to a date, a time and a time zone //! // //! let time_zone = TimeZone::from_posix_tz("CET-1CEST,M3.5.0,M10.5.0/3")?; //! //! // Found date time is unique //! let found_date_times = DateTime::find(2000, 1, 1, 0, 0, 0, 0, time_zone.as_ref())?; //! let unique = found_date_times.unique().unwrap(); //! assert_eq!(unique, found_date_times.earliest().unwrap()); //! assert_eq!(unique, found_date_times.latest().unwrap()); //! assert_eq!(unique.local_time_type().ut_offset(), 3600); //! assert_eq!(unique.local_time_type().time_zone_designation(), "CET"); //! //! // Found date time was skipped by a forward transition //! let found_date_times = DateTime::find(2000, 3, 26, 2, 30, 0, 0, time_zone.as_ref())?; //! //! assert_eq!(found_date_times.unique(), None); //! //! let earliest = found_date_times.earliest().unwrap(); //! assert_eq!(earliest.hour(), 2); //! assert_eq!(earliest.minute(), 0); //! assert_eq!(earliest.local_time_type().ut_offset(), 3600); //! assert_eq!(earliest.local_time_type().time_zone_designation(), "CET"); //! //! let latest = found_date_times.latest().unwrap(); //! assert_eq!(latest.hour(), 3); //! assert_eq!(latest.minute(), 0); //! assert_eq!(latest.local_time_type().ut_offset(), 7200); //! assert_eq!(latest.local_time_type().time_zone_designation(), "CEST"); //! //! // Found date time is ambiguous because of a backward transition //! let found_date_times = DateTime::find(2000, 10, 29, 2, 30, 0, 0, time_zone.as_ref())?; //! //! assert_eq!(found_date_times.unique(), None); //! //! let earliest = found_date_times.earliest().unwrap(); //! assert_eq!(earliest.hour(), 2); //! assert_eq!(earliest.minute(), 30); //! assert_eq!(earliest.local_time_type().ut_offset(), 7200); //! assert_eq!(earliest.local_time_type().time_zone_designation(), "CEST"); //! //! let latest = found_date_times.latest().unwrap(); //! assert_eq!(latest.hour(), 2); //! assert_eq!(latest.minute(), 30); //! assert_eq!(latest.local_time_type().ut_offset(), 3600); //! assert_eq!(latest.local_time_type().time_zone_designation(), "CET"); //! # } //! # Ok(()) //! # } //! ``` #[cfg(feature = "alloc")] extern crate alloc; mod constants; mod utils; #[cfg(feature = "std")] mod parse; pub mod datetime; pub mod error; pub mod timezone; #[doc(inline)] pub use datetime::{DateTime, UtcDateTime}; #[doc(inline)] pub use error::TzError; #[doc(inline)] pub use timezone::{LocalTimeType, TimeZoneRef}; #[doc(inline)] #[cfg(feature = "alloc")] pub use timezone::TimeZone; /// Alias for [`std::result::Result`] with the crate unified error pub type Result = core::result::Result; tz-rs-0.6.14/src/parse/mod.rs000064400000000000000000000001551046102023000140220ustar 00000000000000//! Parsing functions. mod tz_file; mod tz_string; pub(crate) use tz_file::*; pub(crate) use tz_string::*; tz-rs-0.6.14/src/parse/tz_file.rs000064400000000000000000000366631046102023000147140ustar 00000000000000//! Functions used for parsing a TZif file. use super::tz_string::parse_posix_tz; use crate::error::{TzError, TzFileError}; use crate::timezone::*; use crate::utils::*; use std::convert::TryInto; use std::fs::File; use std::io; use std::iter; use std::str; /// TZif version #[derive(Debug, Copy, Clone, Eq, PartialEq)] enum Version { /// Version 1 V1, /// Version 2 V2, /// Version 3 V3, } /// TZif header #[derive(Debug)] struct Header { /// TZif version version: Version, /// Number of UT/local indicators ut_local_count: usize, /// Number of standard/wall indicators std_wall_count: usize, /// Number of leap-second records leap_count: usize, /// Number of transition times transition_count: usize, /// Number of local time type records type_count: usize, /// Number of time zone designations bytes char_count: usize, } /// Parse TZif header fn parse_header(cursor: &mut Cursor) -> Result { let magic = cursor.read_exact(4)?; if magic != *b"TZif" { return Err(TzFileError::InvalidTzFile("invalid magic number")); } let version = match cursor.read_exact(1)? { [0x00] => Version::V1, [0x32] => Version::V2, [0x33] => Version::V3, _ => return Err(TzFileError::UnsupportedTzFile("unsupported TZif version")), }; cursor.read_exact(15)?; let ut_local_count = u32::from_be_bytes(cursor.read_exact(4)?.try_into()?); let std_wall_count = u32::from_be_bytes(cursor.read_exact(4)?.try_into()?); let leap_count = u32::from_be_bytes(cursor.read_exact(4)?.try_into()?); let transition_count = u32::from_be_bytes(cursor.read_exact(4)?.try_into()?); let type_count = u32::from_be_bytes(cursor.read_exact(4)?.try_into()?); let char_count = u32::from_be_bytes(cursor.read_exact(4)?.try_into()?); 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)) { return Err(TzFileError::InvalidTzFile("invalid header")); } Ok(Header { version, ut_local_count: ut_local_count as usize, std_wall_count: std_wall_count as usize, leap_count: leap_count as usize, transition_count: transition_count as usize, type_count: type_count as usize, char_count: char_count as usize, }) } /// Parse TZif footer fn parse_footer(footer: &[u8], use_string_extensions: bool) -> Result, TzError> { let footer = str::from_utf8(footer)?; if !(footer.starts_with('\n') && footer.ends_with('\n')) { return Err(TzFileError::InvalidTzFile("invalid footer").into()); } let tz_string = footer.trim_matches(|c: char| c.is_ascii_whitespace()); if tz_string.starts_with(':') || tz_string.contains('\0') { return Err(TzFileError::InvalidTzFile("invalid footer").into()); } if !tz_string.is_empty() { Ok(Some(parse_posix_tz(tz_string.as_bytes(), use_string_extensions)).transpose()?) } else { Ok(None) } } /// TZif data blocks struct DataBlock<'a> { /// Time size in bytes time_size: usize, /// Transition times data block transition_times: &'a [u8], /// Transition types data block transition_types: &'a [u8], /// Local time types data block local_time_types: &'a [u8], /// Time zone designations data block time_zone_designations: &'a [u8], /// Leap seconds data block leap_seconds: &'a [u8], /// UT/local indicators data block std_walls: &'a [u8], /// Standard/wall indicators data block ut_locals: &'a [u8], } impl<'a> DataBlock<'a> { /// Read TZif data blocks fn new(cursor: &mut Cursor<'a>, header: &Header, version: Version) -> Result { let time_size = match version { Version::V1 => 4, Version::V2 | Version::V3 => 8, }; Ok(Self { time_size, transition_times: cursor.read_exact(header.transition_count * time_size)?, transition_types: cursor.read_exact(header.transition_count)?, local_time_types: cursor.read_exact(header.type_count * 6)?, time_zone_designations: cursor.read_exact(header.char_count)?, leap_seconds: cursor.read_exact(header.leap_count * (time_size + 4))?, std_walls: cursor.read_exact(header.std_wall_count)?, ut_locals: cursor.read_exact(header.ut_local_count)?, }) } /// Parse time values fn parse_time(&self, arr: &[u8], version: Version) -> Result { Ok(match version { Version::V1 => i32::from_be_bytes(arr.try_into()?).into(), Version::V2 | Version::V3 => i64::from_be_bytes(arr.try_into()?), }) } /// Parse time zone data fn parse(&self, header: &Header, footer: Option<&[u8]>) -> Result { let mut transitions = Vec::with_capacity(header.transition_count); for (arr_time, &local_time_type_index) in self.transition_times.chunks_exact(self.time_size).zip(self.transition_types) { let unix_leap_time = self.parse_time(&arr_time[0..self.time_size], header.version)?; let local_time_type_index = local_time_type_index as usize; transitions.push(Transition::new(unix_leap_time, local_time_type_index)); } let mut local_time_types = Vec::with_capacity(header.type_count); for arr in self.local_time_types.chunks_exact(6) { let ut_offset = i32::from_be_bytes(arr[0..4].try_into()?); let is_dst = match arr[4] { 0 => false, 1 => true, _ => return Err(TzFileError::InvalidTzFile("invalid DST indicator").into()), }; let char_index = arr[5] as usize; if char_index >= header.char_count { return Err(TzFileError::InvalidTzFile("invalid time zone designation char index").into()); } let time_zone_designation = match self.time_zone_designations[char_index..].iter().position(|&c| c == b'\0') { None => return Err(TzFileError::InvalidTzFile("invalid time zone designation char index").into()), Some(position) => { let time_zone_designation = &self.time_zone_designations[char_index..char_index + position]; if !time_zone_designation.is_empty() { Some(time_zone_designation) } else { None } } }; local_time_types.push(LocalTimeType::new(ut_offset, is_dst, time_zone_designation)?); } let mut leap_seconds = Vec::with_capacity(header.leap_count); for arr in self.leap_seconds.chunks_exact(self.time_size + 4) { let unix_leap_time = self.parse_time(&arr[0..self.time_size], header.version)?; let correction = i32::from_be_bytes(arr[self.time_size..self.time_size + 4].try_into()?); leap_seconds.push(LeapSecond::new(unix_leap_time, correction)); } let std_walls_iter = self.std_walls.iter().copied().chain(iter::repeat(0)); let ut_locals_iter = self.ut_locals.iter().copied().chain(iter::repeat(0)); for (std_wall, ut_local) in std_walls_iter.zip(ut_locals_iter).take(header.type_count) { if !matches!((std_wall, ut_local), (0, 0) | (1, 0) | (1, 1)) { return Err(TzFileError::InvalidTzFile("invalid couple of standard/wall and UT/local indicators").into()); } } let extra_rule = footer.and_then(|footer| parse_footer(footer, header.version == Version::V3).transpose()).transpose()?; Ok(TimeZone::new(transitions, local_time_types, leap_seconds, extra_rule)?) } } /// Parse TZif file as described in [RFC 8536](https://datatracker.ietf.org/doc/html/rfc8536) pub(crate) fn parse_tz_file(bytes: &[u8]) -> Result { let mut cursor = Cursor::new(bytes); let header = parse_header(&mut cursor)?; match header.version { Version::V1 => { let data_block = DataBlock::new(&mut cursor, &header, header.version)?; if !cursor.is_empty() { return Err(TzFileError::InvalidTzFile("remaining data after end of TZif v1 data block").into()); } Ok(data_block.parse(&header, None)?) } Version::V2 | Version::V3 => { // Skip v1 data block DataBlock::new(&mut cursor, &header, Version::V1)?; let header = parse_header(&mut cursor)?; let data_block = DataBlock::new(&mut cursor, &header, header.version)?; let footer = cursor.remaining(); Ok(data_block.parse(&header, Some(footer))?) } } } /// Open the TZif file corresponding to a TZ string pub(crate) fn get_tz_file(tz_string: &str) -> Result { // Don't check system timezone directories on non-UNIX platforms #[cfg(not(unix))] return Ok(File::open(tz_string)?); #[cfg(unix)] { // Possible system timezone directories const ZONE_INFO_DIRECTORIES: [&str; 3] = ["/usr/share/zoneinfo", "/share/zoneinfo", "/etc/zoneinfo"]; if tz_string.starts_with('/') { Ok(File::open(tz_string)?) } else { for folder in &ZONE_INFO_DIRECTORIES { if let Ok(file) = File::open(format!("{}/{}", folder, tz_string)) { return Ok(file); } } Err(TzFileError::IoError(io::ErrorKind::NotFound.into())) } } } #[cfg(test)] mod test { use super::*; #[test] fn test_v1_file_with_leap_seconds() -> Result<(), TzError> { 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"; let time_zone = parse_tz_file(bytes)?; let time_zone_result = TimeZone::new( vec![], vec![LocalTimeType::new(0, false, Some(b"UTC"))?], vec![ LeapSecond::new(78796800, 1), LeapSecond::new(94694401, 2), LeapSecond::new(126230402, 3), LeapSecond::new(157766403, 4), LeapSecond::new(189302404, 5), LeapSecond::new(220924805, 6), LeapSecond::new(252460806, 7), LeapSecond::new(283996807, 8), LeapSecond::new(315532808, 9), LeapSecond::new(362793609, 10), LeapSecond::new(394329610, 11), LeapSecond::new(425865611, 12), LeapSecond::new(489024012, 13), LeapSecond::new(567993613, 14), LeapSecond::new(631152014, 15), LeapSecond::new(662688015, 16), LeapSecond::new(709948816, 17), LeapSecond::new(741484817, 18), LeapSecond::new(773020818, 19), LeapSecond::new(820454419, 20), LeapSecond::new(867715220, 21), LeapSecond::new(915148821, 22), LeapSecond::new(1136073622, 23), LeapSecond::new(1230768023, 24), LeapSecond::new(1341100824, 25), LeapSecond::new(1435708825, 26), LeapSecond::new(1483228826, 27), ], None, )?; assert_eq!(time_zone, time_zone_result); Ok(()) } #[test] fn test_v2_file() -> Result<(), TzError> { 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"; let time_zone = parse_tz_file(bytes)?; let time_zone_result = TimeZone::new( vec![ Transition::new(-2334101314, 1), Transition::new(-1157283000, 2), Transition::new(-1155436200, 1), Transition::new(-880198200, 3), Transition::new(-769395600, 4), Transition::new(-765376200, 1), Transition::new(-712150200, 5), ], vec![ LocalTimeType::new(-37886, false, Some(b"LMT"))?, LocalTimeType::new(-37800, false, Some(b"HST"))?, LocalTimeType::new(-34200, true, Some(b"HDT"))?, LocalTimeType::new(-34200, true, Some(b"HWT"))?, LocalTimeType::new(-34200, true, Some(b"HPT"))?, LocalTimeType::new(-36000, false, Some(b"HST"))?, ], vec![], Some(TransitionRule::Fixed(LocalTimeType::new(-36000, false, Some(b"HST"))?)), )?; assert_eq!(time_zone, time_zone_result); assert_eq!(*time_zone.find_local_time_type(-1156939200)?, LocalTimeType::new(-34200, true, Some(b"HDT"))?); assert_eq!(*time_zone.find_local_time_type(1546300800)?, LocalTimeType::new(-36000, false, Some(b"HST"))?); Ok(()) } #[test] fn test_v3_file() -> Result<(), TzError> { 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"; let time_zone = parse_tz_file(bytes)?; let time_zone_result = TimeZone::new( vec![Transition::new(2145916800, 0)], vec![LocalTimeType::new(7200, false, Some(b"IST"))?], vec![], Some(TransitionRule::Alternate(AlternateTime::new( LocalTimeType::new(7200, false, Some(b"IST"))?, LocalTimeType::new(10800, true, Some(b"IDT"))?, RuleDay::MonthWeekDay(MonthWeekDay::new(3, 4, 4)?), 93600, RuleDay::MonthWeekDay(MonthWeekDay::new(10, 5, 0)?), 7200, )?)), )?; assert_eq!(time_zone, time_zone_result); Ok(()) } } tz-rs-0.6.14/src/parse/tz_string.rs000064400000000000000000000277101046102023000152740ustar 00000000000000//! Functions used for parsing a TZ string. use crate::error::{TzError, TzStringError}; use crate::timezone::*; use crate::utils::*; use std::num::ParseIntError; use std::str::{self, FromStr}; /// Parse integer from a slice of bytes fn parse_int>(bytes: &[u8]) -> Result { Ok(str::from_utf8(bytes)?.parse()?) } /// Parse time zone designation fn parse_time_zone_designation<'a>(cursor: &mut Cursor<'a>) -> Result<&'a [u8], TzStringError> { let unquoted = if cursor.remaining().first() == Some(&b'<') { cursor.read_exact(1)?; let unquoted = cursor.read_until(|&x| x == b'>')?; cursor.read_exact(1)?; unquoted } else { cursor.read_while(u8::is_ascii_alphabetic)? }; Ok(unquoted) } /// Parse hours, minutes and seconds fn parse_hhmmss(cursor: &mut Cursor) -> Result<(i32, i32, i32), TzStringError> { let hour = parse_int(cursor.read_while(u8::is_ascii_digit)?)?; let mut minute = 0; let mut second = 0; if cursor.read_optional_tag(b":")? { minute = parse_int(cursor.read_while(u8::is_ascii_digit)?)?; if cursor.read_optional_tag(b":")? { second = parse_int(cursor.read_while(u8::is_ascii_digit)?)?; } } Ok((hour, minute, second)) } /// Parse signed hours, minutes and seconds fn parse_signed_hhmmss(cursor: &mut Cursor) -> Result<(i32, i32, i32, i32), TzStringError> { let mut sign = 1; if let Some(&c @ b'+') | Some(&c @ b'-') = cursor.remaining().first() { cursor.read_exact(1)?; if c == b'-' { sign = -1; } } let (hour, minute, second) = parse_hhmmss(cursor)?; Ok((sign, hour, minute, second)) } /// Parse time zone offset fn parse_offset(cursor: &mut Cursor) -> Result { let (sign, hour, minute, second) = parse_signed_hhmmss(cursor)?; if !(0..=24).contains(&hour) { return Err(TzStringError::InvalidTzString("invalid offset hour")); } if !(0..=59).contains(&minute) { return Err(TzStringError::InvalidTzString("invalid offset minute")); } if !(0..=59).contains(&second) { return Err(TzStringError::InvalidTzString("invalid offset second")); } Ok(sign * (hour * 3600 + minute * 60 + second)) } /// Parse transition rule day fn parse_rule_day(cursor: &mut Cursor) -> Result { match cursor.remaining().first() { Some(b'J') => { cursor.read_exact(1)?; Ok(RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(parse_int(cursor.read_while(u8::is_ascii_digit)?)?)?)) } Some(b'M') => { cursor.read_exact(1)?; let month = parse_int(cursor.read_while(u8::is_ascii_digit)?)?; cursor.read_tag(b".")?; let week = parse_int(cursor.read_while(u8::is_ascii_digit)?)?; cursor.read_tag(b".")?; let week_day = parse_int(cursor.read_while(u8::is_ascii_digit)?)?; Ok(RuleDay::MonthWeekDay(MonthWeekDay::new(month, week, week_day)?)) } _ => Ok(RuleDay::Julian0WithLeap(Julian0WithLeap::new(parse_int(cursor.read_while(u8::is_ascii_digit)?)?)?)), } } /// Parse transition rule time fn parse_rule_time(cursor: &mut Cursor) -> Result { let (hour, minute, second) = parse_hhmmss(cursor)?; if !(0..=24).contains(&hour) { return Err(TzStringError::InvalidTzString("invalid day time hour")); } if !(0..=59).contains(&minute) { return Err(TzStringError::InvalidTzString("invalid day time minute")); } if !(0..=59).contains(&second) { return Err(TzStringError::InvalidTzString("invalid day time second")); } Ok(hour * 3600 + minute * 60 + second) } /// Parse transition rule time with TZ string extensions fn parse_rule_time_extended(cursor: &mut Cursor) -> Result { let (sign, hour, minute, second) = parse_signed_hhmmss(cursor)?; if !(-167..=167).contains(&hour) { return Err(TzStringError::InvalidTzString("invalid day time hour")); } if !(0..=59).contains(&minute) { return Err(TzStringError::InvalidTzString("invalid day time minute")); } if !(0..=59).contains(&second) { return Err(TzStringError::InvalidTzString("invalid day time second")); } Ok(sign * (hour * 3600 + minute * 60 + second)) } /// Parse transition rule fn parse_rule_block(cursor: &mut Cursor, use_string_extensions: bool) -> Result<(RuleDay, i32), TzError> { let date = parse_rule_day(cursor)?; let time = if cursor.read_optional_tag(b"/")? { if use_string_extensions { parse_rule_time_extended(cursor)? } else { parse_rule_time(cursor)? } } else { 2 * 3600 }; Ok((date, time)) } /// 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). /// /// TZ string extensions from [RFC 8536](https://datatracker.ietf.org/doc/html/rfc8536#section-3.3.1) may be used. /// pub(crate) fn parse_posix_tz(tz_string: &[u8], use_string_extensions: bool) -> Result { let mut cursor = Cursor::new(tz_string); let std_time_zone = Some(parse_time_zone_designation(&mut cursor)?); let std_offset = parse_offset(&mut cursor)?; if cursor.is_empty() { return Ok(TransitionRule::Fixed(LocalTimeType::new(-std_offset, false, std_time_zone)?)); } let dst_time_zone = Some(parse_time_zone_designation(&mut cursor)?); let dst_offset = match cursor.remaining().first() { Some(&b',') => std_offset - 3600, Some(_) => parse_offset(&mut cursor)?, None => return Err(TzStringError::UnsupportedTzString("DST start and end rules must be provided").into()), }; if cursor.is_empty() { return Err(TzStringError::UnsupportedTzString("DST start and end rules must be provided").into()); } cursor.read_tag(b",")?; let (dst_start, dst_start_time) = parse_rule_block(&mut cursor, use_string_extensions)?; cursor.read_tag(b",")?; let (dst_end, dst_end_time) = parse_rule_block(&mut cursor, use_string_extensions)?; if !cursor.is_empty() { return Err(TzStringError::InvalidTzString("remaining data after parsing TZ string").into()); } Ok(TransitionRule::Alternate(AlternateTime::new( LocalTimeType::new(-std_offset, false, std_time_zone)?, LocalTimeType::new(-dst_offset, true, dst_time_zone)?, dst_start, dst_start_time, dst_end, dst_end_time, )?)) } #[cfg(test)] mod test { use super::*; #[test] fn test_no_dst() -> Result<(), TzError> { let tz_string = b"HST10"; let transition_rule = parse_posix_tz(tz_string, false)?; let transition_rule_result = TransitionRule::Fixed(LocalTimeType::new(-36000, false, Some(b"HST"))?); assert_eq!(transition_rule, transition_rule_result); Ok(()) } #[test] fn test_quoted() -> Result<(), TzError> { let tz_string = b"<-03>+3<+03>-3,J1,J365"; let transition_rule = parse_posix_tz(tz_string, false)?; let transition_rule_result = TransitionRule::Alternate(AlternateTime::new( LocalTimeType::new(-10800, false, Some(b"-03"))?, LocalTimeType::new(10800, true, Some(b"+03"))?, RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(1)?), 7200, RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(365)?), 7200, )?); assert_eq!(transition_rule, transition_rule_result); Ok(()) } #[test] fn test_full() -> Result<(), TzError> { let tz_string = b"NZST-12:00:00NZDT-13:00:00,M10.1.0/02:00:00,M3.3.0/02:00:00"; let transition_rule = parse_posix_tz(tz_string, false)?; let transition_rule_result = TransitionRule::Alternate(AlternateTime::new( LocalTimeType::new(43200, false, Some(b"NZST"))?, LocalTimeType::new(46800, true, Some(b"NZDT"))?, RuleDay::MonthWeekDay(MonthWeekDay::new(10, 1, 0)?), 7200, RuleDay::MonthWeekDay(MonthWeekDay::new(3, 3, 0)?), 7200, )?); assert_eq!(transition_rule, transition_rule_result); Ok(()) } #[test] fn test_negative_dst() -> Result<(), TzError> { let tz_string = b"IST-1GMT0,M10.5.0,M3.5.0/1"; let transition_rule = parse_posix_tz(tz_string, false)?; let transition_rule_result = TransitionRule::Alternate(AlternateTime::new( LocalTimeType::new(3600, false, Some(b"IST"))?, LocalTimeType::new(0, true, Some(b"GMT"))?, RuleDay::MonthWeekDay(MonthWeekDay::new(10, 5, 0)?), 7200, RuleDay::MonthWeekDay(MonthWeekDay::new(3, 5, 0)?), 3600, )?); assert_eq!(transition_rule, transition_rule_result); Ok(()) } #[test] fn test_negative_hour() -> Result<(), TzError> { let tz_string = b"<-03>3<-02>,M3.5.0/-2,M10.5.0/-1"; assert!(parse_posix_tz(tz_string, false).is_err()); let transition_rule = parse_posix_tz(tz_string, true)?; let transition_rule_result = TransitionRule::Alternate(AlternateTime::new( LocalTimeType::new(-10800, false, Some(b"-03"))?, LocalTimeType::new(-7200, true, Some(b"-02"))?, RuleDay::MonthWeekDay(MonthWeekDay::new(3, 5, 0)?), -7200, RuleDay::MonthWeekDay(MonthWeekDay::new(10, 5, 0)?), -3600, )?); assert_eq!(transition_rule, transition_rule_result); Ok(()) } #[test] fn test_all_year_dst() -> Result<(), TzError> { let tz_string = b"EST5EDT,0/0,J365/25"; assert!(parse_posix_tz(tz_string, false).is_err()); let transition_rule = parse_posix_tz(tz_string, true)?; let transition_rule_result = TransitionRule::Alternate(AlternateTime::new( LocalTimeType::new(-18000, false, Some(b"EST"))?, LocalTimeType::new(-14400, true, Some(b"EDT"))?, RuleDay::Julian0WithLeap(Julian0WithLeap::new(0)?), 0, RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(365)?), 90000, )?); assert_eq!(transition_rule, transition_rule_result); Ok(()) } #[test] fn test_min_dst_offset() -> Result<(), TzError> { let tz_string = b"STD24:59:59DST,J1,J365"; let transition_rule = parse_posix_tz(tz_string, false)?; let transition_rule_result = TransitionRule::Alternate(AlternateTime::new( LocalTimeType::new(-89999, false, Some(b"STD"))?, LocalTimeType::new(-86399, true, Some(b"DST"))?, RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(1)?), 7200, RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(365)?), 7200, )?); assert_eq!(transition_rule, transition_rule_result); Ok(()) } #[test] fn test_max_dst_offset() -> Result<(), TzError> { let tz_string = b"STD-24:59:59DST,J1,J365"; let transition_rule = parse_posix_tz(tz_string, false)?; let transition_rule_result = TransitionRule::Alternate(AlternateTime::new( LocalTimeType::new(89999, false, Some(b"STD"))?, LocalTimeType::new(93599, true, Some(b"DST"))?, RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(1)?), 7200, RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(365)?), 7200, )?); assert_eq!(transition_rule, transition_rule_result); Ok(()) } #[test] fn test_error() -> Result<(), TzError> { assert!(matches!(parse_posix_tz(b"IST-1GMT0", false), Err(TzError::TzStringError(TzStringError::UnsupportedTzString(_))))); assert!(matches!(parse_posix_tz(b"EET-2EEST", false), Err(TzError::TzStringError(TzStringError::UnsupportedTzString(_))))); Ok(()) } } tz-rs-0.6.14/src/timezone/mod.rs000064400000000000000000000651161046102023000145520ustar 00000000000000//! Types related to a time zone. mod rule; pub use rule::*; use crate::datetime::{days_since_unix_epoch, is_leap_year}; use crate::error::*; use crate::utils::*; use crate::UtcDateTime; use core::cmp::Ordering; use core::fmt; use core::str; #[cfg(feature = "alloc")] use alloc::{vec, vec::Vec}; /// Transition of a TZif file #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub struct Transition { /// Unix leap time unix_leap_time: i64, /// Index specifying the local time type of the transition local_time_type_index: usize, } impl Transition { /// Construct a TZif file transition #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn new(unix_leap_time: i64, local_time_type_index: usize) -> Self { Self { unix_leap_time, local_time_type_index } } /// Returns Unix leap time #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn unix_leap_time(&self) -> i64 { self.unix_leap_time } /// Returns local time type index #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn local_time_type_index(&self) -> usize { self.local_time_type_index } } /// Leap second of a TZif file #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub struct LeapSecond { /// Unix leap time unix_leap_time: i64, /// Leap second correction correction: i32, } impl LeapSecond { /// Construct a TZif file leap second #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn new(unix_leap_time: i64, correction: i32) -> Self { Self { unix_leap_time, correction } } /// Returns Unix leap time #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn unix_leap_time(&self) -> i64 { self.unix_leap_time } /// Returns leap second correction #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn correction(&self) -> i32 { self.correction } } /// ASCII-encoded fixed-capacity string, used for storing time zone designations #[derive(Copy, Clone, Eq, PartialEq)] struct TzAsciiStr { /// Length-prefixed string buffer bytes: [u8; 8], } impl TzAsciiStr { /// Construct a time zone designation string #[cfg_attr(feature = "const", const_fn::const_fn)] fn new(input: &[u8]) -> Result { let len = input.len(); if !(3 <= len && len <= 7) { return Err(LocalTimeTypeError("time zone designation must have between 3 and 7 characters")); } let mut bytes = [0; 8]; bytes[0] = input.len() as u8; let mut i = 0; while i < len { let b = input[i]; if !matches!(b, b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'+' | b'-') { return Err(LocalTimeTypeError("invalid characters in time zone designation")); } bytes[i + 1] = b; i += 1; } Ok(Self { bytes }) } /// Returns time zone designation as a byte slice #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] fn as_bytes(&self) -> &[u8] { match &self.bytes { [3, head @ .., _, _, _, _] => head, [4, head @ .., _, _, _] => head, [5, head @ .., _, _] => head, [6, head @ .., _] => head, [7, head @ ..] => head, _ => const_panic!(), // unreachable } } /// Returns time zone designation as a string #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] fn as_str(&self) -> &str { // SAFETY: ASCII is valid UTF-8 unsafe { str::from_utf8_unchecked(self.as_bytes()) } } /// Check if two time zone designations are equal #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] fn equal(&self, other: &Self) -> bool { u64::from_ne_bytes(self.bytes) == u64::from_ne_bytes(other.bytes) } } impl fmt::Debug for TzAsciiStr { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.as_str().fmt(f) } } /// Local time type associated to a time zone #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub struct LocalTimeType { /// Offset from UTC in seconds ut_offset: i32, /// Daylight Saving Time indicator is_dst: bool, /// Time zone designation time_zone_designation: Option, } impl LocalTimeType { /// Construct a local time type #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn new(ut_offset: i32, is_dst: bool, time_zone_designation: Option<&[u8]>) -> Result { if ut_offset == i32::MIN { return Err(LocalTimeTypeError("invalid UTC offset")); } let time_zone_designation = match time_zone_designation { None => None, Some(time_zone_designation) => match TzAsciiStr::new(time_zone_designation) { Err(error) => return Err(error), Ok(time_zone_designation) => Some(time_zone_designation), }, }; Ok(Self { ut_offset, is_dst, time_zone_designation }) } /// Construct the local time type associated to UTC #[inline] pub const fn utc() -> Self { Self { ut_offset: 0, is_dst: false, time_zone_designation: None } } /// Construct a local time type with the specified UTC offset in seconds #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn with_ut_offset(ut_offset: i32) -> Result { if ut_offset == i32::MIN { return Err(LocalTimeTypeError("invalid UTC offset")); } Ok(Self { ut_offset, is_dst: false, time_zone_designation: None }) } /// Returns offset from UTC in seconds #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn ut_offset(&self) -> i32 { self.ut_offset } /// Returns daylight saving time indicator #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn is_dst(&self) -> bool { self.is_dst } /// Returns time zone designation #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn time_zone_designation(&self) -> &str { match &self.time_zone_designation { Some(s) => s.as_str(), None => "", } } /// Check if two local time types are equal #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] fn equal(&self, other: &Self) -> bool { self.ut_offset == other.ut_offset && self.is_dst == other.is_dst && match (&self.time_zone_designation, &other.time_zone_designation) { (Some(x), Some(y)) => x.equal(y), (None, None) => true, _ => false, } } } /// Time zone #[cfg(feature = "alloc")] #[cfg_attr(docsrs, doc(cfg(feature = "alloc")))] #[derive(Debug, Clone, Eq, PartialEq)] pub struct TimeZone { /// List of transitions transitions: Vec, /// List of local time types (cannot be empty) local_time_types: Vec, /// List of leap seconds leap_seconds: Vec, /// Extra transition rule applicable after the last transition extra_rule: Option, } /// Reference to a time zone #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub struct TimeZoneRef<'a> { /// List of transitions transitions: &'a [Transition], /// List of local time types (cannot be empty) local_time_types: &'a [LocalTimeType], /// List of leap seconds leap_seconds: &'a [LeapSecond], /// Extra transition rule applicable after the last transition extra_rule: &'a Option, } impl<'a> TimeZoneRef<'a> { /// Construct a time zone reference #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn new( transitions: &'a [Transition], local_time_types: &'a [LocalTimeType], leap_seconds: &'a [LeapSecond], extra_rule: &'a Option, ) -> Result { let time_zone_ref = Self::new_unchecked(transitions, local_time_types, leap_seconds, extra_rule); if let Err(error) = time_zone_ref.check_inputs() { return Err(error); } Ok(time_zone_ref) } /// Construct the time zone reference associated to UTC #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn utc() -> Self { const UTC: LocalTimeType = LocalTimeType::utc(); Self { transitions: &[], local_time_types: &[UTC], leap_seconds: &[], extra_rule: &None } } /// Returns list of transitions #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn transitions(&self) -> &'a [Transition] { self.transitions } /// Returns list of local time types #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn local_time_types(&self) -> &'a [LocalTimeType] { self.local_time_types } /// Returns list of leap seconds #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn leap_seconds(&self) -> &'a [LeapSecond] { self.leap_seconds } /// Returns extra transition rule applicable after the last transition #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn extra_rule(&self) -> &'a Option { self.extra_rule } /// Find the local time type associated to the time zone at the specified Unix time in seconds #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn find_local_time_type(&self, unix_time: i64) -> Result<&'a LocalTimeType, FindLocalTimeTypeError> { let extra_rule = match self.transitions { [] => match self.extra_rule { Some(extra_rule) => extra_rule, None => return Ok(&self.local_time_types[0]), }, [.., last_transition] => { let unix_leap_time = match self.unix_time_to_unix_leap_time(unix_time) { Ok(unix_leap_time) => unix_leap_time, Err(OutOfRangeError(error)) => return Err(FindLocalTimeTypeError(error)), }; if unix_leap_time >= last_transition.unix_leap_time { match self.extra_rule { Some(extra_rule) => extra_rule, None => return Err(FindLocalTimeTypeError("no local time type is available for the specified timestamp")), } } else { let index = match binary_search_transitions(self.transitions, unix_leap_time) { Ok(x) => x + 1, Err(x) => x, }; let local_time_type_index = if index > 0 { self.transitions[index - 1].local_time_type_index } else { 0 }; return Ok(&self.local_time_types[local_time_type_index]); } } }; match extra_rule.find_local_time_type(unix_time) { Ok(local_time_type) => Ok(local_time_type), Err(OutOfRangeError(error)) => Err(FindLocalTimeTypeError(error)), } } /// Construct a reference to a time zone #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] fn new_unchecked( transitions: &'a [Transition], local_time_types: &'a [LocalTimeType], leap_seconds: &'a [LeapSecond], extra_rule: &'a Option, ) -> Self { Self { transitions, local_time_types, leap_seconds, extra_rule } } /// Check time zone inputs #[cfg_attr(feature = "const", const_fn::const_fn)] fn check_inputs(&self) -> Result<(), TimeZoneError> { use crate::constants::*; // Check local time types let local_time_types_size = self.local_time_types.len(); if local_time_types_size == 0 { return Err(TimeZoneError("list of local time types must not be empty")); } // Check transitions let mut i_transition = 0; while i_transition < self.transitions.len() { if self.transitions[i_transition].local_time_type_index >= local_time_types_size { return Err(TimeZoneError("invalid local time type index")); } if i_transition + 1 < self.transitions.len() && self.transitions[i_transition].unix_leap_time >= self.transitions[i_transition + 1].unix_leap_time { return Err(TimeZoneError("invalid transition")); } i_transition += 1; } // Check leap seconds if !(self.leap_seconds.is_empty() || self.leap_seconds[0].unix_leap_time >= 0 && self.leap_seconds[0].correction.saturating_abs() == 1) { return Err(TimeZoneError("invalid leap second")); } let min_interval = SECONDS_PER_28_DAYS - 1; let mut i_leap_second = 0; while i_leap_second < self.leap_seconds.len() { if i_leap_second + 1 < self.leap_seconds.len() { let x0 = &self.leap_seconds[i_leap_second]; let x1 = &self.leap_seconds[i_leap_second + 1]; let diff_unix_leap_time = x1.unix_leap_time.saturating_sub(x0.unix_leap_time); let abs_diff_correction = x1.correction.saturating_sub(x0.correction).saturating_abs(); if !(diff_unix_leap_time >= min_interval && abs_diff_correction == 1) { return Err(TimeZoneError("invalid leap second")); } } i_leap_second += 1; } // Check extra rule if let (Some(extra_rule), [.., last_transition]) = (&self.extra_rule, self.transitions) { let last_local_time_type = &self.local_time_types[last_transition.local_time_type_index]; let unix_time = match self.unix_leap_time_to_unix_time(last_transition.unix_leap_time) { Ok(unix_time) => unix_time, Err(OutOfRangeError(error)) => return Err(TimeZoneError(error)), }; let rule_local_time_type = match extra_rule.find_local_time_type(unix_time) { Ok(rule_local_time_type) => rule_local_time_type, Err(OutOfRangeError(error)) => return Err(TimeZoneError(error)), }; if !last_local_time_type.equal(rule_local_time_type) { return Err(TimeZoneError("extra transition rule is inconsistent with the last transition")); } } Ok(()) } /// Convert Unix time to Unix leap time, from the list of leap seconds in a time zone #[cfg_attr(feature = "const", const_fn::const_fn)] pub(crate) fn unix_time_to_unix_leap_time(&self, unix_time: i64) -> Result { let mut unix_leap_time = unix_time; let mut i = 0; while i < self.leap_seconds.len() { let leap_second = &self.leap_seconds[i]; if unix_leap_time < leap_second.unix_leap_time { break; } unix_leap_time = match unix_time.checked_add(leap_second.correction as i64) { Some(unix_leap_time) => unix_leap_time, None => return Err(OutOfRangeError("out of range operation")), }; i += 1; } Ok(unix_leap_time) } /// Convert Unix leap time to Unix time, from the list of leap seconds in a time zone #[cfg_attr(feature = "const", const_fn::const_fn)] pub(crate) fn unix_leap_time_to_unix_time(&self, unix_leap_time: i64) -> Result { if unix_leap_time == i64::MIN { return Err(OutOfRangeError("out of range operation")); } let index = match binary_search_leap_seconds(self.leap_seconds, unix_leap_time - 1) { Ok(x) => x + 1, Err(x) => x, }; let correction = if index > 0 { self.leap_seconds[index - 1].correction } else { 0 }; match unix_leap_time.checked_sub(correction as i64) { Some(unix_time) => Ok(unix_time), None => Err(OutOfRangeError("out of range operation")), } } } #[cfg(feature = "alloc")] impl TimeZone { /// Construct a time zone pub fn new( transitions: Vec, local_time_types: Vec, leap_seconds: Vec, extra_rule: Option, ) -> Result { TimeZoneRef::new_unchecked(&transitions, &local_time_types, &leap_seconds, &extra_rule).check_inputs()?; Ok(Self { transitions, local_time_types, leap_seconds, extra_rule }) } /// Returns a reference to the time zone #[inline] pub fn as_ref(&self) -> TimeZoneRef { TimeZoneRef::new_unchecked(&self.transitions, &self.local_time_types, &self.leap_seconds, &self.extra_rule) } /// Construct the time zone associated to UTC #[inline] pub fn utc() -> Self { Self { transitions: Vec::new(), local_time_types: vec![LocalTimeType::utc()], leap_seconds: Vec::new(), extra_rule: None } } /// Construct a time zone with the specified UTC offset in seconds #[inline] pub fn fixed(ut_offset: i32) -> Result { Ok(Self { transitions: Vec::new(), local_time_types: vec![LocalTimeType::with_ut_offset(ut_offset)?], leap_seconds: Vec::new(), extra_rule: None }) } /// Returns local time zone. /// /// This method in not supported on non-UNIX platforms, and returns the UTC time zone instead. /// #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] pub fn local() -> Result { #[cfg(not(unix))] let local_time_zone = Self::utc(); #[cfg(unix)] let local_time_zone = Self::from_posix_tz("localtime")?; Ok(local_time_zone) } /// Construct a time zone from the contents of a time zone file #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] pub fn from_tz_data(bytes: &[u8]) -> Result { crate::parse::parse_tz_file(bytes) } /// 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). #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] pub fn from_posix_tz(tz_string: &str) -> Result { use crate::parse::*; use std::fs::{self, File}; use std::io::{self, Read}; if tz_string.is_empty() { return Err(TzError::TzStringError(TzStringError::InvalidTzString("empty TZ string"))); } if tz_string == "localtime" { return parse_tz_file(&fs::read("/etc/localtime")?); } let read = |mut file: File| -> io::Result<_> { let mut bytes = Vec::new(); file.read_to_end(&mut bytes)?; Ok(bytes) }; let mut chars = tz_string.chars(); if chars.next() == Some(':') { return parse_tz_file(&read(get_tz_file(chars.as_str())?)?); } match get_tz_file(tz_string) { Ok(file) => parse_tz_file(&read(file)?), Err(_) => { let tz_string = tz_string.trim_matches(|c: char| c.is_ascii_whitespace()); // TZ string extensions are not allowed let rule = parse_posix_tz(tz_string.as_bytes(), false)?; let local_time_types = match rule { TransitionRule::Fixed(local_time_type) => vec![local_time_type], TransitionRule::Alternate(alternate_time) => vec![*alternate_time.std(), *alternate_time.dst()], }; Ok(TimeZone::new(vec![], local_time_types, vec![], Some(rule))?) } } } /// Find the current local time type associated to the time zone #[cfg(feature = "std")] #[cfg_attr(docsrs, doc(cfg(feature = "std")))] pub fn find_current_local_time_type(&self) -> Result<&LocalTimeType, TzError> { use core::convert::TryInto; use std::time::SystemTime; Ok(self.find_local_time_type(SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?.as_secs().try_into()?)?) } /// Find the local time type associated to the time zone at the specified Unix time in seconds pub fn find_local_time_type(&self, unix_time: i64) -> Result<&LocalTimeType, FindLocalTimeTypeError> { self.as_ref().find_local_time_type(unix_time) } } #[cfg(test)] mod test { use super::*; use crate::Result; #[test] fn test_tz_ascii_str() -> Result<()> { assert!(matches!(TzAsciiStr::new(b""), Err(LocalTimeTypeError(_)))); assert!(matches!(TzAsciiStr::new(b"1"), Err(LocalTimeTypeError(_)))); assert!(matches!(TzAsciiStr::new(b"12"), Err(LocalTimeTypeError(_)))); assert_eq!(TzAsciiStr::new(b"123")?.as_bytes(), b"123"); assert_eq!(TzAsciiStr::new(b"1234")?.as_bytes(), b"1234"); assert_eq!(TzAsciiStr::new(b"12345")?.as_bytes(), b"12345"); assert_eq!(TzAsciiStr::new(b"123456")?.as_bytes(), b"123456"); assert_eq!(TzAsciiStr::new(b"1234567")?.as_bytes(), b"1234567"); assert!(matches!(TzAsciiStr::new(b"12345678"), Err(LocalTimeTypeError(_)))); assert!(matches!(TzAsciiStr::new(b"123456789"), Err(LocalTimeTypeError(_)))); assert!(matches!(TzAsciiStr::new(b"1234567890"), Err(LocalTimeTypeError(_)))); assert!(matches!(TzAsciiStr::new(b"123\0\0\0"), Err(LocalTimeTypeError(_)))); Ok(()) } #[cfg(feature = "alloc")] #[test] fn test_time_zone() -> Result<()> { let utc = LocalTimeType::utc(); let cet = LocalTimeType::with_ut_offset(3600)?; let utc_local_time_types = vec![utc]; let fixed_extra_rule = TransitionRule::Fixed(cet); let time_zone_1 = TimeZone::new(vec![], utc_local_time_types.clone(), vec![], None)?; let time_zone_2 = TimeZone::new(vec![], utc_local_time_types.clone(), vec![], Some(fixed_extra_rule))?; let time_zone_3 = TimeZone::new(vec![Transition::new(0, 0)], utc_local_time_types.clone(), vec![], None)?; 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))?; assert_eq!(*time_zone_1.find_local_time_type(0)?, utc); assert_eq!(*time_zone_2.find_local_time_type(0)?, cet); assert_eq!(*time_zone_3.find_local_time_type(-1)?, utc); assert!(matches!(time_zone_3.find_local_time_type(0), Err(FindLocalTimeTypeError(_)))); assert_eq!(*time_zone_4.find_local_time_type(-1)?, utc); assert_eq!(*time_zone_4.find_local_time_type(0)?, cet); let time_zone_err = TimeZone::new(vec![Transition::new(0, 0)], utc_local_time_types, vec![], Some(fixed_extra_rule)); assert!(time_zone_err.is_err()); Ok(()) } #[cfg(feature = "std")] #[test] fn test_time_zone_from_posix_tz() -> Result<()> { #[cfg(unix)] { let time_zone_local = TimeZone::local()?; let time_zone_local_1 = TimeZone::from_posix_tz("localtime")?; let time_zone_local_2 = TimeZone::from_posix_tz("/etc/localtime")?; let time_zone_local_3 = TimeZone::from_posix_tz(":/etc/localtime")?; assert_eq!(time_zone_local, time_zone_local_1); assert_eq!(time_zone_local, time_zone_local_2); assert_eq!(time_zone_local, time_zone_local_3); assert!(matches!(time_zone_local.find_current_local_time_type(), Ok(_) | Err(TzError::FindLocalTimeTypeError(_)))); let time_zone_utc = TimeZone::from_posix_tz("UTC")?; assert_eq!(time_zone_utc.find_local_time_type(0)?.ut_offset(), 0); } assert!(TimeZone::from_posix_tz("EST5EDT,0/0,J365/25").is_err()); assert!(TimeZone::from_posix_tz("").is_err()); Ok(()) } #[cfg(feature = "alloc")] #[test] fn test_leap_seconds() -> Result<()> { let time_zone = TimeZone::new( vec![], vec![LocalTimeType::new(0, false, Some(b"UTC"))?], vec![ LeapSecond::new(78796800, 1), LeapSecond::new(94694401, 2), LeapSecond::new(126230402, 3), LeapSecond::new(157766403, 4), LeapSecond::new(189302404, 5), LeapSecond::new(220924805, 6), LeapSecond::new(252460806, 7), LeapSecond::new(283996807, 8), LeapSecond::new(315532808, 9), LeapSecond::new(362793609, 10), LeapSecond::new(394329610, 11), LeapSecond::new(425865611, 12), LeapSecond::new(489024012, 13), LeapSecond::new(567993613, 14), LeapSecond::new(631152014, 15), LeapSecond::new(662688015, 16), LeapSecond::new(709948816, 17), LeapSecond::new(741484817, 18), LeapSecond::new(773020818, 19), LeapSecond::new(820454419, 20), LeapSecond::new(867715220, 21), LeapSecond::new(915148821, 22), LeapSecond::new(1136073622, 23), LeapSecond::new(1230768023, 24), LeapSecond::new(1341100824, 25), LeapSecond::new(1435708825, 26), LeapSecond::new(1483228826, 27), ], None, )?; let time_zone_ref = time_zone.as_ref(); assert!(matches!(time_zone_ref.unix_leap_time_to_unix_time(1136073621), Ok(1136073599))); assert!(matches!(time_zone_ref.unix_leap_time_to_unix_time(1136073622), Ok(1136073600))); assert!(matches!(time_zone_ref.unix_leap_time_to_unix_time(1136073623), Ok(1136073600))); assert!(matches!(time_zone_ref.unix_leap_time_to_unix_time(1136073624), Ok(1136073601))); assert!(matches!(time_zone_ref.unix_time_to_unix_leap_time(1136073599), Ok(1136073621))); assert!(matches!(time_zone_ref.unix_time_to_unix_leap_time(1136073600), Ok(1136073623))); assert!(matches!(time_zone_ref.unix_time_to_unix_leap_time(1136073601), Ok(1136073624))); Ok(()) } #[cfg(feature = "alloc")] #[test] fn test_leap_seconds_overflow() -> Result<()> { let time_zone_err = TimeZone::new( vec![Transition::new(i64::MIN, 0)], vec![LocalTimeType::utc()], vec![LeapSecond::new(0, 1)], Some(TransitionRule::Fixed(LocalTimeType::utc())), ); assert!(time_zone_err.is_err()); let time_zone = TimeZone::new(vec![Transition::new(i64::MAX, 0)], vec![LocalTimeType::utc()], vec![LeapSecond::new(0, 1)], None)?; assert!(matches!(time_zone.find_local_time_type(i64::MAX), Err(FindLocalTimeTypeError(_)))); Ok(()) } } tz-rs-0.6.14/src/timezone/rule.rs000064400000000000000000001356411046102023000147430ustar 00000000000000//! Types related to a time zone extra transition rule. use crate::constants::*; use crate::timezone::*; /// Informations needed for checking DST transition rules consistency, for a Julian day #[derive(Debug, PartialEq, Eq)] struct JulianDayCheckInfos { /// Offset in seconds from the start of a normal year start_normal_year_offset: i64, /// Offset in seconds from the end of a normal year end_normal_year_offset: i64, /// Offset in seconds from the start of a leap year start_leap_year_offset: i64, /// Offset in seconds from the end of a leap year end_leap_year_offset: i64, } /// Informations needed for checking DST transition rules consistency, for a day represented by a month, a month week and a week day #[derive(Debug, PartialEq, Eq)] struct MonthWeekDayCheckInfos { /// Possible offset range in seconds from the start of a normal year start_normal_year_offset_range: (i64, i64), /// Possible offset range in seconds from the end of a normal year end_normal_year_offset_range: (i64, i64), /// Possible offset range in seconds from the start of a leap year start_leap_year_offset_range: (i64, i64), /// Possible offset range in seconds from the end of a leap year end_leap_year_offset_range: (i64, i64), } /// Julian day in `[1, 365]`, without taking occasional February 29th into account, which is not referenceable #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub struct Julian1WithoutLeap(u16); impl Julian1WithoutLeap { /// Construct a transition rule day represented by a Julian day in `[1, 365]`, without taking occasional February 29th into account, which is not referenceable #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn new(julian_day_1: u16) -> Result { if !(1 <= julian_day_1 && julian_day_1 <= 365) { return Err(TransitionRuleError("invalid rule day julian day")); } Ok(Self(julian_day_1)) } /// Returns inner value #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn get(&self) -> u16 { self.0 } /// Compute transition date /// /// ## Outputs /// /// * `month`: Month in `[1, 12]` /// * `month_day`: Day of the month in `[1, 31]` /// #[cfg_attr(feature = "const", const_fn::const_fn)] fn transition_date(&self) -> (usize, i64) { let year_day = self.0 as i64; let month = match binary_search_i64(&CUMUL_DAYS_IN_MONTHS_NORMAL_YEAR, year_day - 1) { Ok(x) => x + 1, Err(x) => x, }; let month_day = year_day - CUMUL_DAYS_IN_MONTHS_NORMAL_YEAR[month - 1]; (month, month_day) } /// Compute the informations needed for checking DST transition rules consistency #[cfg_attr(feature = "const", const_fn::const_fn)] fn compute_check_infos(&self, utc_day_time: i64) -> JulianDayCheckInfos { let start_normal_year_offset = (self.0 as i64 - 1) * SECONDS_PER_DAY + utc_day_time; let start_leap_year_offset = if self.0 <= 59 { start_normal_year_offset } else { start_normal_year_offset + SECONDS_PER_DAY }; JulianDayCheckInfos { start_normal_year_offset, end_normal_year_offset: start_normal_year_offset - SECONDS_PER_NORMAL_YEAR, start_leap_year_offset, end_leap_year_offset: start_leap_year_offset - SECONDS_PER_LEAP_YEAR, } } } /// Zero-based Julian day in `[0, 365]`, taking occasional February 29th into account and allowing December 32nd #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub struct Julian0WithLeap(u16); impl Julian0WithLeap { /// 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 #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn new(julian_day_0: u16) -> Result { if julian_day_0 > 365 { return Err(TransitionRuleError("invalid rule day julian day")); } Ok(Self(julian_day_0)) } /// Returns inner value #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn get(&self) -> u16 { self.0 } /// Compute transition date. /// /// On a non-leap year, a value of `365` corresponds to December 32nd (which is January 1st of the next year). /// /// ## Outputs /// /// * `month`: Month in `[1, 12]` /// * `month_day`: Day of the month in `[1, 32]` /// #[cfg_attr(feature = "const", const_fn::const_fn)] fn transition_date(&self, leap_year: bool) -> (usize, i64) { let cumul_day_in_months = if leap_year { &CUMUL_DAYS_IN_MONTHS_LEAP_YEAR } else { &CUMUL_DAYS_IN_MONTHS_NORMAL_YEAR }; let year_day = self.0 as i64; let month = match binary_search_i64(cumul_day_in_months, year_day) { Ok(x) => x + 1, Err(x) => x, }; let month_day = 1 + year_day - cumul_day_in_months[month - 1]; (month, month_day) } /// Compute the informations needed for checking DST transition rules consistency #[cfg_attr(feature = "const", const_fn::const_fn)] fn compute_check_infos(&self, utc_day_time: i64) -> JulianDayCheckInfos { let start_year_offset = self.0 as i64 * SECONDS_PER_DAY + utc_day_time; JulianDayCheckInfos { start_normal_year_offset: start_year_offset, end_normal_year_offset: start_year_offset - SECONDS_PER_NORMAL_YEAR, start_leap_year_offset: start_year_offset, end_leap_year_offset: start_year_offset - SECONDS_PER_LEAP_YEAR, } } } /// Day represented by a month, a month week and a week day #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub struct MonthWeekDay { /// Month in `[1, 12]` month: u8, /// Week of the month in `[1, 5]`, with `5` representing the last week of the month week: u8, /// Day of the week in `[0, 6]` from Sunday week_day: u8, } impl MonthWeekDay { /// Construct a transition rule day represented by a month, a month week and a week day #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn new(month: u8, week: u8, week_day: u8) -> Result { if !(1 <= month && month <= 12) { return Err(TransitionRuleError("invalid rule day month")); } if !(1 <= week && week <= 5) { return Err(TransitionRuleError("invalid rule day week")); } if week_day > 6 { return Err(TransitionRuleError("invalid rule day week day")); } Ok(Self { month, week, week_day }) } /// Returns month in `[1, 12]` #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn month(&self) -> u8 { self.month } /// Returns week of the month in `[1, 5]`, with `5` representing the last week of the month #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn week(&self) -> u8 { self.week } /// Returns day of the week in `[0, 6]` from Sunday #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn week_day(&self) -> u8 { self.week_day } /// Compute transition date on a specific year /// /// ## Outputs /// /// * `month`: Month in `[1, 12]` /// * `month_day`: Day of the month in `[1, 31]` /// #[cfg_attr(feature = "const", const_fn::const_fn)] fn transition_date(&self, year: i32) -> (usize, i64) { let month = self.month as usize; let week = self.week as i64; let week_day = self.week_day as i64; let mut days_in_month = DAYS_IN_MONTHS_NORMAL_YEAR[month - 1]; if month == 2 { days_in_month += is_leap_year(year) as i64; } let week_day_of_first_month_day = (4 + days_since_unix_epoch(year, month, 1)).rem_euclid(DAYS_PER_WEEK); let first_week_day_occurence_in_month = 1 + (week_day as i64 - week_day_of_first_month_day).rem_euclid(DAYS_PER_WEEK); let mut month_day = first_week_day_occurence_in_month + (week as i64 - 1) * DAYS_PER_WEEK; if month_day > days_in_month { month_day -= DAYS_PER_WEEK } (month, month_day) } /// Compute the informations needed for checking DST transition rules consistency #[cfg_attr(feature = "const", const_fn::const_fn)] fn compute_check_infos(&self, utc_day_time: i64) -> MonthWeekDayCheckInfos { let month = self.month as usize; let week = self.week as i64; let (normal_year_month_day_range, leap_year_month_day_range) = { if week == 5 { let normal_year_days_in_month = DAYS_IN_MONTHS_NORMAL_YEAR[month - 1]; let leap_year_days_in_month = if month == 2 { normal_year_days_in_month + 1 } else { normal_year_days_in_month }; let normal_year_month_day_range = (normal_year_days_in_month - 6, normal_year_days_in_month); let leap_year_month_day_range = (leap_year_days_in_month - 6, leap_year_days_in_month); (normal_year_month_day_range, leap_year_month_day_range) } else { let month_day_range = (week * DAYS_PER_WEEK - 6, week * DAYS_PER_WEEK); (month_day_range, month_day_range) } }; let start_normal_year_offset_range = ( (CUMUL_DAYS_IN_MONTHS_NORMAL_YEAR[month - 1] + normal_year_month_day_range.0 - 1) * SECONDS_PER_DAY + utc_day_time, (CUMUL_DAYS_IN_MONTHS_NORMAL_YEAR[month - 1] + normal_year_month_day_range.1 - 1) * SECONDS_PER_DAY + utc_day_time, ); let start_leap_year_offset_range = ( (CUMUL_DAYS_IN_MONTHS_LEAP_YEAR[month - 1] + leap_year_month_day_range.0 - 1) * SECONDS_PER_DAY + utc_day_time, (CUMUL_DAYS_IN_MONTHS_LEAP_YEAR[month - 1] + leap_year_month_day_range.1 - 1) * SECONDS_PER_DAY + utc_day_time, ); MonthWeekDayCheckInfos { start_normal_year_offset_range, end_normal_year_offset_range: ( start_normal_year_offset_range.0 - SECONDS_PER_NORMAL_YEAR, start_normal_year_offset_range.1 - SECONDS_PER_NORMAL_YEAR, ), start_leap_year_offset_range, 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), } } } /// Transition rule day #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum RuleDay { /// Julian day in `[1, 365]`, without taking occasional February 29th into account, which is not referenceable Julian1WithoutLeap(Julian1WithoutLeap), /// Zero-based Julian day in `[0, 365]`, taking occasional February 29th into account and allowing December 32nd Julian0WithLeap(Julian0WithLeap), /// Day represented by a month, a month week and a week day MonthWeekDay(MonthWeekDay), } impl RuleDay { /// Compute transition date for the provided year. /// /// The December 32nd date is possible, which corresponds to January 1st of the next year. /// /// ## Outputs /// /// * `month`: Month in `[1, 12]` /// * `month_day`: Day of the month in `[1, 32]` /// #[cfg_attr(feature = "const", const_fn::const_fn)] fn transition_date(&self, year: i32) -> (usize, i64) { match self { Self::Julian1WithoutLeap(rule_day) => rule_day.transition_date(), Self::Julian0WithLeap(rule_day) => rule_day.transition_date(is_leap_year(year)), Self::MonthWeekDay(rule_day) => rule_day.transition_date(year), } } /// Returns the UTC Unix time in seconds associated to the transition date for the provided year #[cfg_attr(feature = "const", const_fn::const_fn)] pub(crate) fn unix_time(&self, year: i32, day_time_in_utc: i64) -> i64 { let (month, month_day) = self.transition_date(year); days_since_unix_epoch(year, month, month_day) * SECONDS_PER_DAY + day_time_in_utc } } /// Transition rule representing alternate local time types #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub struct AlternateTime { /// Local time type for standard time std: LocalTimeType, /// Local time type for Daylight Saving Time dst: LocalTimeType, /// Start day of Daylight Saving Time dst_start: RuleDay, /// Local start day time of Daylight Saving Time, in seconds dst_start_time: i32, /// End day of Daylight Saving Time dst_end: RuleDay, /// Local end day time of Daylight Saving Time, in seconds dst_end_time: i32, } impl AlternateTime { /// Construct a transition rule representing alternate local time types #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn new( std: LocalTimeType, dst: LocalTimeType, dst_start: RuleDay, dst_start_time: i32, dst_end: RuleDay, dst_end_time: i32, ) -> Result { let std_ut_offset = std.ut_offset as i64; let dst_ut_offset = dst.ut_offset as i64; // Limit UTC offset to POSIX-required range if !(-25 * SECONDS_PER_HOUR < std_ut_offset && std_ut_offset < 26 * SECONDS_PER_HOUR) { return Err(TransitionRuleError("invalid standard time UTC offset")); } if !(-25 * SECONDS_PER_HOUR < dst_ut_offset && dst_ut_offset < 26 * SECONDS_PER_HOUR) { return Err(TransitionRuleError("invalid Daylight Saving Time UTC offset")); } // Overflow is not possible if !((dst_start_time as i64).abs() < SECONDS_PER_WEEK && (dst_end_time as i64).abs() < SECONDS_PER_WEEK) { return Err(TransitionRuleError("invalid DST start or end time")); } // Check DST transition rules consistency if !check_dst_transition_rules_consistency(&std, &dst, dst_start, dst_start_time, dst_end, dst_end_time) { return Err(TransitionRuleError("DST transition rules are not consistent from one year to another")); } Ok(Self { std, dst, dst_start, dst_start_time, dst_end, dst_end_time }) } /// Returns local time type for standard time #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn std(&self) -> &LocalTimeType { &self.std } /// Returns local time type for Daylight Saving Time #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn dst(&self) -> &LocalTimeType { &self.dst } /// Returns start day of Daylight Saving Time #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn dst_start(&self) -> &RuleDay { &self.dst_start } /// Returns local start day time of Daylight Saving Time, in seconds #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn dst_start_time(&self) -> i32 { self.dst_start_time } /// Returns end day of Daylight Saving Time #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn dst_end(&self) -> &RuleDay { &self.dst_end } /// Returns local end day time of Daylight Saving Time, in seconds #[inline] #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn dst_end_time(&self) -> i32 { self.dst_end_time } /// Find the local time type associated to the alternate transition rule at the specified Unix time in seconds #[cfg_attr(feature = "const", const_fn::const_fn)] fn find_local_time_type(&self, unix_time: i64) -> Result<&LocalTimeType, OutOfRangeError> { // Overflow is not possible let dst_start_time_in_utc = self.dst_start_time as i64 - self.std.ut_offset as i64; let dst_end_time_in_utc = self.dst_end_time as i64 - self.dst.ut_offset as i64; let current_year = match UtcDateTime::from_timespec(unix_time, 0) { Ok(utc_date_time) => utc_date_time.year(), Err(error) => return Err(error), }; // Check if the current year is valid for the following computations if !(i32::MIN + 2 <= current_year && current_year <= i32::MAX - 2) { return Err(OutOfRangeError("out of range date time")); } let current_year_dst_start_unix_time = self.dst_start.unix_time(current_year, dst_start_time_in_utc); let current_year_dst_end_unix_time = self.dst_end.unix_time(current_year, dst_end_time_in_utc); // Check DST start/end Unix times for previous/current/next years to support for transition day times outside of [0h, 24h] range. // This is sufficient since the absolute value of DST start/end time in UTC is less than 2 weeks. // Moreover, inconsistent DST transition rules are not allowed, so there won't be additional transitions at the year boundary. let is_dst = match cmp(current_year_dst_start_unix_time, current_year_dst_end_unix_time) { Ordering::Less | Ordering::Equal => { if unix_time < current_year_dst_start_unix_time { let previous_year_dst_end_unix_time = self.dst_end.unix_time(current_year - 1, dst_end_time_in_utc); if unix_time < previous_year_dst_end_unix_time { let previous_year_dst_start_unix_time = self.dst_start.unix_time(current_year - 1, dst_start_time_in_utc); previous_year_dst_start_unix_time <= unix_time } else { false } } else if unix_time < current_year_dst_end_unix_time { true } else { let next_year_dst_start_unix_time = self.dst_start.unix_time(current_year + 1, dst_start_time_in_utc); if next_year_dst_start_unix_time <= unix_time { let next_year_dst_end_unix_time = self.dst_end.unix_time(current_year + 1, dst_end_time_in_utc); unix_time < next_year_dst_end_unix_time } else { false } } } Ordering::Greater => { if unix_time < current_year_dst_end_unix_time { let previous_year_dst_start_unix_time = self.dst_start.unix_time(current_year - 1, dst_start_time_in_utc); if unix_time < previous_year_dst_start_unix_time { let previous_year_dst_end_unix_time = self.dst_end.unix_time(current_year - 1, dst_end_time_in_utc); unix_time < previous_year_dst_end_unix_time } else { true } } else if unix_time < current_year_dst_start_unix_time { false } else { let next_year_dst_end_unix_time = self.dst_end.unix_time(current_year + 1, dst_end_time_in_utc); if next_year_dst_end_unix_time <= unix_time { let next_year_dst_start_unix_time = self.dst_start.unix_time(current_year + 1, dst_start_time_in_utc); next_year_dst_start_unix_time <= unix_time } else { true } } } }; if is_dst { Ok(&self.dst) } else { Ok(&self.std) } } } /// Transition rule #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum TransitionRule { /// Fixed local time type Fixed(LocalTimeType), /// Alternate local time types Alternate(AlternateTime), } impl TransitionRule { /// Find the local time type associated to the transition rule at the specified Unix time in seconds #[cfg_attr(feature = "const", const_fn::const_fn)] pub(super) fn find_local_time_type(&self, unix_time: i64) -> Result<&LocalTimeType, OutOfRangeError> { match self { Self::Fixed(local_time_type) => Ok(local_time_type), Self::Alternate(alternate_time) => alternate_time.find_local_time_type(unix_time), } } } /// Check DST transition rules consistency, which ensures that the DST start and end time are always in the same order. /// /// 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. /// #[cfg_attr(feature = "const", const_fn::const_fn)] fn check_dst_transition_rules_consistency( std: &LocalTimeType, dst: &LocalTimeType, dst_start: RuleDay, dst_start_time: i32, dst_end: RuleDay, dst_end_time: i32, ) -> bool { // Overflow is not possible let dst_start_time_in_utc = dst_start_time as i64 - std.ut_offset as i64; let dst_end_time_in_utc = dst_end_time as i64 - dst.ut_offset as i64; match (dst_start, dst_end) { (RuleDay::Julian1WithoutLeap(start_day), RuleDay::Julian1WithoutLeap(end_day)) => { check_two_julian_days(start_day.compute_check_infos(dst_start_time_in_utc), end_day.compute_check_infos(dst_end_time_in_utc)) } (RuleDay::Julian1WithoutLeap(start_day), RuleDay::Julian0WithLeap(end_day)) => { check_two_julian_days(start_day.compute_check_infos(dst_start_time_in_utc), end_day.compute_check_infos(dst_end_time_in_utc)) } (RuleDay::Julian0WithLeap(start_day), RuleDay::Julian1WithoutLeap(end_day)) => { check_two_julian_days(start_day.compute_check_infos(dst_start_time_in_utc), end_day.compute_check_infos(dst_end_time_in_utc)) } (RuleDay::Julian0WithLeap(start_day), RuleDay::Julian0WithLeap(end_day)) => { check_two_julian_days(start_day.compute_check_infos(dst_start_time_in_utc), end_day.compute_check_infos(dst_end_time_in_utc)) } (RuleDay::Julian1WithoutLeap(start_day), RuleDay::MonthWeekDay(end_day)) => { 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)) } (RuleDay::Julian0WithLeap(start_day), RuleDay::MonthWeekDay(end_day)) => { 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)) } (RuleDay::MonthWeekDay(start_day), RuleDay::Julian1WithoutLeap(end_day)) => { 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)) } (RuleDay::MonthWeekDay(start_day), RuleDay::Julian0WithLeap(end_day)) => { 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)) } (RuleDay::MonthWeekDay(start_day), RuleDay::MonthWeekDay(end_day)) => { check_two_month_week_days(start_day, dst_start_time_in_utc, end_day, dst_end_time_in_utc) } } } /// Check DST transition rules consistency for two Julian days #[cfg_attr(feature = "const", const_fn::const_fn)] fn check_two_julian_days(check_infos_1: JulianDayCheckInfos, check_infos_2: JulianDayCheckInfos) -> bool { // Check in same year let (before, after) = if check_infos_1.start_normal_year_offset <= check_infos_2.start_normal_year_offset && check_infos_1.start_leap_year_offset <= check_infos_2.start_leap_year_offset { (&check_infos_1, &check_infos_2) } else if check_infos_2.start_normal_year_offset <= check_infos_1.start_normal_year_offset && check_infos_2.start_leap_year_offset <= check_infos_1.start_leap_year_offset { (&check_infos_2, &check_infos_1) } else { return false; }; // Check in consecutive years if after.end_normal_year_offset <= before.start_normal_year_offset && after.end_normal_year_offset <= before.start_leap_year_offset && after.end_leap_year_offset <= before.start_normal_year_offset { return true; } if before.start_normal_year_offset <= after.end_normal_year_offset && before.start_leap_year_offset <= after.end_normal_year_offset && before.start_normal_year_offset <= after.end_leap_year_offset { return true; } false } /// Check DST transition rules consistency for a Julian day and a day represented by a month, a month week and a week day #[cfg_attr(feature = "const", const_fn::const_fn)] fn check_month_week_day_and_julian_day(check_infos_1: MonthWeekDayCheckInfos, check_infos_2: JulianDayCheckInfos) -> bool { // Check in same year, then in consecutive years if check_infos_2.start_normal_year_offset <= check_infos_1.start_normal_year_offset_range.0 && check_infos_2.start_leap_year_offset <= check_infos_1.start_leap_year_offset_range.0 { let (before, after) = (&check_infos_2, &check_infos_1); if after.end_normal_year_offset_range.1 <= before.start_normal_year_offset && after.end_normal_year_offset_range.1 <= before.start_leap_year_offset && after.end_leap_year_offset_range.1 <= before.start_normal_year_offset { return true; }; if before.start_normal_year_offset <= after.end_normal_year_offset_range.0 && before.start_leap_year_offset <= after.end_normal_year_offset_range.0 && before.start_normal_year_offset <= after.end_leap_year_offset_range.0 { return true; }; return false; } if check_infos_1.start_normal_year_offset_range.1 <= check_infos_2.start_normal_year_offset && check_infos_1.start_leap_year_offset_range.1 <= check_infos_2.start_leap_year_offset { let (before, after) = (&check_infos_1, &check_infos_2); if after.end_normal_year_offset <= before.start_normal_year_offset_range.0 && after.end_normal_year_offset <= before.start_leap_year_offset_range.0 && after.end_leap_year_offset <= before.start_normal_year_offset_range.0 { return true; } if before.start_normal_year_offset_range.1 <= after.end_normal_year_offset && before.start_leap_year_offset_range.1 <= after.end_normal_year_offset && before.start_normal_year_offset_range.1 <= after.end_leap_year_offset { return true; } return false; } false } /// Check DST transition rules consistency for two days represented by a month, a month week and a week day #[cfg_attr(feature = "const", const_fn::const_fn)] 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 { // Sort rule days let (month_week_day_before, utc_day_time_before, month_week_day_after, utc_day_time_after) = { let rem = (month_week_day_2.month as i64 - month_week_day_1.month as i64).rem_euclid(MONTHS_PER_YEAR); if rem == 0 { if month_week_day_1.week <= month_week_day_2.week { (month_week_day_1, utc_day_time_1, month_week_day_2, utc_day_time_2) } else { (month_week_day_2, utc_day_time_2, month_week_day_1, utc_day_time_1) } } else if rem == 1 { (month_week_day_1, utc_day_time_1, month_week_day_2, utc_day_time_2) } else if rem == MONTHS_PER_YEAR - 1 { (month_week_day_2, utc_day_time_2, month_week_day_1, utc_day_time_1) } else { // Months are not equal or consecutive, so rule days are separated by more than 3 weeks and cannot swap their order return true; } }; let month_before = month_week_day_before.month as usize; let week_before = month_week_day_before.week as i64; let week_day_before = month_week_day_before.week_day as i64; let month_after = month_week_day_after.month as usize; let week_after = month_week_day_after.week as i64; let week_day_after = month_week_day_after.week_day as i64; let (diff_days_min, diff_days_max) = if week_day_before == week_day_after { // Rule days are separated by a whole number of weeks let (diff_week_min, diff_week_max) = match (week_before, week_after) { // All months have more than 29 days on a leap year, so the 5th week is non-empty (1..=4, 5) if month_before == month_after => (4 - week_before, 5 - week_before), (1..=4, 1..=4) if month_before != month_after => (4 - week_before + week_after, 5 - week_before + week_after), _ => return true, // rule days are synchronized or separated by more than 3 weeks }; (diff_week_min * DAYS_PER_WEEK, diff_week_max * DAYS_PER_WEEK) } else { // week_day_before != week_day_after let n = (week_day_after - week_day_before).rem_euclid(DAYS_PER_WEEK); // n >= 1 if month_before == month_after { match (week_before, week_after) { (5, 5) => (n - DAYS_PER_WEEK, n), (1..=4, 1..=4) => (n + DAYS_PER_WEEK * (week_after - week_before - 1), n + DAYS_PER_WEEK * (week_after - week_before)), (1..=4, 5) => { // For February month: // * On a normal year, we have n > (days_in_month % DAYS_PER_WEEK). // * On a leap year, we have n >= (days_in_month % DAYS_PER_WEEK). // // Since we want to check all possible years at the same time, checking only non-leap year is enough. let days_in_month = DAYS_IN_MONTHS_NORMAL_YEAR[month_before - 1]; match cmp(n, days_in_month % DAYS_PER_WEEK) { Ordering::Less => (n + DAYS_PER_WEEK * (4 - week_before), n + DAYS_PER_WEEK * (5 - week_before)), Ordering::Equal => return true, // rule days are synchronized Ordering::Greater => (n + DAYS_PER_WEEK * (3 - week_before), n + DAYS_PER_WEEK * (4 - week_before)), } } _ => const_panic!(), // unreachable } } else { // month_before != month_after match (week_before, week_after) { (1..=4, 1..=4) => { // Same as above let days_in_month = DAYS_IN_MONTHS_NORMAL_YEAR[month_before - 1]; match cmp(n, days_in_month % DAYS_PER_WEEK) { Ordering::Less => (n + DAYS_PER_WEEK * (4 - week_before + week_after), n + DAYS_PER_WEEK * (5 - week_before + week_after)), Ordering::Equal => return true, // rule days are synchronized Ordering::Greater => (n + DAYS_PER_WEEK * (3 - week_before + week_after), n + DAYS_PER_WEEK * (4 - week_before + week_after)), } } (5, 1..=4) => (n + DAYS_PER_WEEK * (week_after - 1), n + DAYS_PER_WEEK * week_after), _ => return true, // rule days are separated by more than 3 weeks } } }; let diff_days_seconds_min = diff_days_min * SECONDS_PER_DAY; let diff_days_seconds_max = diff_days_max * SECONDS_PER_DAY; // Check possible order swap of rule days utc_day_time_before <= diff_days_seconds_min + utc_day_time_after || diff_days_seconds_max + utc_day_time_after <= utc_day_time_before } #[cfg(test)] mod test { use super::*; use crate::Result; #[test] fn test_compute_check_infos() -> Result<()> { let check_julian = |check_infos: JulianDayCheckInfos, start_normal, end_normal, start_leap, end_leap| { assert_eq!(check_infos.start_normal_year_offset, start_normal); assert_eq!(check_infos.end_normal_year_offset, end_normal); assert_eq!(check_infos.start_leap_year_offset, start_leap); assert_eq!(check_infos.end_leap_year_offset, end_leap); }; let check_mwd = |check_infos: MonthWeekDayCheckInfos, start_normal, end_normal, start_leap, end_leap| { assert_eq!(check_infos.start_normal_year_offset_range, start_normal); assert_eq!(check_infos.end_normal_year_offset_range, end_normal); assert_eq!(check_infos.start_leap_year_offset_range, start_leap); assert_eq!(check_infos.end_leap_year_offset_range, end_leap); }; check_julian(Julian1WithoutLeap::new(1)?.compute_check_infos(1), 1, -31535999, 1, -31622399); check_julian(Julian1WithoutLeap::new(365)?.compute_check_infos(1), 31449601, -86399, 31536001, -86399); check_julian(Julian0WithLeap::new(0)?.compute_check_infos(1), 1, -31535999, 1, -31622399); check_julian(Julian0WithLeap::new(365)?.compute_check_infos(1), 31536001, 1, 31536001, -86399); check_mwd(MonthWeekDay::new(1, 1, 0)?.compute_check_infos(1), (1, 518401), (-31535999, -31017599), (1, 518401), (-31622399, -31103999)); check_mwd(MonthWeekDay::new(1, 5, 0)?.compute_check_infos(1), (2073601, 2592001), (-29462399, -28943999), (2073601, 2592001), (-29548799, -29030399)); check_mwd(MonthWeekDay::new(2, 4, 0)?.compute_check_infos(1), (4492801, 5011201), (-27043199, -26524799), (4492801, 5011201), (-27129599, -26611199)); check_mwd(MonthWeekDay::new(2, 5, 0)?.compute_check_infos(1), (4492801, 5011201), (-27043199, -26524799), (4579201, 5097601), (-27043199, -26524799)); check_mwd(MonthWeekDay::new(3, 1, 0)?.compute_check_infos(1), (5097601, 5616001), (-26438399, -25919999), (5184001, 5702401), (-26438399, -25919999)); check_mwd(MonthWeekDay::new(3, 5, 0)?.compute_check_infos(1), (7171201, 7689601), (-24364799, -23846399), (7257601, 7776001), (-24364799, -23846399)); check_mwd(MonthWeekDay::new(12, 5, 0)?.compute_check_infos(1), (30931201, 31449601), (-604799, -86399), (31017601, 31536001), (-604799, -86399)); Ok(()) } #[test] fn test_check_dst_transition_rules_consistency() -> Result<()> { let utc = LocalTimeType::utc(); let julian_1 = |year_day| Result::Ok(RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(year_day)?)); let julian_0 = |year_day| Result::Ok(RuleDay::Julian0WithLeap(Julian0WithLeap::new(year_day)?)); let mwd = |month, week, week_day| Result::Ok(RuleDay::MonthWeekDay(MonthWeekDay::new(month, week, week_day)?)); let check = |dst_start, dst_start_time, dst_end, dst_end_time| { let check_1 = check_dst_transition_rules_consistency(&utc, &utc, dst_start, dst_start_time, dst_end, dst_end_time); let check_2 = check_dst_transition_rules_consistency(&utc, &utc, dst_end, dst_end_time, dst_start, dst_start_time); assert_eq!(check_1, check_2); check_1 }; let check_all = |dst_start, dst_start_times: &[i32], dst_end, dst_end_time, results: &[bool]| { assert_eq!(dst_start_times.len(), results.len()); for (&dst_start_time, &result) in dst_start_times.iter().zip(results) { assert_eq!(check(dst_start, dst_start_time, dst_end, dst_end_time), result); } }; const DAY_1: i32 = 86400; const DAY_2: i32 = 2 * DAY_1; const DAY_3: i32 = 3 * DAY_1; const DAY_4: i32 = 4 * DAY_1; const DAY_5: i32 = 5 * DAY_1; const DAY_6: i32 = 6 * DAY_1; check_all(julian_1(59)?, &[-1, 0, 1], julian_1(60)?, -DAY_1, &[true, true, false]); check_all(julian_1(365)?, &[-1, 0, 1], julian_1(1)?, -DAY_1, &[true, true, true]); check_all(julian_0(58)?, &[-1, 0, 1], julian_0(59)?, -DAY_1, &[true, true, true]); check_all(julian_0(364)?, &[-1, 0, 1], julian_0(0)?, -DAY_1, &[true, true, false]); check_all(julian_0(365)?, &[-1, 0, 1], julian_0(0)?, 0, &[true, true, false]); check_all(julian_1(90)?, &[-1, 0, 1], julian_0(90)?, 0, &[true, true, false]); check_all(julian_1(365)?, &[-1, 0, 1], julian_0(0)?, 0, &[true, true, true]); check_all(julian_0(89)?, &[-1, 0, 1], julian_1(90)?, 0, &[true, true, false]); check_all(julian_0(364)?, &[-1, 0, 1], julian_1(1)?, -DAY_1, &[true, true, false]); check_all(julian_0(365)?, &[-1, 0, 1], julian_1(1)?, 0, &[true, true, false]); check_all(mwd(1, 4, 0)?, &[-1, 0, 1], julian_1(28)?, 0, &[true, true, false]); check_all(mwd(2, 5, 0)?, &[-1, 0, 1], julian_1(60)?, -DAY_1, &[true, true, false]); check_all(mwd(12, 5, 0)?, &[-1, 0, 1], julian_1(1)?, -DAY_1, &[true, true, false]); check_all(mwd(12, 5, 0)?, &[DAY_3 - 1, DAY_3, DAY_3 + 1], julian_1(1)?, -DAY_4, &[false, true, true]); check_all(mwd(1, 4, 0)?, &[-1, 0, 1], julian_0(27)?, 0, &[true, true, false]); check_all(mwd(2, 5, 0)?, &[-1, 0, 1], julian_0(58)?, DAY_1, &[true, true, false]); check_all(mwd(2, 4, 0)?, &[-1, 0, 1], julian_0(59)?, -DAY_1, &[true, true, false]); check_all(mwd(2, 5, 0)?, &[-1, 0, 1], julian_0(59)?, 0, &[true, true, false]); check_all(mwd(12, 5, 0)?, &[-1, 0, 1], julian_0(0)?, -DAY_1, &[true, true, false]); check_all(mwd(12, 5, 0)?, &[DAY_3 - 1, DAY_3, DAY_3 + 1], julian_0(0)?, -DAY_4, &[false, true, true]); check_all(julian_1(1)?, &[-1, 0, 1], mwd(1, 1, 0)?, 0, &[true, true, false]); check_all(julian_1(53)?, &[-1, 0, 1], mwd(2, 5, 0)?, 0, &[true, true, false]); check_all(julian_1(365)?, &[-1, 0, 1], mwd(1, 1, 0)?, -DAY_1, &[true, true, false]); check_all(julian_1(365)?, &[DAY_3 - 1, DAY_3, DAY_3 + 1], mwd(1, 1, 0)?, -DAY_4, &[false, true, true]); check_all(julian_0(0)?, &[-1, 0, 1], mwd(1, 1, 0)?, 0, &[true, true, false]); check_all(julian_0(52)?, &[-1, 0, 1], mwd(2, 5, 0)?, 0, &[true, true, false]); check_all(julian_0(59)?, &[-1, 0, 1], mwd(3, 1, 0)?, 0, &[true, true, false]); check_all(julian_0(59)?, &[-DAY_3 - 1, -DAY_3, -DAY_3 + 1], mwd(2, 5, 0)?, DAY_4, &[true, true, false]); check_all(julian_0(364)?, &[-1, 0, 1], mwd(1, 1, 0)?, -DAY_1, &[true, true, false]); check_all(julian_0(365)?, &[-1, 0, 1], mwd(1, 1, 0)?, 0, &[true, true, false]); check_all(julian_0(364)?, &[DAY_4 - 1, DAY_4, DAY_4 + 1], mwd(1, 1, 0)?, -DAY_4, &[false, true, true]); check_all(julian_0(365)?, &[DAY_3 - 1, DAY_3, DAY_3 + 1], mwd(1, 1, 0)?, -DAY_4, &[false, true, true]); let months_per_year = MONTHS_PER_YEAR as u8; for i in 0..months_per_year - 1 { let month = i + 1; let month_1 = (i + 1) % months_per_year + 1; let month_2 = (i + 2) % months_per_year + 1; assert!(check(mwd(month, 1, 0)?, 0, mwd(month_2, 1, 0)?, 0)); assert!(check(mwd(month, 3, 0)?, DAY_4, mwd(month, 4, 0)?, -DAY_3)); check_all(mwd(month, 5, 0)?, &[-1, 0, 1], mwd(month, 5, 0)?, 0, &[true, true, true]); check_all(mwd(month, 4, 0)?, &[-1, 0, 1], mwd(month, 5, 0)?, 0, &[true, true, false]); check_all(mwd(month, 4, 0)?, &[DAY_4 - 1, DAY_4, DAY_4 + 1], mwd(month_1, 1, 0)?, -DAY_3, &[true, true, false]); check_all(mwd(month, 5, 0)?, &[DAY_4 - 1, DAY_4, DAY_4 + 1], mwd(month_1, 1, 0)?, -DAY_3, &[true, true, true]); check_all(mwd(month, 5, 0)?, &[-1, 0, 1], mwd(month_1, 5, 0)?, 0, &[true, true, true]); check_all(mwd(month, 3, 2)?, &[-1, 0, 1], mwd(month, 4, 3)?, -DAY_1, &[true, true, false]); check_all(mwd(month, 5, 2)?, &[-1, 0, 1], mwd(month, 5, 3)?, -DAY_1, &[false, true, true]); check_all(mwd(month, 5, 2)?, &[-1, 0, 1], mwd(month_1, 1, 3)?, -DAY_1, &[true, true, false]); check_all(mwd(month, 5, 2)?, &[-1, 0, 1], mwd(month_1, 5, 3)?, 0, &[true, true, true]); } check_all(mwd(2, 4, 2)?, &[-1, 0, 1], mwd(2, 5, 3)?, -DAY_1, &[false, true, true]); check_all(mwd(3, 4, 2)?, &[-1, 0, 1], mwd(3, 5, 4)?, -DAY_2, &[true, true, false]); check_all(mwd(3, 4, 2)?, &[-1, 0, 1], mwd(3, 5, 5)?, -DAY_3, &[true, true, true]); check_all(mwd(3, 4, 2)?, &[-1, 0, 1], mwd(3, 5, 6)?, -DAY_4, &[false, true, true]); check_all(mwd(4, 4, 2)?, &[-1, 0, 1], mwd(4, 5, 3)?, -DAY_1, &[true, true, false]); check_all(mwd(4, 4, 2)?, &[-1, 0, 1], mwd(4, 5, 4)?, -DAY_2, &[true, true, true]); check_all(mwd(4, 4, 2)?, &[-1, 0, 1], mwd(4, 5, 5)?, -DAY_3, &[false, true, true]); check_all(mwd(2, 4, 2)?, &[DAY_5 - 1, DAY_5, DAY_5 + 1], mwd(3, 1, 3)?, -DAY_3, &[false, true, true]); check_all(mwd(3, 4, 2)?, &[DAY_5 - 1, DAY_5, DAY_5 + 1], mwd(4, 1, 4)?, -DAY_4, &[true, true, false]); check_all(mwd(3, 4, 2)?, &[DAY_5 - 1, DAY_5, DAY_5 + 1], mwd(4, 1, 5)?, -DAY_5, &[true, true, true]); check_all(mwd(3, 4, 2)?, &[DAY_5 - 1, DAY_5, DAY_5 + 1], mwd(4, 1, 6)?, -DAY_6, &[false, true, true]); check_all(mwd(4, 4, 2)?, &[DAY_5 - 1, DAY_5, DAY_5 + 1], mwd(5, 1, 3)?, -DAY_3, &[true, true, false]); check_all(mwd(4, 4, 2)?, &[DAY_5 - 1, DAY_5, DAY_5 + 1], mwd(5, 1, 4)?, -DAY_4, &[true, true, true]); check_all(mwd(4, 4, 2)?, &[DAY_5 - 1, DAY_5, DAY_5 + 1], mwd(5, 1, 5)?, -DAY_5, &[false, true, true]); Ok(()) } #[test] fn test_rule_day() -> Result<()> { let rule_day_j1 = RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(60)?); assert_eq!(rule_day_j1.transition_date(2000), (3, 1)); assert_eq!(rule_day_j1.transition_date(2001), (3, 1)); assert_eq!(rule_day_j1.unix_time(2000, 43200), 951912000); let rule_day_j0 = RuleDay::Julian0WithLeap(Julian0WithLeap::new(59)?); assert_eq!(rule_day_j0.transition_date(2000), (2, 29)); assert_eq!(rule_day_j0.transition_date(2001), (3, 1)); assert_eq!(rule_day_j0.unix_time(2000, 43200), 951825600); let rule_day_j0_max = RuleDay::Julian0WithLeap(Julian0WithLeap::new(365)?); assert_eq!(rule_day_j0_max.transition_date(2000), (12, 31)); assert_eq!(rule_day_j0_max.transition_date(2001), (12, 32)); assert_eq!( RuleDay::Julian0WithLeap(Julian0WithLeap::new(365)?).unix_time(2000, 0), RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(365)?).unix_time(2000, 0) ); assert_eq!( RuleDay::Julian0WithLeap(Julian0WithLeap::new(365)?).unix_time(1999, 0), RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(1)?).unix_time(2000, 0), ); let rule_day_mwd = RuleDay::MonthWeekDay(MonthWeekDay::new(2, 5, 2)?); assert_eq!(rule_day_mwd.transition_date(2000), (2, 29)); assert_eq!(rule_day_mwd.transition_date(2001), (2, 27)); assert_eq!(rule_day_mwd.unix_time(2000, 43200), 951825600); assert_eq!(rule_day_mwd.unix_time(2001, 43200), 983275200); Ok(()) } #[test] fn test_transition_rule() -> Result<()> { let transition_rule_fixed = TransitionRule::Fixed(LocalTimeType::new(-36000, false, None)?); assert_eq!(transition_rule_fixed.find_local_time_type(0)?.ut_offset(), -36000); let transition_rule_dst = TransitionRule::Alternate(AlternateTime::new( LocalTimeType::new(43200, false, Some(b"NZST"))?, LocalTimeType::new(46800, true, Some(b"NZDT"))?, RuleDay::MonthWeekDay(MonthWeekDay::new(10, 1, 0)?), 7200, RuleDay::MonthWeekDay(MonthWeekDay::new(3, 3, 0)?), 7200, )?); assert_eq!(transition_rule_dst.find_local_time_type(953384399)?.ut_offset(), 46800); assert_eq!(transition_rule_dst.find_local_time_type(953384400)?.ut_offset(), 43200); assert_eq!(transition_rule_dst.find_local_time_type(970322399)?.ut_offset(), 43200); assert_eq!(transition_rule_dst.find_local_time_type(970322400)?.ut_offset(), 46800); let transition_rule_negative_dst = TransitionRule::Alternate(AlternateTime::new( LocalTimeType::new(3600, false, Some(b"IST"))?, LocalTimeType::new(0, true, Some(b"GMT"))?, RuleDay::MonthWeekDay(MonthWeekDay::new(10, 5, 0)?), 7200, RuleDay::MonthWeekDay(MonthWeekDay::new(3, 5, 0)?), 3600, )?); assert_eq!(transition_rule_negative_dst.find_local_time_type(954032399)?.ut_offset(), 0); assert_eq!(transition_rule_negative_dst.find_local_time_type(954032400)?.ut_offset(), 3600); assert_eq!(transition_rule_negative_dst.find_local_time_type(972781199)?.ut_offset(), 3600); assert_eq!(transition_rule_negative_dst.find_local_time_type(972781200)?.ut_offset(), 0); let transition_rule_negative_time_1 = TransitionRule::Alternate(AlternateTime::new( LocalTimeType::new(0, false, None)?, LocalTimeType::new(0, true, None)?, RuleDay::Julian0WithLeap(Julian0WithLeap::new(100)?), 0, RuleDay::Julian0WithLeap(Julian0WithLeap::new(101)?), -86500, )?); assert!(transition_rule_negative_time_1.find_local_time_type(8639899)?.is_dst()); assert!(!transition_rule_negative_time_1.find_local_time_type(8639900)?.is_dst()); assert!(!transition_rule_negative_time_1.find_local_time_type(8639999)?.is_dst()); assert!(transition_rule_negative_time_1.find_local_time_type(8640000)?.is_dst()); let transition_rule_negative_time_2 = TransitionRule::Alternate(AlternateTime::new( LocalTimeType::new(-10800, false, Some(b"-03"))?, LocalTimeType::new(-7200, true, Some(b"-02"))?, RuleDay::MonthWeekDay(MonthWeekDay::new(3, 5, 0)?), -7200, RuleDay::MonthWeekDay(MonthWeekDay::new(10, 5, 0)?), -3600, )?); assert_eq!(transition_rule_negative_time_2.find_local_time_type(954032399)?.ut_offset(), -10800); assert_eq!(transition_rule_negative_time_2.find_local_time_type(954032400)?.ut_offset(), -7200); assert_eq!(transition_rule_negative_time_2.find_local_time_type(972781199)?.ut_offset(), -7200); assert_eq!(transition_rule_negative_time_2.find_local_time_type(972781200)?.ut_offset(), -10800); let transition_rule_all_year_dst = TransitionRule::Alternate(AlternateTime::new( LocalTimeType::new(-18000, false, Some(b"EST"))?, LocalTimeType::new(-14400, true, Some(b"EDT"))?, RuleDay::Julian0WithLeap(Julian0WithLeap::new(0)?), 0, RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(365)?), 90000, )?); assert_eq!(transition_rule_all_year_dst.find_local_time_type(946702799)?.ut_offset(), -14400); assert_eq!(transition_rule_all_year_dst.find_local_time_type(946702800)?.ut_offset(), -14400); Ok(()) } #[test] fn test_transition_rule_overflow() -> Result<()> { let transition_rule_1 = TransitionRule::Alternate(AlternateTime::new( LocalTimeType::new(-1, false, None)?, LocalTimeType::new(-1, true, None)?, RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(365)?), 0, RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(1)?), 0, )?); let transition_rule_2 = TransitionRule::Alternate(AlternateTime::new( LocalTimeType::new(1, false, None)?, LocalTimeType::new(1, true, None)?, RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(365)?), 0, RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(1)?), 0, )?); assert!(matches!(transition_rule_1.find_local_time_type(i64::MIN), Err(OutOfRangeError(_)))); assert!(matches!(transition_rule_2.find_local_time_type(i64::MAX), Err(OutOfRangeError(_)))); Ok(()) } } tz-rs-0.6.14/src/utils/const_fns.rs000064400000000000000000000061001046102023000152610ustar 00000000000000//! Some useful constant functions. use crate::error::OutOfRangeError; use crate::timezone::{LeapSecond, Transition}; use core::cmp::Ordering; /// Compare two values #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn cmp(a: i64, b: i64) -> Ordering { if a < b { Ordering::Less } else if a == b { Ordering::Equal } else { Ordering::Greater } } /// Returns the minimum of two values #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn min(a: i64, b: i64) -> i64 { match cmp(a, b) { Ordering::Less | Ordering::Equal => a, Ordering::Greater => b, } } /// Macro for implementing integer conversion macro_rules! impl_try_into_integer { ($from_type:ty, $to_type:ty, $value:expr) => {{ let min = <$to_type>::MIN as $from_type; let max = <$to_type>::MAX as $from_type; if min <= $value && $value <= max { Ok($value as $to_type) } else { Err(OutOfRangeError("out of range integer conversion")) } }}; } /// Convert a `i64` value to a `i32` value #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn try_into_i32(value: i64) -> Result { impl_try_into_integer!(i64, i32, value) } /// Convert a `i128` value to a `i64` value #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn try_into_i64(value: i128) -> Result { impl_try_into_integer!(i128, i64, value) } /// Macro for implementing binary search macro_rules! impl_binary_search { ($slice:expr, $f:expr, $x:expr) => {{ let mut size = $slice.len(); let mut left = 0; let mut right = size; while left < right { let mid = left + size / 2; let v = $f(&$slice[mid]); if v < $x { left = mid + 1; } else if v > $x { right = mid; } else { return Ok(mid); } size = right - left; } Err(left) }}; } /// Copy the input value #[cfg_attr(feature = "const", const_fn::const_fn)] fn copied(x: &i64) -> i64 { *x } /// Binary searches a sorted `i64` slice for the given element #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn binary_search_i64(slice: &[i64], x: i64) -> Result { impl_binary_search!(slice, copied, x) } /// Binary searches a sorted `Transition` slice for the given element #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn binary_search_transitions(slice: &[Transition], x: i64) -> Result { impl_binary_search!(slice, Transition::unix_leap_time, x) } /// Binary searches a sorted `LeapSecond` slice for the given element #[cfg_attr(feature = "const", const_fn::const_fn)] pub fn binary_search_leap_seconds(slice: &[LeapSecond], x: i64) -> Result { impl_binary_search!(slice, LeapSecond::unix_leap_time, x) } /// Macro for panicking in a const context macro_rules! const_panic { () => {{ #[allow(unconditional_panic)] let panic = [][0]; panic }}; } pub(crate) use const_panic; tz-rs-0.6.14/src/utils/mod.rs000064400000000000000000000002211046102023000140420ustar 00000000000000//! Some useful utilities. mod const_fns; #[cfg(feature = "std")] mod types; pub use const_fns::*; #[cfg(feature = "std")] pub use types::*; tz-rs-0.6.14/src/utils/types.rs000064400000000000000000000046421046102023000144420ustar 00000000000000//! Some useful types. use std::io::{Error, ErrorKind}; /// A `Cursor` contains a slice of a buffer and a read count. #[derive(Debug, Eq, PartialEq)] pub struct Cursor<'a> { /// Slice representing the remaining data to be read remaining: &'a [u8], /// Number of already read bytes read_count: usize, } impl<'a> Cursor<'a> { /// Construct a new `Cursor` from remaining data pub fn new(remaining: &'a [u8]) -> Self { Self { remaining, read_count: 0 } } /// Returns remaining data pub fn remaining(&self) -> &'a [u8] { self.remaining } /// Returns `true` if data is remaining pub fn is_empty(&self) -> bool { self.remaining.is_empty() } /// Read exactly `count` bytes, reducing remaining data and incrementing read count pub fn read_exact(&mut self, count: usize) -> Result<&'a [u8], Error> { match (self.remaining.get(..count), self.remaining.get(count..)) { (Some(result), Some(remaining)) => { self.remaining = remaining; self.read_count += count; Ok(result) } _ => Err(Error::from(ErrorKind::UnexpectedEof)), } } /// Read bytes and compare them to the provided tag pub fn read_tag(&mut self, tag: &[u8]) -> Result<(), Error> { if self.read_exact(tag.len())? == tag { Ok(()) } else { Err(Error::from(ErrorKind::InvalidData)) } } /// Read bytes if the remaining data is prefixed by the provided tag pub fn read_optional_tag(&mut self, tag: &[u8]) -> Result { if self.remaining.starts_with(tag) { self.read_exact(tag.len())?; Ok(true) } else { Ok(false) } } /// Read bytes as long as the provided predicate is true pub fn read_while bool>(&mut self, f: F) -> Result<&'a [u8], Error> { match self.remaining.iter().position(|x| !f(x)) { None => self.read_exact(self.remaining.len()), Some(position) => self.read_exact(position), } } /// Read bytes until the provided predicate is true pub fn read_until bool>(&mut self, f: F) -> Result<&'a [u8], Error> { match self.remaining.iter().position(f) { None => self.read_exact(self.remaining.len()), Some(position) => self.read_exact(position), } } }