tz-rs-0.7.3/.cargo_vcs_info.json0000644000000001360000000000100121220ustar { "git": { "sha1": "9f26398ae57cf638c6a9b1ae3e925e476ac9b991" }, "path_in_vcs": "" }tz-rs-0.7.3/.github/dependabot.yml000064400000000000000000000003161046102023000151020ustar 00000000000000version: 2 updates: - package-ecosystem: "cargo" directory: "/" schedule: interval: "daily" - package-ecosystem: "github-actions" directory: "/" schedule: interval: "daily" tz-rs-0.7.3/.github/workflows/audit.yml000064400000000000000000000007141046102023000161420ustar 00000000000000name: Audit on: schedule: - cron: "0 0 * * 0" jobs: audit: name: "Audit" runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 - 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.7.3/.github/workflows/ci.yml000064400000000000000000000016551046102023000154340ustar 00000000000000name: CI on: push: branches: ["master"] pull_request: branches: ["master"] schedule: - cron: "0 0 * * 0" jobs: doc: name: "Doc" runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 - name: Install Rust run: | rustup set profile minimal rustup toolchain install nightly rustup override set nightly - name: Doc run: RUSTDOCFLAGS="-D warnings --cfg docsrs" cargo doc --all-features --no-deps test: name: "Test" runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v6 - name: Install Rust run: | rustup set profile minimal rustup toolchain install 1.85 stable nightly --component clippy rustup override set stable - name: Test run: ./check_release.sh env: CARGO_NET_GIT_FETCH_WITH_CLI: true tz-rs-0.7.3/.gitignore000064400000000000000000000000321046102023000126750ustar 00000000000000.vscode Cargo.lock target tz-rs-0.7.3/Cargo.lock0000644000000002250000000000100100740ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 4 [[package]] name = "tz-rs" version = "0.7.3" tz-rs-0.7.3/Cargo.toml0000644000000025470000000000100101300ustar # 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 = "2024" rust-version = "1.85" name = "tz-rs" version = "0.7.3" authors = ["x-hgg-x"] build = false autolib = false autobins = false autoexamples = false autotests = false autobenches = false 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", ] [features] alloc = [] default = ["std"] std = ["alloc"] [lib] name = "tz" path = "src/lib.rs" [[example]] name = "settings" path = "examples/settings.rs" [[example]] name = "statics" path = "examples/statics.rs" [[example]] name = "time" path = "examples/time.rs" tz-rs-0.7.3/Cargo.toml.orig000064400000000000000000000010741046102023000136030ustar 00000000000000[package] name = "tz-rs" version = "0.7.3" edition = "2024" rust-version = "1.85" 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"] [features] default = ["std"] std = ["alloc"] alloc = [] tz-rs-0.7.3/LICENSE-Apache000064400000000000000000000261351046102023000131050ustar 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.7.3/LICENSE-MIT000064400000000000000000000020501046102023000123430ustar 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.7.3/README.md000064400000000000000000000072641046102023000122020ustar 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.85+-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 expressed 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.85+`. ## 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.7.3/check_release.sh000075500000000000000000000007171046102023000140330ustar 00000000000000#!/bin/sh set -e fmt_cmd="cargo fmt --all -- --check" echo "+ $fmt_cmd" $fmt_cmd run() { cargo_arg=$1 bin_arg=$2 for rust in "1.85" "stable" "nightly"; do for feature in "" "alloc" "std"; do cmd="cargo +$rust -q $cargo_arg --all-targets --no-default-features --features=$const,$feature $bin_arg" echo "+ $cmd" $cmd echo "\n" done done } run "clippy" "-- -D warnings" run "test" tz-rs-0.7.3/clippy.toml000064400000000000000000000000161046102023000131040ustar 00000000000000msrv = "1.85" tz-rs-0.7.3/examples/settings.rs000064400000000000000000000007131046102023000147370ustar 00000000000000fn main() -> Result<(), tz::Error> { #[cfg(feature = "std")] { use tz::TimeZoneSettings; const TIME_ZONE_SETTINGS: TimeZoneSettings<'static> = TimeZoneSettings::new(&["/usr/share/zoneinfo", "/share/zoneinfo", "/etc/zoneinfo"], |path| Ok(std::fs::read(path)?)); let time_zone_local = TIME_ZONE_SETTINGS.parse_local()?; println!("{:?}", time_zone_local.find_current_local_time_type()?); } Ok(()) } tz-rs-0.7.3/examples/statics.rs000064400000000000000000000055401046102023000145540ustar 00000000000000use tz::datetime::{DateTime, UtcDateTime}; use tz::error::TzError; use tz::timezone::{AlternateTime, LeapSecond, LocalTimeType, MonthWeekDay, RuleDay, TimeZoneRef, Transition, TransitionRule}; fn main() -> Result<(), TzError> { macro_rules! unwrap { ($x:expr) => { match $x { Ok(x) => x, Err(_) => panic!(), } }; } const TIME_ZONE_REF: TimeZoneRef<'static> = 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), ], const { &[ 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), ], const { &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 LOCAL_TIME_TYPE: LocalTimeType = *unwrap!(TIME_ZONE_REF.find_local_time_type(0)); const UTC: TimeZoneRef<'static> = 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!("{LOCAL_TIME_TYPE:?}"); println!("{UNIX_EPOCH:?}"); println!("{UTC_DATE_TIME:?}"); println!("{DATE_TIME:#?}"); println!("{DATE_TIME_1:#?}"); println!("{DATE_TIME_2:#?}"); Ok(()) } tz-rs-0.7.3/examples/time.rs000064400000000000000000000121441046102023000140360ustar 00000000000000fn main() -> Result<(), tz::Error> { #[cfg(feature = "std")] { use tz::{DateTime, LocalTimeType, TimeZone, UtcDateTime}; // // 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 println!("{:?}", 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"); // // DateTime // // Get the current UTC date time println!("{:?}", 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)?; 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()?; println!("{:#?}", DateTime::now(time_zone_local.as_ref())?); // Create a new date time with an UTC offset (2000-01-01T01:00:00.123456789+01:00) println!("{:#?}", DateTime::new(2000, 1, 1, 1, 0, 0, 123_456_789, LocalTimeType::with_ut_offset(3600)?)?); // // 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.7.3/rustfmt.toml000064400000000000000000000000551046102023000133130ustar 00000000000000max_width = 160 use_small_heuristics = "Max" tz-rs-0.7.3/src/constants/mod.rs000064400000000000000000000043351046102023000146470ustar 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.7.3/src/datetime/find.rs000064400000000000000000001007431046102023000145700ustar 00000000000000//! Types related to the [`DateTime::find`] method. use crate::datetime::{DateTime, UtcDateTime, check_date_time_inputs, unix_time}; use crate::error::TzError; use crate::timezone::{TimeZoneRef, TransitionRule}; #[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")] #[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 DateTimeList for FoundDateTimeListRefMut<'_> { 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<(), TzError> { 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| -> Result<_, TzError> { match last_cached_time { Some((index, value)) if index == local_time_type_index => 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))); 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(TzError::OutOfRange); } // 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 tests { use super::*; use crate::datetime::tests::check_equal_date_time; use crate::timezone::{AlternateTime, Julian0WithLeap, Julian1WithoutLeap, LocalTimeType, RuleDay, TimeZone, Transition}; 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<(), TzError> { let new_date_time = |(year, month, month_day, hour, minute, second, ut_offset)| { 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<(), TzError> { 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<(), TzError> { 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| -> Result<_, TzError> { 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); 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<(), TzError> { 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<(), TzError> { 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<(), TzError> { 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<(), TzError> { 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.7.3/src/datetime/mod.rs000064400000000000000000001760551046102023000144400ustar 00000000000000//! Types related to a date time. mod find; #[doc(inline)] #[cfg(feature = "alloc")] pub use find::FoundDateTimeList; #[doc(inline)] pub use find::{FoundDateTimeKind, FoundDateTimeListRefMut}; use crate::constants::*; use crate::datetime::find::find_date_time; use crate::error::TzError; use crate::error::datetime::DateTimeError; use crate::timezone::{LocalTimeType, TimeZoneRef}; use crate::utils::{min, try_into_i32, try_into_i64}; use core::cmp::Ordering; use core::fmt; use core::ops::{Add, AddAssign, Sub, SubAssign}; use core::time::Duration; #[cfg(feature = "std")] use std::time::SystemTime; /// UTC date time expressed 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 { /// Unix epoch (`1970-01-01T00:00:00Z`) pub const UNIX_EPOCH: Self = Self { year: 1970, month: 1, month_day: 1, hour: 0, minute: 0, second: 0, nanoseconds: 0 }; /// Minimum allowed UTC date time pub const MIN: Self = Self { year: i32::MIN, month: 1, month_day: 1, hour: 0, minute: 0, second: 0, nanoseconds: 0 }; /// Maximum allowed UTC date time pub const MAX: Self = Self { year: i32::MAX, month: 12, month_day: 31, hour: 23, minute: 59, second: 59, nanoseconds: 999_999_999 }; /// Minimum allowed Unix time in seconds const MIN_UNIX_TIME: i64 = UtcDateTime::MIN.unix_time(); /// Maximum allowed Unix time in seconds const MAX_UNIX_TIME: i64 = UtcDateTime::MAX.unix_time(); /// Check if the UTC date time associated to a Unix time in seconds is valid const fn check_unix_time(unix_time: i64) -> Result<(), TzError> { if Self::MIN_UNIX_TIME <= unix_time && unix_time <= Self::MAX_UNIX_TIME { Ok(()) } else { Err(TzError::OutOfRange) } } /// 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]` /// pub const 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(TzError::OutOfRange); } if let Err(error) = check_date_time_inputs(year, month, month_day, hour, minute, second, nanoseconds) { return Err(TzError::DateTime(error)); } Ok(Self { year, month, month_day, hour, minute, second, nanoseconds }) } /// Construct a UTC date time from a Unix time in seconds and nanoseconds pub const 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(TzError::OutOfRange), }; 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`) pub const 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 Unix time in seconds associated to the UTC date time pub const 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. /// pub const fn project(&self, time_zone_ref: TimeZoneRef<'_>) -> Result { DateTime::from_timespec(self.unix_time(), self.nanoseconds, time_zone_ref) } /// Add a given duration to the UTC date time, returning `None` if the result cannot be represented. pub const fn checked_add(&self, duration: Duration) -> Option { // Overflow is not possible let total_nanoseconds = self.total_nanoseconds() + duration.as_nanos() as i128; match Self::from_total_nanoseconds(total_nanoseconds) { Ok(x) => Some(x), Err(_) => None, } } /// Subtract a given duration from the UTC date time, returning `None` if the result cannot be represented. pub const fn checked_sub(&self, duration: Duration) -> Option { // Overflow is not possible let total_nanoseconds = self.total_nanoseconds() - duration.as_nanos() as i128; match Self::from_total_nanoseconds(total_nanoseconds) { Ok(x) => Some(x), Err(_) => None, } } /// Get the duration elapsed since an earlier point in time. /// /// Returns `Ok(Duration)` if `earlier` is before `self`, or `Err(Duration)` otherwise with the duration in the opposite direction. /// pub const fn duration_since(&self, earlier: Self) -> Result { let current_total_nanoseconds = self.total_nanoseconds(); let earlier_total_nanoseconds = earlier.total_nanoseconds(); duration_between(earlier_total_nanoseconds, current_total_nanoseconds) } /// Returns the current UTC date time #[cfg(feature = "std")] pub fn now() -> Result { SystemTime::now().try_into() } } #[cfg(feature = "std")] impl TryFrom for UtcDateTime { type Error = TzError; fn try_from(time: SystemTime) -> Result { Self::from_total_nanoseconds(crate::utils::system_time::total_nanoseconds(time)) } } #[cfg(feature = "std")] impl From for SystemTime { fn from(utc_date_time: UtcDateTime) -> Self { match duration_between(0, utc_date_time.total_nanoseconds()) { Ok(duration) => SystemTime::UNIX_EPOCH + duration, Err(duration) => SystemTime::UNIX_EPOCH - duration, } } } impl Add for UtcDateTime { type Output = UtcDateTime; /// # Panics /// /// This function may panic if the result cannot be represented. /// See [`UtcDateTime::checked_add`] if this is not desired. /// fn add(self, rhs: Duration) -> Self::Output { self.checked_add(rhs).expect("attempt to add with overflow") } } impl AddAssign for UtcDateTime { fn add_assign(&mut self, rhs: Duration) { *self = *self + rhs } } impl Sub for UtcDateTime { type Output = UtcDateTime; /// # Panics /// /// This function may panic if the result cannot be represented. /// See [`UtcDateTime::checked_sub`] if this is not desired. /// fn sub(self, rhs: Duration) -> Self::Output { self.checked_sub(rhs).expect("attempt to subtract with overflow") } } impl SubAssign for UtcDateTime { fn sub_assign(&mut self, rhs: Duration) { *self = *self - rhs } } /// Date time associated to a local time type, expressed 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 Eq for DateTime {} impl PartialOrd for DateTime { fn partial_cmp(&self, other: &Self) -> Option { Some(self.cmp(other)) } } impl Ord for DateTime { fn cmp(&self, other: &Self) -> Ordering { (self.unix_time, self.nanoseconds).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)] pub const 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(TzError::DateTime(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")] 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 pub const 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(TzError::OutOfRange), }; 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(error) => return Err(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 pub const 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(error) => return Err(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 pub const 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(error) => Err(error), } } /// Construct a date time from total nanoseconds since Unix epoch (`1970-01-01T00:00:00Z`) and a time zone pub const 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(error) => Err(error), } } /// Project the date time into another time zone. /// /// Leap seconds are not preserved. /// pub const fn project(&self, time_zone_ref: TimeZoneRef<'_>) -> Result { Self::from_timespec(self.unix_time, self.nanoseconds, time_zone_ref) } /// Get the duration elapsed since an earlier point in time. /// /// Returns `Ok(Duration)` if `earlier` is before `self`, or `Err(Duration)` otherwise with the duration in the opposite direction. /// pub const fn duration_since(&self, earlier: Self) -> Result { let current_total_nanoseconds = self.total_nanoseconds(); let earlier_total_nanoseconds = earlier.total_nanoseconds(); duration_between(earlier_total_nanoseconds, current_total_nanoseconds) } /// Returns the current date time associated to the specified time zone #[cfg(feature = "std")] pub fn now(time_zone_ref: TimeZoneRef<'_>) -> Result { let now = crate::utils::system_time::total_nanoseconds(SystemTime::now()); Self::from_total_nanoseconds(now, time_zone_ref) } } impl TryFrom for UtcDateTime { type Error = TzError; fn try_from(date_time: DateTime) -> Result { Self::from_timespec(date_time.unix_time, date_time.nanoseconds) } } #[cfg(feature = "std")] impl From for SystemTime { fn from(date_time: DateTime) -> Self { match duration_between(0, date_time.total_nanoseconds()) { Ok(duration) => SystemTime::UNIX_EPOCH + duration, Err(duration) => SystemTime::UNIX_EPOCH - duration, } } } /// Macro for implementing date time getters macro_rules! impl_datetime { () => { /// Returns year #[inline] pub const fn year(&self) -> i32 { self.year } /// Returns month in `[1, 12]` #[inline] pub const fn month(&self) -> u8 { self.month } /// Returns day of the month in `[1, 31]` #[inline] pub const fn month_day(&self) -> u8 { self.month_day } /// Returns hours since midnight in `[0, 23]` #[inline] pub const fn hour(&self) -> u8 { self.hour } /// Returns minutes in `[0, 59]` #[inline] pub const fn minute(&self) -> u8 { self.minute } /// Returns seconds in `[0, 60]`, with a possible leap second #[inline] pub const fn second(&self) -> u8 { self.second } /// Returns nanoseconds in `[0, 999_999_999]` #[inline] pub const fn nanoseconds(&self) -> u32 { self.nanoseconds } /// Returns days since Sunday in `[0, 6]` #[inline] pub const 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] pub const 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] pub const 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] pub const fn local_time_type(&self) -> &LocalTimeType { &self.local_time_type } /// Returns UTC Unix time in seconds #[inline] pub const 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] const 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] const 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] pub(crate) const 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] pub(crate) const 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] const 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] const 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] const fn total_nanoseconds_to_timespec(total_nanoseconds: i128) -> Result<(i64, u32), TzError> { 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)) } /// Get the duration elapsed between two points in time represented as total nanoseconds since Unix epoch (`1970-01-01T00:00:00Z`). /// /// Returns `Ok(Duration)` if `before` is before `after`, or `Err(Duration)` otherwise with the duration in the opposite direction. /// #[inline] const fn duration_between(before: i128, after: i128) -> Result { // Overflow is not possible let total_nanoseconds_diff = after.abs_diff(before); let secs = total_nanoseconds_diff / NANOSECONDS_PER_SECOND as u128; let nanos = total_nanoseconds_diff % NANOSECONDS_PER_SECOND as u128; // Overflow is not possible let duration = Duration::new(secs as u64, nanos as u32); if after >= before { Ok(duration) } else { Err(duration) } } /// 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]` /// const 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::InvalidMonth); } if !(1 <= month_day && month_day <= 31) { return Err(DateTimeError::InvalidMonthDay); } if hour > 23 { return Err(DateTimeError::InvalidHour); } if minute > 59 { return Err(DateTimeError::InvalidMinute); } if second > 60 { return Err(DateTimeError::InvalidSecond); } if nanoseconds >= NANOSECONDS_PER_SECOND { return Err(DateTimeError::InvalidNanoseconds); } 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::InvalidMonthDay); } 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, "{year}-{month:02}-{month_day:02}T{hour:02}:{minute:02}:{second:02}.{nanoseconds:09}")?; 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, "{sign}{offset_hour:02}:{offset_minute:02}")?; if offset_second != 0 { write!(f, ":{offset_second:02}")?; } } else { write!(f, "Z")?; } Ok(()) } #[cfg(test)] mod tests { use super::*; #[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<(), TzError> { 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<(), TzError> { 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<(), TzError> { 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 {} } #[cfg(feature = "alloc")] #[test] fn test_date_time_ord() -> Result<(), TzError> { 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())); let date_time_1 = utc_date_time_1.project(TimeZone::fixed(0)?.as_ref())?; let date_time_2 = utc_date_time_2.project(TimeZone::fixed(3600)?.as_ref())?; let date_time_3 = utc_date_time_3.project(TimeZone::fixed(7200)?.as_ref())?; let date_time_4 = utc_date_time_4.project(TimeZone::fixed(10800)?.as_ref())?; assert_eq!(date_time_1.cmp(&date_time_1), Ordering::Equal); assert_eq!(date_time_1.cmp(&date_time_2), Ordering::Less); assert_eq!(date_time_1.cmp(&date_time_3), Ordering::Less); assert_eq!(date_time_1.cmp(&date_time_4), Ordering::Less); assert_eq!(date_time_2.cmp(&date_time_1), Ordering::Greater); assert_eq!(date_time_2.cmp(&date_time_2), Ordering::Equal); assert_eq!(date_time_2.cmp(&date_time_3), Ordering::Equal); assert_eq!(date_time_2.cmp(&date_time_4), Ordering::Less); assert_eq!(date_time_3.cmp(&date_time_1), Ordering::Greater); assert_eq!(date_time_3.cmp(&date_time_2), Ordering::Equal); assert_eq!(date_time_3.cmp(&date_time_3), Ordering::Equal); assert_eq!(date_time_3.cmp(&date_time_4), Ordering::Less); assert_eq!(date_time_4.cmp(&date_time_1), Ordering::Greater); assert_eq!(date_time_4.cmp(&date_time_2), Ordering::Greater); assert_eq!(date_time_4.cmp(&date_time_3), Ordering::Greater); assert_eq!(date_time_4.cmp(&date_time_4), Ordering::Equal); Ok(()) } #[cfg(feature = "alloc")] #[test] fn test_date_time_format() -> Result<(), TzError> { 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_duration() -> Result<(), TzError> { let utc_date_time = UtcDateTime::new(1, 2, 3, 4, 5, 6, 789_012_345)?; let date_time = utc_date_time.project(TimeZoneRef::utc())?; let duration = Duration::new(24 * 3600 * 365, 999_999_999); let added_utc_date_time = UtcDateTime::new(2, 2, 3, 4, 5, 7, 789_012_344)?; let substracted_utc_date_time = UtcDateTime::new(0, 2, 4, 4, 5, 5, 789_012_346)?; assert_eq!(utc_date_time.checked_add(duration), Some(added_utc_date_time)); assert_eq!(utc_date_time.checked_sub(duration), Some(substracted_utc_date_time)); assert_eq!(utc_date_time.duration_since(substracted_utc_date_time), Ok(duration)); assert_eq!(date_time.duration_since(substracted_utc_date_time.project(TimeZoneRef::utc())?), Ok(duration)); Ok(()) } #[cfg(feature = "alloc")] #[test] fn test_date_time_overflow() -> Result<(), TzError> { 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(TzError::OutOfRange))); assert!(matches!(DateTime::new(i32::MAX, 12, 31, 23, 59, 59, 0, LocalTimeType::with_ut_offset(-1)?), Err(TzError::OutOfRange))); assert!(matches!(UtcDateTime::new(i32::MAX, 12, 31, 23, 59, 60, 0), Err(TzError::OutOfRange))); assert!(matches!(DateTime::new(i32::MAX, 12, 31, 23, 59, 60, 0, LocalTimeType::utc()), Err(TzError::OutOfRange))); 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(TzError::OutOfRange))); assert!(matches!(UtcDateTime::from_timespec(UtcDateTime::MAX_UNIX_TIME + 1, 0), Err(TzError::OutOfRange))); assert!(matches!(UtcDateTime::from_timespec(UtcDateTime::MIN_UNIX_TIME, 0)?.project(TimeZone::fixed(-1)?.as_ref()), Err(TzError::OutOfRange))); assert!(matches!(UtcDateTime::from_timespec(UtcDateTime::MAX_UNIX_TIME, 0)?.project(TimeZone::fixed(1)?.as_ref()), Err(TzError::OutOfRange))); assert!(matches!(UtcDateTime::from_timespec(i64::MIN, 0), Err(TzError::OutOfRange))); assert!(matches!(UtcDateTime::from_timespec(i64::MAX, 0), Err(TzError::OutOfRange))); 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(TzError::OutOfRange))); assert!(matches!(DateTime::from_timespec(i64::MAX, 0, TimeZone::fixed(1)?.as_ref()), Err(TzError::OutOfRange))); assert_eq!(UtcDateTime::MIN.checked_sub(Duration::from_nanos(1)), None); assert_eq!(UtcDateTime::MAX.checked_add(Duration::from_nanos(1)), None); assert_eq!(UtcDateTime::MIN.checked_sub(Duration::MAX), None); assert_eq!(UtcDateTime::MAX.checked_add(Duration::MAX), None); assert_eq!(UtcDateTime::MAX.duration_since(UtcDateTime::MIN), Ok(Duration::new(135536076801503999, 999999999))); assert_eq!(UtcDateTime::MIN.duration_since(UtcDateTime::MAX), Err(Duration::new(135536076801503999, 999999999))); 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<(), TzError> { 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(TzError::OutOfRange))); assert!(matches!(total_nanoseconds_to_timespec(i128::MIN), Err(TzError::OutOfRange))); 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(TzError::OutOfRange))); assert!(matches!(total_nanoseconds_to_timespec(max_total_nanoseconds + 1), Err(TzError::OutOfRange))); Ok(()) } #[test] fn test_duration_between() -> Result<(), TzError> { assert_eq!(duration_between(1_234_567_890, 1_234_567_890), Ok(Duration::ZERO)); assert_eq!(duration_between(-1_000_001_000, 1_000_001_000), Ok(Duration::new(2, 2000))); assert_eq!(duration_between(1_000_001_000, -1_000_001_000), Err(Duration::new(2, 2000))); assert_eq!( duration_between(UtcDateTime::MIN.total_nanoseconds(), UtcDateTime::MAX.total_nanoseconds()), Ok(Duration::new(135536076801503999, 999999999)) ); Ok(()) } #[test] fn test_const() -> Result<(), TzError> { use crate::timezone::{AlternateTime, LeapSecond, MonthWeekDay, RuleDay, Transition, TransitionRule}; macro_rules! unwrap { ($x:expr) => { match $x { Ok(x) => x, Err(_) => panic!(), } }; } const TIME_ZONE_REF: TimeZoneRef<'static> = 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), ], const { &[ 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), ], const { &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<'static> = 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.7.3/src/error/datetime.rs000064400000000000000000000017451046102023000150030ustar 00000000000000//! Date time error types. use core::error::Error; use core::fmt; /// Date time error #[non_exhaustive] #[derive(Debug)] pub enum DateTimeError { /// Invalid month InvalidMonth, /// Invalid month day InvalidMonthDay, /// Invalid hour InvalidHour, /// Invalid minute InvalidMinute, /// Invalid second InvalidSecond, /// Invalid nanoseconds InvalidNanoseconds, } impl fmt::Display for DateTimeError { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { match self { Self::InvalidMonth => f.write_str("invalid month"), Self::InvalidMonthDay => f.write_str("invalid month day"), Self::InvalidHour => f.write_str("invalid hour"), Self::InvalidMinute => f.write_str("invalid minute"), Self::InvalidSecond => f.write_str("invalid second"), Self::InvalidNanoseconds => f.write_str("invalid nanoseconds"), } } } impl Error for DateTimeError {} tz-rs-0.7.3/src/error/mod.rs000064400000000000000000000066001046102023000137610ustar 00000000000000//! Error types. pub mod datetime; pub mod timezone; #[cfg(feature = "alloc")] pub mod parse; use datetime::DateTimeError; use timezone::{LocalTimeTypeError, TimeZoneError, TransitionRuleError}; #[cfg(feature = "alloc")] use parse::{TzFileError, TzStringError}; use core::error; use core::fmt; #[cfg(feature = "alloc")] use alloc::boxed::Box; /// Unified error type for everything in the crate #[non_exhaustive] #[derive(Debug)] pub enum Error { /// I/O error #[cfg(feature = "alloc")] Io(Box), /// Unified error type for every non I/O error in the crate Tz(TzError), } impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { #[cfg(feature = "alloc")] Self::Io(error) => error.fmt(f), Self::Tz(error) => error.fmt(f), } } } impl error::Error for Error {} impl> From for Error { fn from(error: T) -> Self { Self::Tz(error.into()) } } /// Unified error type for every non I/O error in the crate #[non_exhaustive] #[derive(Debug)] pub enum TzError { /// Unified error for parsing a TZif file #[cfg(feature = "alloc")] TzFile(TzFileError), /// Unified error for parsing a TZ string #[cfg(feature = "alloc")] TzString(TzStringError), /// Local time type error LocalTimeType(LocalTimeTypeError), /// Transition rule error TransitionRule(TransitionRuleError), /// Time zone error TimeZone(TimeZoneError), /// Date time error DateTime(DateTimeError), /// Out of range operation OutOfRange, /// No available local time type NoAvailableLocalTimeType, } impl fmt::Display for TzError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { #[cfg(feature = "alloc")] Self::TzFile(error) => write!(f, "invalid TZ file: {error}"), #[cfg(feature = "alloc")] Self::TzString(error) => write!(f, "invalid TZ string: {error}"), Self::LocalTimeType(error) => write!(f, "invalid local time type: {error}"), Self::TransitionRule(error) => write!(f, "invalid transition rule: {error}"), Self::TimeZone(error) => write!(f, "invalid time zone: {error}"), Self::DateTime(error) => write!(f, "invalid date time: {error}"), Self::OutOfRange => f.write_str("out of range operation"), Self::NoAvailableLocalTimeType => write!(f, "no local time type is available for the specified timestamp"), } } } impl error::Error for TzError {} #[cfg(feature = "alloc")] impl From for TzError { fn from(error: TzFileError) -> Self { Self::TzFile(error) } } #[cfg(feature = "alloc")] impl From for TzError { fn from(error: TzStringError) -> Self { Self::TzString(error) } } impl From for TzError { fn from(error: LocalTimeTypeError) -> Self { Self::LocalTimeType(error) } } impl From for TzError { fn from(error: TransitionRuleError) -> Self { Self::TransitionRule(error) } } impl From for TzError { fn from(error: TimeZoneError) -> Self { Self::TimeZone(error) } } impl From for TzError { fn from(error: DateTimeError) -> Self { Self::DateTime(error) } } tz-rs-0.7.3/src/error/parse.rs000064400000000000000000000112361046102023000143150ustar 00000000000000//! Parsing error types. use core::error::Error; use core::fmt; use core::num::ParseIntError; use core::str::Utf8Error; /// Parse data error #[non_exhaustive] #[derive(Debug)] pub enum ParseDataError { /// Unexpected end of data UnexpectedEof, /// Invalid data InvalidData, } impl fmt::Display for ParseDataError { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { match self { Self::UnexpectedEof => f.write_str("unexpected end of data"), Self::InvalidData => f.write_str("invalid data"), } } } impl Error for ParseDataError {} /// Unified error type for parsing a TZ string #[non_exhaustive] #[derive(Debug)] pub enum TzStringError { /// UTF-8 error Utf8(Utf8Error), /// Integer parsing error ParseInt(ParseIntError), /// Parse data error ParseData(ParseDataError), /// Invalid offset hour InvalidOffsetHour, /// Invalid offset minute InvalidOffsetMinute, /// Invalid offset second InvalidOffsetSecond, /// Invalid day time hour InvalidDayTimeHour, /// Invalid day time minute InvalidDayTimeMinute, /// Invalid day time second InvalidDayTimeSecond, /// Missing DST start and end rules MissingDstStartEndRules, /// Remaining data was found after parsing TZ string RemainingData, /// Empty TZ string Empty, } impl fmt::Display for TzStringError { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { match self { Self::Utf8(error) => error.fmt(f), Self::ParseInt(error) => error.fmt(f), Self::ParseData(error) => error.fmt(f), Self::InvalidOffsetHour => f.write_str("invalid offset hour"), Self::InvalidOffsetMinute => f.write_str("invalid offset minute"), Self::InvalidOffsetSecond => f.write_str("invalid offset second"), Self::InvalidDayTimeHour => f.write_str("invalid day time hour"), Self::InvalidDayTimeMinute => f.write_str("invalid day time minute"), Self::InvalidDayTimeSecond => f.write_str("invalid day time second"), Self::MissingDstStartEndRules => f.write_str("missing DST start and end rules"), Self::RemainingData => f.write_str("remaining data after parsing TZ string"), Self::Empty => f.write_str("empty TZ string"), } } } impl Error for TzStringError {} impl From for TzStringError { fn from(error: Utf8Error) -> Self { Self::Utf8(error) } } impl From for TzStringError { fn from(error: ParseIntError) -> Self { Self::ParseInt(error) } } impl From for TzStringError { fn from(error: ParseDataError) -> Self { Self::ParseData(error) } } /// Unified error type for parsing a TZif file #[non_exhaustive] #[derive(Debug)] pub enum TzFileError { /// UTF-8 error Utf8(Utf8Error), /// Parse data error ParseData(ParseDataError), /// Invalid magic number InvalidMagicNumber, /// Unsupported TZif version UnsupportedTzFileVersion, /// Invalid header InvalidHeader, /// Invalid footer InvalidFooter, /// Invalid DST indicator InvalidDstIndicator, /// Invalid time zone designation char index InvalidTimeZoneDesignationCharIndex, /// Invalid couple of standard/wall and UT/local indicators InvalidStdWallUtLocal, /// Remaining data after the end of a TZif v1 data block RemainingDataV1, } impl fmt::Display for TzFileError { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { match self { Self::Utf8(error) => error.fmt(f), Self::ParseData(error) => error.fmt(f), Self::InvalidMagicNumber => f.write_str("invalid magic number"), Self::UnsupportedTzFileVersion => write!(f, "unsupported TZ file version"), Self::InvalidHeader => f.write_str("invalid header"), Self::InvalidFooter => f.write_str("invalid footer"), Self::InvalidDstIndicator => f.write_str("invalid DST indicator"), Self::InvalidTimeZoneDesignationCharIndex => f.write_str("invalid time zone designation char index"), Self::InvalidStdWallUtLocal => f.write_str("invalid couple of standard/wall and UT/local indicators"), Self::RemainingDataV1 => f.write_str("remaining data after the end of a TZif v1 data block"), } } } impl Error for TzFileError {} impl From for TzFileError { fn from(error: Utf8Error) -> Self { Self::Utf8(error) } } impl From for TzFileError { fn from(error: ParseDataError) -> Self { Self::ParseData(error) } } tz-rs-0.7.3/src/error/timezone.rs000064400000000000000000000066241046102023000150420ustar 00000000000000//! Time zone error types. use core::error::Error; use core::fmt; /// Local time type error #[non_exhaustive] #[derive(Debug)] pub enum LocalTimeTypeError { /// Invalid time zone designation length InvalidTimeZoneDesignationLength, /// Invalid characters in time zone designation InvalidTimeZoneDesignationChar, /// Invalid UTC offset InvalidUtcOffset, } impl fmt::Display for LocalTimeTypeError { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { match self { Self::InvalidTimeZoneDesignationLength => f.write_str("time zone designation must have between 3 and 7 characters"), Self::InvalidTimeZoneDesignationChar => f.write_str("invalid characters in time zone designation"), Self::InvalidUtcOffset => f.write_str("invalid UTC offset"), } } } impl Error for LocalTimeTypeError {} /// Transition rule error #[non_exhaustive] #[derive(Debug)] pub enum TransitionRuleError { /// Invalid rule day julian day InvalidRuleDayJulianDay, /// Invalid rule day month InvalidRuleDayMonth, /// Invalid rule day week InvalidRuleDayWeek, /// Invalid rule day week day InvalidRuleDayWeekDay, /// Invalid standard time UTC offset InvalidStdUtcOffset, /// Invalid Daylight Saving Time UTC offset InvalidDstUtcOffset, /// Invalid DST start or end time InvalidDstStartEndTime, /// Inconsistent DST transition rules from one year to another InconsistentRule, } impl fmt::Display for TransitionRuleError { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { match self { Self::InvalidRuleDayJulianDay => f.write_str("invalid rule day julian day"), Self::InvalidRuleDayMonth => f.write_str("invalid rule day month"), Self::InvalidRuleDayWeek => f.write_str("invalid rule day week"), Self::InvalidRuleDayWeekDay => f.write_str("invalid rule day week day"), Self::InvalidStdUtcOffset => f.write_str("invalid standard time UTC offset"), Self::InvalidDstUtcOffset => f.write_str("invalid Daylight Saving Time UTC offset"), Self::InvalidDstStartEndTime => f.write_str("invalid DST start or end time"), Self::InconsistentRule => f.write_str("DST transition rules are not consistent from one year to another"), } } } impl Error for TransitionRuleError {} /// Time zone error #[non_exhaustive] #[derive(Debug)] pub enum TimeZoneError { /// No local time type NoLocalTimeType, /// Invalid local time type index InvalidLocalTimeTypeIndex, /// Invalid transition InvalidTransition, /// Invalid leap second InvalidLeapSecond, /// Inconsistent extra transition rule relative to the last transition InconsistentExtraRule, } impl fmt::Display for TimeZoneError { fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> { match self { Self::NoLocalTimeType => f.write_str("list of local time types must not be empty"), Self::InvalidLocalTimeTypeIndex => f.write_str("invalid local time type index"), Self::InvalidTransition => f.write_str("invalid transition"), Self::InvalidLeapSecond => f.write_str("invalid leap second"), Self::InconsistentExtraRule => f.write_str("extra transition rule is inconsistent with the last transition"), } } } impl Error for TimeZoneError {} tz-rs-0.7.3/src/lib.rs000064400000000000000000000221441046102023000126200ustar 00000000000000#![forbid(unsafe_code)] #![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 expressed 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::Error> { //! # #[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::Error> { //! # #[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(()) //! # } //! ``` //! //! # No std //! //! This crate can be used in `no_std` context. //! //! The `settings.rs` example shows how to construct a [`TimeZone`] by specifying a custom `read_file` function via the [`TimeZoneSettings`] struct. //! #[cfg(feature = "alloc")] extern crate alloc; mod constants; mod utils; #[cfg(feature = "alloc")] mod parse; pub mod datetime; pub mod error; pub mod timezone; #[doc(inline)] pub use datetime::{DateTime, UtcDateTime}; #[doc(inline)] pub use error::{Error, TzError}; #[doc(inline)] pub use timezone::{LocalTimeType, TimeZoneRef}; #[doc(inline)] #[cfg(feature = "alloc")] pub use timezone::{TimeZone, TimeZoneSettings}; tz-rs-0.7.3/src/parse/mod.rs000064400000000000000000000002211046102023000137330ustar 00000000000000//! Parsing functions. mod tz_file; mod tz_string; mod utils; pub(crate) use tz_file::parse_tz_file; pub(crate) use tz_string::parse_posix_tz; tz-rs-0.7.3/src/parse/tz_file.rs000064400000000000000000000353521046102023000146250ustar 00000000000000//! Functions used for parsing a TZif file. use crate::error::TzError; use crate::error::parse::TzFileError; use crate::parse::tz_string::parse_posix_tz; use crate::parse::utils::{Cursor, read_chunk_exact, read_exact}; use crate::timezone::{LeapSecond, LocalTimeType, TimeZone, Transition, TransitionRule}; use alloc::vec::Vec; use core::iter; use core::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 = read_exact(cursor, 4)?; if magic != *b"TZif" { return Err(TzFileError::InvalidMagicNumber); } let version = match read_exact(cursor, 1)? { [0x00] => Version::V1, [0x32] => Version::V2, [0x33] => Version::V3, _ => return Err(TzFileError::UnsupportedTzFileVersion), }; read_exact(cursor, 15)?; let ut_local_count = u32::from_be_bytes(*read_chunk_exact(cursor)?); let std_wall_count = u32::from_be_bytes(*read_chunk_exact(cursor)?); let leap_count = u32::from_be_bytes(*read_chunk_exact(cursor)?); let transition_count = u32::from_be_bytes(*read_chunk_exact(cursor)?); let type_count = u32::from_be_bytes(*read_chunk_exact(cursor)?); let char_count = u32::from_be_bytes(*read_chunk_exact(cursor)?); 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::InvalidHeader); } 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).map_err(TzFileError::from)?; if !(footer.starts_with('\n') && footer.ends_with('\n')) { return Err(TzError::TzFile(TzFileError::InvalidFooter)); } let tz_string = footer.trim_matches(|c: char| c.is_ascii_whitespace()); if tz_string.starts_with(':') || tz_string.contains('\0') { return Err(TzError::TzFile(TzFileError::InvalidFooter)); } 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 DataBlocks<'a, const 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], } /// Read TZif data blocks fn read_data_blocks<'a, const TIME_SIZE: usize>(cursor: &mut Cursor<'a>, header: &Header) -> Result, TzFileError> { Ok(DataBlocks { transition_times: read_exact(cursor, header.transition_count * TIME_SIZE)?, transition_types: read_exact(cursor, header.transition_count)?, local_time_types: read_exact(cursor, header.type_count * 6)?, time_zone_designations: read_exact(cursor, header.char_count)?, leap_seconds: read_exact(cursor, header.leap_count * (TIME_SIZE + 4))?, std_walls: read_exact(cursor, header.std_wall_count)?, ut_locals: read_exact(cursor, header.ut_local_count)?, }) } trait ParseTime { type TimeData; fn parse_time(&self, data: &Self::TimeData) -> i64; } impl ParseTime for DataBlocks<'_, 4> { type TimeData = [u8; 4]; fn parse_time(&self, data: &Self::TimeData) -> i64 { i32::from_be_bytes(*data).into() } } impl ParseTime for DataBlocks<'_, 8> { type TimeData = [u8; 8]; fn parse_time(&self, data: &Self::TimeData) -> i64 { i64::from_be_bytes(*data) } } impl<'a, const TIME_SIZE: usize> DataBlocks<'a, TIME_SIZE> where DataBlocks<'a, TIME_SIZE>: ParseTime, { /// Parse time zone data fn parse(&self, header: &Header, footer: Option<&[u8]>) -> Result { let mut transitions = Vec::with_capacity(header.transition_count); for (time_data, &local_time_type_index) in self.transition_times.chunks_exact(TIME_SIZE).zip(self.transition_types) { let time_data = time_data.first_chunk::().unwrap(); let unix_leap_time = self.parse_time(time_data); 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 data in self.local_time_types.chunks_exact(6) { let [d0, d1, d2, d3, d4, d5] = <[u8; 6]>::try_from(data).unwrap(); let ut_offset = i32::from_be_bytes([d0, d1, d2, d3]); let is_dst = match d4 { 0 => false, 1 => true, _ => return Err(TzError::TzFile(TzFileError::InvalidDstIndicator)), }; let char_index = d5 as usize; if char_index >= header.char_count { return Err(TzError::TzFile(TzFileError::InvalidTimeZoneDesignationCharIndex)); } let time_zone_designation = match self.time_zone_designations[char_index..].iter().position(|&c| c == b'\0') { None => return Err(TzError::TzFile(TzFileError::InvalidTimeZoneDesignationCharIndex)), 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 data in self.leap_seconds.chunks_exact(TIME_SIZE + 4) { let (time_data, tail) = data.split_first_chunk::().unwrap(); let correction_data = tail.first_chunk::<4>().unwrap(); let unix_leap_time = self.parse_time(time_data); let correction = i32::from_be_bytes(*correction_data); 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(TzError::TzFile(TzFileError::InvalidStdWallUtLocal)); } } let extra_rule = footer.and_then(|footer| parse_footer(footer, header.version == Version::V3).transpose()).transpose()?; 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 = bytes; let header = parse_header(&mut cursor)?; match header.version { Version::V1 => { let data_blocks = read_data_blocks::<4>(&mut cursor, &header)?; if !cursor.is_empty() { return Err(TzError::TzFile(TzFileError::RemainingDataV1)); } Ok(data_blocks.parse(&header, None)?) } Version::V2 | Version::V3 => { // Skip v1 data block read_data_blocks::<4>(&mut cursor, &header)?; let header = parse_header(&mut cursor)?; let data_blocks = read_data_blocks::<8>(&mut cursor, &header)?; let footer = cursor; Ok(data_blocks.parse(&header, Some(footer))?) } } } #[cfg(test)] mod tests { use super::*; use crate::timezone::{AlternateTime, MonthWeekDay, RuleDay, TimeZone}; use alloc::vec; #[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.7.3/src/parse/tz_string.rs000064400000000000000000000302761046102023000152140ustar 00000000000000//! Functions used for parsing a TZ string. use crate::error::TzError; use crate::error::parse::{ParseDataError, TzStringError}; use crate::parse::utils::{Cursor, read_exact, read_optional_tag, read_tag, read_until, read_while}; use crate::timezone::{AlternateTime, Julian0WithLeap, Julian1WithoutLeap, LocalTimeType, MonthWeekDay, RuleDay, TransitionRule}; use core::num::ParseIntError; use core::str::{self, FromStr}; /// Convert the `Err` variant of a `Result` fn map_err(result: Result) -> Result { Ok(result?) } /// 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], ParseDataError> { let unquoted = if cursor.first() == Some(&b'<') { read_exact(cursor, 1)?; let unquoted = read_until(cursor, |&x| x == b'>')?; read_exact(cursor, 1)?; unquoted } else { read_while(cursor, 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(read_while(cursor, u8::is_ascii_digit)?)?; let mut minute = 0; let mut second = 0; if read_optional_tag(cursor, b":")? { minute = parse_int(read_while(cursor, u8::is_ascii_digit)?)?; if read_optional_tag(cursor, b":")? { second = parse_int(read_while(cursor, 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.first() { read_exact(cursor, 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::InvalidOffsetHour); } if !(0..=59).contains(&minute) { return Err(TzStringError::InvalidOffsetMinute); } if !(0..=59).contains(&second) { return Err(TzStringError::InvalidOffsetSecond); } Ok(sign * (hour * 3600 + minute * 60 + second)) } /// Parse transition rule day fn parse_rule_day(cursor: &mut Cursor<'_>) -> Result { match cursor.first() { Some(b'J') => { map_err(read_exact(cursor, 1))?; let data = map_err(read_while(cursor, u8::is_ascii_digit))?; Ok(RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(parse_int(data)?)?)) } Some(b'M') => { map_err(read_exact(cursor, 1))?; let month = parse_int(map_err(read_while(cursor, u8::is_ascii_digit))?)?; map_err(read_tag(cursor, b"."))?; let week = parse_int(map_err(read_while(cursor, u8::is_ascii_digit))?)?; map_err(read_tag(cursor, b"."))?; let week_day = parse_int(map_err(read_while(cursor, u8::is_ascii_digit))?)?; Ok(RuleDay::MonthWeekDay(MonthWeekDay::new(month, week, week_day)?)) } _ => { let data = map_err(read_while(cursor, u8::is_ascii_digit))?; Ok(RuleDay::Julian0WithLeap(Julian0WithLeap::new(parse_int(data)?)?)) } } } /// 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::InvalidDayTimeHour); } if !(0..=59).contains(&minute) { return Err(TzStringError::InvalidDayTimeMinute); } if !(0..=59).contains(&second) { return Err(TzStringError::InvalidDayTimeSecond); } 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::InvalidDayTimeHour); } if !(0..=59).contains(&minute) { return Err(TzStringError::InvalidDayTimeMinute); } if !(0..=59).contains(&second) { return Err(TzStringError::InvalidDayTimeSecond); } 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 map_err(read_optional_tag(cursor, 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 = tz_string; let std_time_zone = Some(map_err(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(map_err(parse_time_zone_designation(&mut cursor))?); let dst_offset = match cursor.first() { Some(&b',') => std_offset - 3600, Some(_) => parse_offset(&mut cursor)?, None => return Err(TzError::TzString(TzStringError::MissingDstStartEndRules)), }; if cursor.is_empty() { return Err(TzError::TzString(TzStringError::MissingDstStartEndRules)); } map_err(read_tag(&mut cursor, b","))?; let (dst_start, dst_start_time) = parse_rule_block(&mut cursor, use_string_extensions)?; map_err(read_tag(&mut cursor, b","))?; let (dst_end, dst_end_time) = parse_rule_block(&mut cursor, use_string_extensions)?; if !cursor.is_empty() { return Err(TzError::TzString(TzStringError::RemainingData)); } 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 tests { 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::TzString(TzStringError::MissingDstStartEndRules)))); assert!(matches!(parse_posix_tz(b"EET-2EEST", false), Err(TzError::TzString(TzStringError::MissingDstStartEndRules)))); Ok(()) } } tz-rs-0.7.3/src/parse/utils.rs000064400000000000000000000036131046102023000143240ustar 00000000000000//! Some useful functions. use crate::error::parse::ParseDataError; /// Cursor type alias pub(super) type Cursor<'a> = &'a [u8]; /// Read exactly `count` bytes and reduce remaining data pub(super) fn read_exact<'a>(cursor: &mut Cursor<'a>, count: usize) -> Result<&'a [u8], ParseDataError> { match cursor.split_at_checked(count) { Some((result, tail)) => { *cursor = tail; Ok(result) } None => Err(ParseDataError::UnexpectedEof), } } /// Read exactly `N` bytes into an array and reduce remaining data pub(super) fn read_chunk_exact<'a, const N: usize>(cursor: &mut Cursor<'a>) -> Result<&'a [u8; N], ParseDataError> { match cursor.split_first_chunk::() { Some((result, tail)) => { *cursor = tail; Ok(result) } None => Err(ParseDataError::UnexpectedEof), } } /// Read bytes and compare them to the provided tag pub(super) fn read_tag(cursor: &mut Cursor<'_>, tag: &[u8]) -> Result<(), ParseDataError> { if read_exact(cursor, tag.len())? == tag { Ok(()) } else { Err(ParseDataError::InvalidData) } } /// Read bytes if the remaining data is prefixed by the provided tag pub(super) fn read_optional_tag(cursor: &mut Cursor<'_>, tag: &[u8]) -> Result { if cursor.starts_with(tag) { read_exact(cursor, tag.len())?; Ok(true) } else { Ok(false) } } /// Read bytes as long as the provided predicate is true pub(super) fn read_while<'a, F: Fn(&u8) -> bool>(cursor: &mut Cursor<'a>, f: F) -> Result<&'a [u8], ParseDataError> { read_exact(cursor, cursor.iter().position(|x| !f(x)).unwrap_or(cursor.len())) } /// Read bytes until the provided predicate is true pub(super) fn read_until<'a, F: Fn(&u8) -> bool>(cursor: &mut Cursor<'a>, f: F) -> Result<&'a [u8], ParseDataError> { read_exact(cursor, cursor.iter().position(f).unwrap_or(cursor.len())) } tz-rs-0.7.3/src/timezone/mod.rs000064400000000000000000000667561046102023000145040ustar 00000000000000//! Types related to a time zone. mod rule; #[doc(inline)] pub use rule::{AlternateTime, Julian0WithLeap, Julian1WithoutLeap, MonthWeekDay, RuleDay, TransitionRule}; use crate::error::TzError; use crate::error::timezone::{LocalTimeTypeError, TimeZoneError}; use crate::utils::{binary_search_leap_seconds, binary_search_transitions}; #[cfg(feature = "alloc")] use crate::{ error::parse::TzStringError, parse::{parse_posix_tz, parse_tz_file}, }; use core::fmt; use core::str; #[cfg(feature = "alloc")] use alloc::{boxed::Box, format, vec, vec::Vec}; #[cfg(feature = "std")] use std::time::SystemTime; /// 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] pub const 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] pub const fn unix_leap_time(&self) -> i64 { self.unix_leap_time } /// Returns local time type index #[inline] pub const 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] pub const fn new(unix_leap_time: i64, correction: i32) -> Self { Self { unix_leap_time, correction } } /// Returns Unix leap time #[inline] pub const fn unix_leap_time(&self) -> i64 { self.unix_leap_time } /// Returns leap second correction #[inline] pub const fn correction(&self) -> i32 { self.correction } } /// ASCII-encoded fixed-capacity string, used for storing time zone designations. /// /// POSIX only supports time zone designations with at least three characters, /// but this type is extended to also support military time zones like `"Z"`. #[derive(Copy, Clone, Eq, PartialEq)] struct TzAsciiStr { /// Length-prefixed string buffer bytes: [u8; 8], } impl TzAsciiStr { /// Construct a time zone designation string const fn new(input: &[u8]) -> Result { let len = input.len(); if !(1 <= len && len <= 7) { return Err(LocalTimeTypeError::InvalidTimeZoneDesignationLength); } 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::InvalidTimeZoneDesignationChar); } bytes[i + 1] = b; i += 1; } Ok(Self { bytes }) } /// Returns time zone designation as a byte slice #[inline] const fn as_bytes(&self) -> &[u8] { match &self.bytes { [1, head @ .., _, _, _, _, _, _] => head, [2, head @ .., _, _, _, _, _] => head, [3, head @ .., _, _, _, _] => head, [4, head @ .., _, _, _] => head, [5, head @ .., _, _] => head, [6, head @ .., _] => head, [7, head @ ..] => head, _ => unreachable!(), } } /// Returns time zone designation as a string #[inline] const fn as_str(&self) -> &str { match str::from_utf8(self.as_bytes()) { Ok(s) => s, Err(_) => panic!("unreachable code: ASCII is valid UTF-8"), } } /// Check if two time zone designations are equal #[inline] const 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 pub const fn new(ut_offset: i32, is_dst: bool, time_zone_designation: Option<&[u8]>) -> Result { if ut_offset == i32::MIN { return Err(LocalTimeTypeError::InvalidUtcOffset); } 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] pub const fn with_ut_offset(ut_offset: i32) -> Result { if ut_offset == i32::MIN { return Err(LocalTimeTypeError::InvalidUtcOffset); } Ok(Self { ut_offset, is_dst: false, time_zone_designation: None }) } /// Returns offset from UTC in seconds #[inline] pub const fn ut_offset(&self) -> i32 { self.ut_offset } /// Returns daylight saving time indicator #[inline] pub const fn is_dst(&self) -> bool { self.is_dst } /// Returns time zone designation #[inline] pub const 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] const 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, } } } /// 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 pub const 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] pub const fn utc() -> Self { Self { transitions: &[], local_time_types: &[const { LocalTimeType::utc() }], leap_seconds: &[], extra_rule: &None } } /// Returns list of transitions #[inline] pub const fn transitions(&self) -> &'a [Transition] { self.transitions } /// Returns list of local time types #[inline] pub const fn local_time_types(&self) -> &'a [LocalTimeType] { self.local_time_types } /// Returns list of leap seconds #[inline] pub const fn leap_seconds(&self) -> &'a [LeapSecond] { self.leap_seconds } /// Returns extra transition rule applicable after the last transition #[inline] pub const 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 pub const fn find_local_time_type(&self, unix_time: i64) -> Result<&'a LocalTimeType, TzError> { 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(error) => return Err(error), }; if unix_leap_time >= last_transition.unix_leap_time { match self.extra_rule { Some(extra_rule) => extra_rule, None => return Err(TzError::NoAvailableLocalTimeType), } } 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]); } } }; extra_rule.find_local_time_type(unix_time) } /// Construct a reference to a time zone #[inline] const 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 const fn check_inputs(&self) -> Result<(), TzError> { 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(TzError::TimeZone(TimeZoneError::NoLocalTimeType)); } // 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(TzError::TimeZone(TimeZoneError::InvalidLocalTimeTypeIndex)); } if i_transition + 1 < self.transitions.len() && self.transitions[i_transition].unix_leap_time >= self.transitions[i_transition + 1].unix_leap_time { return Err(TzError::TimeZone(TimeZoneError::InvalidTransition)); } 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(TzError::TimeZone(TimeZoneError::InvalidLeapSecond)); } 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(TzError::TimeZone(TimeZoneError::InvalidLeapSecond)); } } 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(error) => return Err(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(error) => return Err(error), }; if !last_local_time_type.equal(rule_local_time_type) { return Err(TzError::TimeZone(TimeZoneError::InconsistentExtraRule)); } } Ok(()) } /// Convert Unix time to Unix leap time, from the list of leap seconds in a time zone pub(crate) const 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(TzError::OutOfRange), }; i += 1; } Ok(unix_leap_time) } /// Convert Unix leap time to Unix time, from the list of leap seconds in a time zone pub(crate) const fn unix_leap_time_to_unix_time(&self, unix_leap_time: i64) -> Result { if unix_leap_time == i64::MIN { return Err(TzError::OutOfRange); } 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(TzError::OutOfRange), } } } /// Time zone #[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, } #[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 }) } /// 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, TzError> { self.as_ref().find_local_time_type(unix_time) } /// Construct a time zone from the contents of a time zone file pub fn from_tz_data(bytes: &[u8]) -> Result { parse_tz_file(bytes) } /// Returns local time zone. /// /// This method in not supported on non-UNIX platforms, and returns the UTC time zone instead. /// #[cfg(feature = "std")] pub fn local() -> Result { TimeZoneSettings::DEFAULT.parse_local() } /// 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")] pub fn from_posix_tz(tz_string: &str) -> Result { TimeZoneSettings::DEFAULT.parse_posix_tz(tz_string) } /// Find the current local time type associated to the time zone #[cfg(feature = "std")] pub fn find_current_local_time_type(&self) -> Result<&LocalTimeType, TzError> { self.find_local_time_type(crate::utils::system_time::unix_time(SystemTime::now())) } } /// Read file function type alias #[cfg(feature = "alloc")] type ReadFileFn = fn(path: &str) -> Result, Box>; /// Time zone settings #[cfg(feature = "alloc")] #[derive(Debug)] pub struct TimeZoneSettings<'a> { /// Possible system timezone directories directories: &'a [&'a str], /// Read file function read_file_fn: ReadFileFn, } #[cfg(feature = "alloc")] impl<'a> TimeZoneSettings<'a> { /// Default possible system timezone directories pub const DEFAULT_DIRECTORIES: &'static [&'static str] = &["/usr/share/zoneinfo", "/share/zoneinfo", "/etc/zoneinfo"]; /// Default read file function #[cfg(feature = "std")] pub const DEFAULT_READ_FILE_FN: ReadFileFn = |path| Ok(std::fs::read(path)?); /// Default time zone settings #[cfg(feature = "std")] pub const DEFAULT: TimeZoneSettings<'static> = TimeZoneSettings { directories: Self::DEFAULT_DIRECTORIES, read_file_fn: Self::DEFAULT_READ_FILE_FN }; /// Construct time zone settings pub const fn new(directories: &'a [&'a str], read_file_fn: ReadFileFn) -> TimeZoneSettings<'a> { Self { directories, read_file_fn } } /// Returns local time zone using current settings. /// /// This method in not supported on non-UNIX platforms, and returns the UTC time zone instead. /// pub fn parse_local(&self) -> Result { #[cfg(not(unix))] let local_time_zone = TimeZone::utc(); #[cfg(unix)] let local_time_zone = self.parse_posix_tz("localtime")?; Ok(local_time_zone) } /// Construct a time zone from a POSIX TZ string using current settings, /// as described in [the POSIX documentation of the `TZ` environment variable](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html). pub fn parse_posix_tz(&self, tz_string: &str) -> Result { if tz_string.is_empty() { return Err(TzStringError::Empty.into()); } if tz_string == "localtime" { return Ok(parse_tz_file(&(self.read_file_fn)("/etc/localtime").map_err(crate::Error::Io)?)?); } let mut chars = tz_string.chars(); if chars.next() == Some(':') { return Ok(parse_tz_file(&self.read_tz_file(chars.as_str())?)?); } match self.read_tz_file(tz_string) { Ok(bytes) => Ok(parse_tz_file(&bytes)?), 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))?) } } } /// Read the TZif file corresponding to a TZ string using current settings fn read_tz_file(&self, tz_string: &str) -> Result, crate::Error> { let read_file_fn = |path: &str| (self.read_file_fn)(path).map_err(crate::Error::Io); // Don't check system timezone directories on non-UNIX platforms #[cfg(not(unix))] return Ok(read_file_fn(tz_string)?); #[cfg(unix)] if tz_string.starts_with('/') { Ok(read_file_fn(tz_string)?) } else { self.directories .iter() .find_map(|folder| read_file_fn(&format!("{folder}/{tz_string}")).ok()) .ok_or_else(|| crate::Error::Io("file was not found".into())) } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_tz_ascii_str() -> Result<(), TzError> { assert!(matches!(TzAsciiStr::new(b""), Err(LocalTimeTypeError::InvalidTimeZoneDesignationLength))); assert_eq!(TzAsciiStr::new(b"1")?.as_bytes(), b"1"); assert_eq!(TzAsciiStr::new(b"12")?.as_bytes(), b"12"); 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::InvalidTimeZoneDesignationLength))); assert!(matches!(TzAsciiStr::new(b"123456789"), Err(LocalTimeTypeError::InvalidTimeZoneDesignationLength))); assert!(matches!(TzAsciiStr::new(b"1234567890"), Err(LocalTimeTypeError::InvalidTimeZoneDesignationLength))); assert!(matches!(TzAsciiStr::new(b"123\0\0\0"), Err(LocalTimeTypeError::InvalidTimeZoneDesignationChar))); Ok(()) } #[cfg(feature = "alloc")] #[test] fn test_time_zone() -> Result<(), TzError> { 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(TzError::NoAvailableLocalTimeType))); 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<(), crate::Error> { #[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::NoAvailableLocalTimeType))); 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<(), TzError> { 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<(), TzError> { 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(TzError::OutOfRange))); Ok(()) } } tz-rs-0.7.3/src/timezone/rule.rs000064400000000000000000001332131046102023000146530ustar 00000000000000//! Types related to a time zone extra transition rule. use crate::constants::*; use crate::datetime::{UtcDateTime, days_since_unix_epoch, is_leap_year}; use crate::error::TzError; use crate::error::timezone::TransitionRuleError; use crate::timezone::LocalTimeType; use crate::utils::{binary_search_i64, cmp}; use core::cmp::Ordering; /// 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] pub const fn new(julian_day_1: u16) -> Result { if !(1 <= julian_day_1 && julian_day_1 <= 365) { return Err(TransitionRuleError::InvalidRuleDayJulianDay); } Ok(Self(julian_day_1)) } /// Returns inner value #[inline] pub const fn get(&self) -> u16 { self.0 } /// Compute transition date /// /// ## Outputs /// /// * `month`: Month in `[1, 12]` /// * `month_day`: Day of the month in `[1, 31]` /// const 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 const 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] pub const fn new(julian_day_0: u16) -> Result { if julian_day_0 > 365 { return Err(TransitionRuleError::InvalidRuleDayJulianDay); } Ok(Self(julian_day_0)) } /// Returns inner value #[inline] pub const 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]` /// const 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 const 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] pub const fn new(month: u8, week: u8, week_day: u8) -> Result { if !(1 <= month && month <= 12) { return Err(TransitionRuleError::InvalidRuleDayMonth); } if !(1 <= week && week <= 5) { return Err(TransitionRuleError::InvalidRuleDayWeek); } if week_day > 6 { return Err(TransitionRuleError::InvalidRuleDayWeekDay); } Ok(Self { month, week, week_day }) } /// Returns month in `[1, 12]` #[inline] pub const fn month(&self) -> u8 { self.month } /// Returns week of the month in `[1, 5]`, with `5` representing the last week of the month #[inline] pub const fn week(&self) -> u8 { self.week } /// Returns day of the week in `[0, 6]` from Sunday #[inline] pub const 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]` /// const 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 - week_day_of_first_month_day).rem_euclid(DAYS_PER_WEEK); let mut month_day = first_week_day_occurence_in_month + (week - 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 const 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]` /// const 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 pub(crate) const 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 pub const 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::InvalidStdUtcOffset); } if !(-25 * SECONDS_PER_HOUR < dst_ut_offset && dst_ut_offset < 26 * SECONDS_PER_HOUR) { return Err(TransitionRuleError::InvalidDstUtcOffset); } // 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::InvalidDstStartEndTime); } // 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::InconsistentRule); } Ok(Self { std, dst, dst_start, dst_start_time, dst_end, dst_end_time }) } /// Returns local time type for standard time #[inline] pub const fn std(&self) -> &LocalTimeType { &self.std } /// Returns local time type for Daylight Saving Time #[inline] pub const fn dst(&self) -> &LocalTimeType { &self.dst } /// Returns start day of Daylight Saving Time #[inline] pub const fn dst_start(&self) -> &RuleDay { &self.dst_start } /// Returns local start day time of Daylight Saving Time, in seconds #[inline] pub const fn dst_start_time(&self) -> i32 { self.dst_start_time } /// Returns end day of Daylight Saving Time #[inline] pub const fn dst_end(&self) -> &RuleDay { &self.dst_end } /// Returns local end day time of Daylight Saving Time, in seconds #[inline] pub const 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 const fn find_local_time_type(&self, unix_time: i64) -> Result<&LocalTimeType, TzError> { // 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(TzError::OutOfRange); } 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 pub(super) const fn find_local_time_type(&self, unix_time: i64) -> Result<&LocalTimeType, TzError> { 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. /// const 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 const 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 const 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 const fn check_two_month_week_days(month_week_day_1: MonthWeekDay, utc_day_time_1: i64, month_week_day_2: MonthWeekDay, utc_day_time_2: i64) -> bool { // 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)), } } _ => 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 tests { use super::*; use crate::TzError; #[test] fn test_compute_check_infos() -> Result<(), TzError> { 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<(), TzError> { let utc = LocalTimeType::utc(); let julian_1 = |year_day| -> Result<_, TzError> { Ok(RuleDay::Julian1WithoutLeap(Julian1WithoutLeap::new(year_day)?)) }; let julian_0 = |year_day| -> Result<_, TzError> { Ok(RuleDay::Julian0WithLeap(Julian0WithLeap::new(year_day)?)) }; let mwd = |month, week, week_day| -> Result<_, TzError> { 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<(), TzError> { 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<(), TzError> { 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<(), TzError> { 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(TzError::OutOfRange))); assert!(matches!(transition_rule_2.find_local_time_type(i64::MAX), Err(TzError::OutOfRange))); Ok(()) } } tz-rs-0.7.3/src/utils/const_fns.rs000064400000000000000000000047411046102023000152110ustar 00000000000000//! Some useful constant functions. use crate::error::TzError; use crate::timezone::{LeapSecond, Transition}; use core::cmp::Ordering; /// Compare two values pub(crate) const 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 pub(crate) const 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(TzError::OutOfRange) } }}; } /// Convert a `i64` value to a `i32` value pub(crate) const fn try_into_i32(value: i64) -> Result { impl_try_into_integer!(i64, i32, value) } /// Convert a `i128` value to a `i64` value pub(crate) const 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 const fn copied(x: &i64) -> i64 { *x } /// Binary searches a sorted `i64` slice for the given element pub(crate) const 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 pub(crate) const 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 pub(crate) const fn binary_search_leap_seconds(slice: &[LeapSecond], x: i64) -> Result { impl_binary_search!(slice, LeapSecond::unix_leap_time, x) } tz-rs-0.7.3/src/utils/mod.rs000064400000000000000000000003551046102023000137710ustar 00000000000000//! Some useful utilities. mod const_fns; #[cfg(feature = "std")] pub(crate) mod system_time; pub(crate) use const_fns::{binary_search_i64, binary_search_leap_seconds, binary_search_transitions, cmp, min, try_into_i32, try_into_i64}; tz-rs-0.7.3/src/utils/system_time.rs000064400000000000000000000021001046102023000155420ustar 00000000000000//! Some useful system time functions. use std::time::{Duration, SystemTime}; /// Returns the duration between Unix epoch (`1970-01-01T00:00:00Z`) and a `SystemTime`. /// /// The `Ok` variant corresponds to a positive duration, and the `Err` variant to a negative duration. fn duration_since_epoch(time: SystemTime) -> Result { time.duration_since(SystemTime::UNIX_EPOCH).map_err(|e| e.duration()) } /// Returns the Unix time in seconds for a `SystemTime` pub(crate) fn unix_time(time: SystemTime) -> i64 { match duration_since_epoch(time) { Ok(duration) => 0i64.saturating_add_unsigned(duration.as_secs()), Err(duration) => 0i64.saturating_sub_unsigned(duration.as_secs()), } } /// Returns the total nanoseconds between Unix epoch (`1970-01-01T00:00:00Z`) and a `SystemTime` pub(crate) fn total_nanoseconds(time: SystemTime) -> i128 { match duration_since_epoch(time) { Ok(duration) => 0i128.saturating_add_unsigned(duration.as_nanos()), Err(duration) => 0i128.saturating_sub_unsigned(duration.as_nanos()), } }