├── fuzz ├── .gitignore ├── fuzz_targets │ ├── fuzz_format.rs │ └── fuzz_reader.rs ├── README.md └── Cargo.toml ├── rustfmt.toml ├── .git-ignore-revs ├── bench ├── src │ └── lib.rs ├── Cargo.toml └── benches │ ├── serde.rs │ └── chrono.rs ├── tests ├── ohos │ └── tzdata ├── android │ └── tzdata ├── win_bindings.rs ├── wasm.rs └── dateutils.rs ├── .gitignore ├── .github ├── codecov.yml ├── pull_request_template.md ├── dependabot.yml └── workflows │ ├── codecov.yml │ ├── lint.yml │ └── test.yml ├── taplo.toml ├── deny.toml ├── ci └── core-test │ ├── src │ └── lib.rs │ └── Cargo.toml ├── CITATION.cff ├── src ├── offset │ ├── local │ │ ├── win_bindings.rs │ │ ├── tz_info │ │ │ ├── mod.rs │ │ │ └── parser.rs │ │ ├── unix.rs │ │ ├── tz_data.rs │ │ └── windows.rs │ ├── utc.rs │ └── fixed.rs ├── format │ ├── locales.rs │ └── scan.rs ├── naive │ ├── time │ │ ├── serde.rs │ │ └── tests.rs │ ├── isoweek.rs │ └── mod.rs ├── weekday.rs └── month.rs ├── Cargo.toml ├── README.md └── LICENSE.txt /fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | target 3 | corpus 4 | artifacts 5 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | style_edition = "2024" 2 | use_small_heuristics = "Max" 3 | -------------------------------------------------------------------------------- /.git-ignore-revs: -------------------------------------------------------------------------------- 1 | febb8dc168325ac471b54591c925c48b6a485962 # cargo fmt 2 | -------------------------------------------------------------------------------- /bench/src/lib.rs: -------------------------------------------------------------------------------- 1 | // This file only exists to make `benches` a valid crate. 2 | -------------------------------------------------------------------------------- /tests/ohos/tzdata: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chronotope/chrono/HEAD/tests/ohos/tzdata -------------------------------------------------------------------------------- /tests/android/tzdata: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chronotope/chrono/HEAD/tests/android/tzdata -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | .tool-versions 4 | 5 | # for jetbrains users 6 | .idea/ 7 | -------------------------------------------------------------------------------- /.github/codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | threshold: 1% 6 | -------------------------------------------------------------------------------- /taplo.toml: -------------------------------------------------------------------------------- 1 | include = ["deny.toml", "**/Cargo.toml"] 2 | 3 | [formatting] 4 | inline_table_expand = false 5 | -------------------------------------------------------------------------------- /deny.toml: -------------------------------------------------------------------------------- 1 | [licenses] 2 | allow = ["Apache-2.0", "MIT", "Unicode-3.0"] 3 | private = { ignore = true } 4 | 5 | [advisories] 6 | yanked = "deny" 7 | -------------------------------------------------------------------------------- /ci/core-test/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![no_std] 2 | 3 | use chrono::{TimeZone, Utc}; 4 | 5 | pub fn create_time() { 6 | let _ = Utc.with_ymd_and_hms(2019, 1, 1, 0, 0, 0).unwrap(); 7 | } 8 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/fuzz_format.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use libfuzzer_sys::fuzz_target; 3 | 4 | fuzz_target!(|data: (String, String)| { 5 | use chrono::prelude::*; 6 | let _ = DateTime::parse_from_str(&data.0, &data.1); 7 | }); 8 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | ### Thanks for contributing to chrono! 2 | 3 | If your feature is semver-compatible, please target the main branch; 4 | for semver-incompatible changes, please target the `0.5.x` branch. 5 | 6 | Please consider adding a test to ensure your bug fix/feature will not break in the future. 7 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/fuzz_reader.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | use libfuzzer_sys::fuzz_target; 3 | 4 | fuzz_target!(|data: &[u8]| { 5 | use chrono::prelude::*; 6 | if let Ok(data) = std::str::from_utf8(data) { 7 | let _ = DateTime::parse_from_rfc2822(data); 8 | let _ = DateTime::parse_from_rfc3339(data); 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /fuzz/README.md: -------------------------------------------------------------------------------- 1 | # Fuzzing Chrono 2 | To fuzz Chrono we rely on the [Cargo-fuzz](https://rust-fuzz.github.io/) project. 3 | 4 | To install cargo-fuzz: 5 | ``` 6 | cargo install cargo-fuzz 7 | ``` 8 | 9 | To run the Chrono fuzzer, navigate to the top directory of chrono and issue the following command: 10 | ``` 11 | cargo-fuzz run fuzz_reader 12 | ``` 13 | -------------------------------------------------------------------------------- /ci/core-test/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "core-test" 3 | version = "0.1.0" 4 | authors = [ 5 | "Kang Seonghoon ", 6 | "Brandon W Maister ", 7 | ] 8 | edition = "2018" 9 | 10 | [dependencies] 11 | chrono = { path = "../..", default-features = false, features = ["serde"] } 12 | 13 | [features] 14 | alloc = ["chrono/alloc"] 15 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "cargo" 4 | directory: "/" 5 | schedule: 6 | interval: "weekly" 7 | target-branch: "main" 8 | - package-ecosystem: "cargo" 9 | directory: "/fuzz/" 10 | schedule: 11 | interval: "weekly" 12 | target-branch: "main" 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | schedule: 16 | interval: "weekly" 17 | target-branch: "main" 18 | -------------------------------------------------------------------------------- /fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | 2 | [package] 3 | name = "chrono-fuzz" 4 | version = "0.0.0" 5 | authors = ["David Korczynski "] 6 | publish = false 7 | edition = "2018" 8 | 9 | [package.metadata] 10 | cargo-fuzz = true 11 | 12 | [dependencies] 13 | libfuzzer-sys = "0.4" 14 | 15 | [dependencies.chrono] 16 | path = ".." 17 | 18 | # Prevent this from interfering with workspaces 19 | [workspace] 20 | members = ["."] 21 | 22 | [[bin]] 23 | name = "fuzz_reader" 24 | path = "fuzz_targets/fuzz_reader.rs" 25 | 26 | [[bin]] 27 | name = "fuzz_format" 28 | path = "fuzz_targets/fuzz_format.rs" 29 | -------------------------------------------------------------------------------- /bench/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "benches" 3 | version = "0.1.0" 4 | edition = "2021" 5 | publish = false 6 | 7 | # Even as a `dev-dependency` Criterion and its dependencies can affect the MSRV of chrono. 8 | # But not when it lives in a separate crate :-). 9 | # See https://github.com/chronotope/chrono/pull/1104. 10 | 11 | [features] 12 | unstable-locales = ["chrono/unstable-locales"] 13 | 14 | [dependencies] 15 | chrono = { path = "..", features = ["__internal_bench", "serde"] } 16 | 17 | [[bench]] 18 | name = "chrono" 19 | harness = false 20 | 21 | [[bench]] 22 | name = "serde" 23 | harness = false 24 | 25 | [dev-dependencies] 26 | criterion = "0.5.0" 27 | serde_json = "1" 28 | -------------------------------------------------------------------------------- /CITATION.cff: -------------------------------------------------------------------------------- 1 | # Parser settings. 2 | cff-version: 1.2.0 3 | message: Please cite this crate using these information. 4 | 5 | # Version information. 6 | date-released: 2025-02-26 7 | version: 0.4.41 8 | 9 | # Project information. 10 | abstract: Date and time library for Rust 11 | authors: 12 | - alias: quodlibetor 13 | family-names: Maister 14 | given-names: Brandon W. 15 | - alias: djc 16 | family-names: Ochtman 17 | given-names: Dirkjan 18 | - alias: lifthrasiir 19 | family-names: Seonghoon 20 | given-names: Kang 21 | - alias: esheppa 22 | family-names: Sheppard 23 | given-names: Eric 24 | - alias: pitdicker 25 | family-names: Dicker 26 | given-names: Paul 27 | license: 28 | - Apache-2.0 29 | - MIT 30 | repository-artifact: https://crates.io/crates/chrono 31 | repository-code: https://github.com/chronotope/chrono 32 | title: chrono 33 | url: https://docs.rs/chrono 34 | -------------------------------------------------------------------------------- /bench/benches/serde.rs: -------------------------------------------------------------------------------- 1 | use criterion::{Criterion, black_box, criterion_group, criterion_main}; 2 | 3 | use chrono::NaiveDateTime; 4 | 5 | fn bench_ser_naivedatetime_string(c: &mut Criterion) { 6 | c.bench_function("bench_ser_naivedatetime_string", |b| { 7 | let dt: NaiveDateTime = "2000-01-01T00:00:00".parse().unwrap(); 8 | b.iter(|| { 9 | black_box(serde_json::to_string(&dt)).unwrap(); 10 | }); 11 | }); 12 | } 13 | 14 | fn bench_ser_naivedatetime_writer(c: &mut Criterion) { 15 | c.bench_function("bench_ser_naivedatetime_writer", |b| { 16 | let mut s: Vec = Vec::with_capacity(20); 17 | let dt: NaiveDateTime = "2000-01-01T00:00:00".parse().unwrap(); 18 | b.iter(|| { 19 | let s = &mut s; 20 | s.clear(); 21 | black_box(serde_json::to_writer(s, &dt)).unwrap(); 22 | }); 23 | }); 24 | } 25 | 26 | criterion_group!(benches, bench_ser_naivedatetime_writer, bench_ser_naivedatetime_string); 27 | criterion_main!(benches); 28 | -------------------------------------------------------------------------------- /tests/win_bindings.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | use windows_bindgen::bindgen; 3 | 4 | #[test] 5 | fn gen_bindings() { 6 | let existing = fs::read_to_string(BINDINGS).unwrap(); 7 | 8 | bindgen([ 9 | "--out", 10 | BINDINGS, 11 | "--flat", 12 | "--no-comment", 13 | "--no-deps", 14 | "--sys", 15 | "--filter", 16 | "GetTimeZoneInformationForYear", 17 | "SystemTimeToFileTime", 18 | "SystemTimeToTzSpecificLocalTime", 19 | "TzSpecificLocalTimeToSystemTime", 20 | ]) 21 | .unwrap(); 22 | 23 | // Check the output is the same as before. 24 | // Depending on the git configuration the file may have been checked out with `\r\n` newlines or 25 | // with `\n`. Compare line-by-line to ignore this difference. 26 | let mut new = fs::read_to_string(BINDINGS).unwrap(); 27 | if existing.contains("\r\n") && !new.contains("\r\n") { 28 | new = new.replace("\n", "\r\n"); 29 | } else if !existing.contains("\r\n") && new.contains("\r\n") { 30 | new = new.replace("\r\n", "\n"); 31 | } 32 | 33 | similar_asserts::assert_eq!(existing, new); 34 | if !new.lines().eq(existing.lines()) { 35 | panic!("generated file `{BINDINGS}` is changed."); 36 | } 37 | } 38 | 39 | const BINDINGS: &str = "src/offset/local/win_bindings.rs"; 40 | -------------------------------------------------------------------------------- /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | name: codecov 2 | 3 | env: 4 | # It's really `--all-features`, but not adding the mutually exclusive features from rkyv 5 | ALL_NON_EXCLUSIVE_FEATURES: --features "default unstable-locales rkyv-64 rkyv-validation serde arbitrary" 6 | 7 | on: 8 | push: 9 | branches: [main, 0.5.x] 10 | pull_request: 11 | jobs: 12 | # Run code coverage using cargo-llvm-cov then upload to codecov.io 13 | job_code_coverage: 14 | name: llvm-cov 15 | runs-on: ubuntu-latest 16 | environment: Coverage 17 | env: 18 | CARGO_TERM_COLOR: always 19 | steps: 20 | - uses: actions/checkout@v6 21 | # nightly is required for --doctests, see cargo-llvm-cov#2 22 | - uses: dtolnay/rust-toolchain@master 23 | with: 24 | toolchain: nightly 25 | components: rustfmt 26 | - name: Install cargo-llvm-cov 27 | uses: taiki-e/install-action@cargo-llvm-cov 28 | - name: Generate code coverage 29 | run: cargo +nightly llvm-cov ${{ env.ALL_NON_EXCLUSIVE_FEATURES }} --workspace --lcov --doctests --output-path lcov.info 30 | - name: Upload coverage to Codecov 31 | uses: codecov/codecov-action@v5 32 | env: 33 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 34 | with: 35 | files: lcov.info 36 | fail_ci_if_error: true 37 | -------------------------------------------------------------------------------- /src/offset/local/win_bindings.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case, non_upper_case_globals, non_camel_case_types, dead_code, clippy::all)] 2 | 3 | windows_link::link!("kernel32.dll" "system" fn GetTimeZoneInformationForYear(wyear : u16, pdtzi : *const DYNAMIC_TIME_ZONE_INFORMATION, ptzi : *mut TIME_ZONE_INFORMATION) -> BOOL); 4 | windows_link::link!("kernel32.dll" "system" fn SystemTimeToFileTime(lpsystemtime : *const SYSTEMTIME, lpfiletime : *mut FILETIME) -> BOOL); 5 | windows_link::link!("kernel32.dll" "system" fn SystemTimeToTzSpecificLocalTime(lptimezoneinformation : *const TIME_ZONE_INFORMATION, lpuniversaltime : *const SYSTEMTIME, lplocaltime : *mut SYSTEMTIME) -> BOOL); 6 | windows_link::link!("kernel32.dll" "system" fn TzSpecificLocalTimeToSystemTime(lptimezoneinformation : *const TIME_ZONE_INFORMATION, lplocaltime : *const SYSTEMTIME, lpuniversaltime : *mut SYSTEMTIME) -> BOOL); 7 | pub type BOOL = i32; 8 | #[repr(C)] 9 | #[derive(Clone, Copy)] 10 | pub struct DYNAMIC_TIME_ZONE_INFORMATION { 11 | pub Bias: i32, 12 | pub StandardName: [u16; 32], 13 | pub StandardDate: SYSTEMTIME, 14 | pub StandardBias: i32, 15 | pub DaylightName: [u16; 32], 16 | pub DaylightDate: SYSTEMTIME, 17 | pub DaylightBias: i32, 18 | pub TimeZoneKeyName: [u16; 128], 19 | pub DynamicDaylightTimeDisabled: bool, 20 | } 21 | impl Default for DYNAMIC_TIME_ZONE_INFORMATION { 22 | fn default() -> Self { 23 | unsafe { core::mem::zeroed() } 24 | } 25 | } 26 | #[repr(C)] 27 | #[derive(Clone, Copy, Default)] 28 | pub struct FILETIME { 29 | pub dwLowDateTime: u32, 30 | pub dwHighDateTime: u32, 31 | } 32 | #[repr(C)] 33 | #[derive(Clone, Copy, Default)] 34 | pub struct SYSTEMTIME { 35 | pub wYear: u16, 36 | pub wMonth: u16, 37 | pub wDayOfWeek: u16, 38 | pub wDay: u16, 39 | pub wHour: u16, 40 | pub wMinute: u16, 41 | pub wSecond: u16, 42 | pub wMilliseconds: u16, 43 | } 44 | #[repr(C)] 45 | #[derive(Clone, Copy)] 46 | pub struct TIME_ZONE_INFORMATION { 47 | pub Bias: i32, 48 | pub StandardName: [u16; 32], 49 | pub StandardDate: SYSTEMTIME, 50 | pub StandardBias: i32, 51 | pub DaylightName: [u16; 32], 52 | pub DaylightDate: SYSTEMTIME, 53 | pub DaylightBias: i32, 54 | } 55 | impl Default for TIME_ZONE_INFORMATION { 56 | fn default() -> Self { 57 | unsafe { core::mem::zeroed() } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | env: 4 | # It's really `--all-features`, but not adding the mutually exclusive features from rkyv 5 | ALL_NON_EXCLUSIVE_FEATURES: --features "default unstable-locales rkyv-64 rkyv-validation serde arbitrary" 6 | 7 | on: 8 | push: 9 | branches: [main, 0.5.x] 10 | pull_request: 11 | schedule: 12 | - cron: "38 6 * * 5" 13 | 14 | jobs: 15 | lint: 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v6 20 | - uses: dtolnay/rust-toolchain@stable 21 | with: 22 | targets: x86_64-unknown-linux-gnu, x86_64-pc-windows-msvc 23 | components: clippy, rustfmt 24 | - uses: Swatinem/rust-cache@v2 25 | - run: cargo fmt --check -- --color=always 26 | - run: cargo fmt --check --manifest-path fuzz/Cargo.toml 27 | - run: cargo fmt --check --manifest-path bench/Cargo.toml 28 | - run: | 29 | cargo clippy ${{ env.ALL_NON_EXCLUSIVE_FEATURES }} --all-targets --color=always \ 30 | -- -D warnings 31 | - run: | 32 | cargo clippy --target=x86_64-pc-windows-msvc --all-targets --color=always \ 33 | -- -D warnings 34 | - run: | 35 | cargo clippy --manifest-path fuzz/Cargo.toml --all-targets --color=always \ 36 | -- -D warnings 37 | - run: | 38 | cargo clippy --manifest-path bench/Cargo.toml --all-targets --color=always \ 39 | -- -D warnings 40 | env: 41 | RUSTFLAGS: "-Dwarnings" 42 | 43 | toml: 44 | runs-on: ubuntu-latest 45 | container: 46 | image: tamasfe/taplo:0.8.0 47 | steps: 48 | - uses: actions/checkout@v6 49 | - run: taplo lint 50 | - run: taplo fmt --check --diff 51 | 52 | cargo-deny: 53 | runs-on: ubuntu-latest 54 | steps: 55 | - uses: actions/checkout@v6 56 | - uses: EmbarkStudios/cargo-deny-action@v2 57 | 58 | check-doc: 59 | runs-on: ubuntu-latest 60 | steps: 61 | - uses: actions/checkout@v6 62 | - uses: dtolnay/rust-toolchain@stable 63 | - run: cargo install cargo-deadlinks 64 | - run: cargo deadlinks -- ${{ env.ALL_NON_EXCLUSIVE_FEATURES }} 65 | - run: cargo doc ${{ env.ALL_NON_EXCLUSIVE_FEATURES }} --no-deps 66 | env: 67 | RUSTDOCFLAGS: -Dwarnings 68 | 69 | cffconvert: 70 | runs-on: ubuntu-latest 71 | steps: 72 | - uses: actions/checkout@v6 73 | with: 74 | persist-credentials: false 75 | - uses: citation-file-format/cffconvert-github-action@2.0.0 76 | with: 77 | args: --validate 78 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "chrono" 3 | version = "0.4.42" 4 | description = "Date and time library for Rust" 5 | homepage = "https://github.com/chronotope/chrono" 6 | documentation = "https://docs.rs/chrono/" 7 | repository = "https://github.com/chronotope/chrono" 8 | keywords = ["date", "time", "calendar"] 9 | categories = ["date-and-time"] 10 | readme = "README.md" 11 | license = "MIT OR Apache-2.0" 12 | include = ["src/*", "tests/*.rs", "LICENSE.txt", "CITATION.cff"] 13 | edition = "2021" 14 | rust-version = "1.62.0" 15 | 16 | [lib] 17 | name = "chrono" 18 | 19 | [features] 20 | # Don't forget to adjust `ALL_NON_EXCLUSIVE_FEATURES` in CI scripts when adding a feature or an optional dependency. 21 | default = ["clock", "std", "oldtime", "wasmbind"] 22 | alloc = [] 23 | defmt = ["dep:defmt", "pure-rust-locales?/defmt"] 24 | libc = [] 25 | winapi = ["windows-link"] 26 | std = ["alloc"] 27 | clock = ["winapi", "iana-time-zone", "now"] 28 | now = ["std"] 29 | core-error = [] 30 | oldtime = [] 31 | wasmbind = ["wasm-bindgen", "js-sys"] 32 | unstable-locales = ["pure-rust-locales"] 33 | # Note that rkyv-16, rkyv-32, and rkyv-64 are mutually exclusive. 34 | rkyv = ["dep:rkyv", "rkyv/size_32"] 35 | rkyv-16 = ["dep:rkyv", "rkyv?/size_16"] 36 | rkyv-32 = ["dep:rkyv", "rkyv?/size_32"] 37 | rkyv-64 = ["dep:rkyv", "rkyv?/size_64"] 38 | rkyv-validation = ["rkyv?/validation"] 39 | # Features for internal use only: 40 | __internal_bench = [] 41 | 42 | [dependencies] 43 | num-traits = { version = "0.2", default-features = false } 44 | serde = { version = "1.0.99", default-features = false, optional = true } 45 | pure-rust-locales = { version = "0.8.2", optional = true } 46 | rkyv = { version = "0.7.43", optional = true, default-features = false } 47 | arbitrary = { version = "1.0.0", features = ["derive"], optional = true } 48 | defmt = { version = "1.0.1", optional = true } 49 | 50 | [target.'cfg(all(target_arch = "wasm32", not(any(target_os = "emscripten", target_os = "wasi"))))'.dependencies] 51 | wasm-bindgen = { version = "0.2", optional = true } 52 | js-sys = { version = "0.3", optional = true } # contains FFI bindings for the JS Date API 53 | 54 | [target.'cfg(windows)'.dependencies] 55 | windows-link = { version = "0.2", optional = true } 56 | 57 | [target.'cfg(unix)'.dependencies] 58 | iana-time-zone = { version = "0.1.45", optional = true, features = ["fallback"] } 59 | 60 | [dev-dependencies] 61 | serde_json = { version = "1" } 62 | serde_derive = { version = "1", default-features = false } 63 | similar-asserts = { version = "1.6.1" } 64 | bincode = { version = "1.3.0" } 65 | windows-bindgen = { version = "0.65" } # MSRV is 1.74 66 | 67 | [target.'cfg(all(target_arch = "wasm32", not(any(target_os = "emscripten", target_os = "wasi"))))'.dev-dependencies] 68 | wasm-bindgen-test = "0.3" 69 | 70 | [package.metadata.docs.rs] 71 | features = ["arbitrary", "rkyv", "serde", "unstable-locales"] 72 | rustdoc-args = ["--cfg", "docsrs"] 73 | 74 | [package.metadata.playground] 75 | features = ["serde"] 76 | -------------------------------------------------------------------------------- /tests/wasm.rs: -------------------------------------------------------------------------------- 1 | //! Run this test with: 2 | //! `env TZ="$(date +%z)" NOW="$(date +%s)" wasm-pack test --node -- --features wasmbind` 3 | //! 4 | //! The `TZ` and `NOW` variables are used to compare the results inside the WASM environment with 5 | //! the host system. 6 | //! The check will fail if the local timezone does not match one of the timezones defined below. 7 | 8 | #![cfg(all( 9 | target_arch = "wasm32", 10 | feature = "wasmbind", 11 | feature = "clock", 12 | not(any(target_os = "emscripten", target_os = "wasi")) 13 | ))] 14 | 15 | use chrono::prelude::*; 16 | use wasm_bindgen_test::*; 17 | 18 | #[wasm_bindgen_test] 19 | fn now() { 20 | let utc: DateTime = Utc::now(); 21 | let local: DateTime = Local::now(); 22 | 23 | // Ensure time set by the test script is correct 24 | let now = env!("NOW"); 25 | let actual = NaiveDateTime::parse_from_str(&now, "%s").unwrap().and_utc(); 26 | let diff = utc - actual; 27 | assert!( 28 | diff < chrono::TimeDelta::try_minutes(5).unwrap(), 29 | "expected {} - {} == {} < 5m (env var: {})", 30 | utc, 31 | actual, 32 | diff, 33 | now, 34 | ); 35 | 36 | let tz = env!("TZ"); 37 | eprintln!("testing with tz={}", tz); 38 | 39 | // Ensure offset retrieved when getting local time is correct 40 | let expected_offset = match tz { 41 | "ACST-9:30" => FixedOffset::east_opt(19 * 30 * 60).unwrap(), 42 | "Asia/Katmandu" => FixedOffset::east_opt(23 * 15 * 60).unwrap(), // No DST thankfully 43 | "EDT" | "EST4" | "-0400" => FixedOffset::east_opt(-4 * 60 * 60).unwrap(), 44 | "EST" | "-0500" => FixedOffset::east_opt(-5 * 60 * 60).unwrap(), 45 | "UTC0" | "+0000" => FixedOffset::east_opt(0).unwrap(), 46 | tz => panic!("unexpected TZ {}", tz), 47 | }; 48 | assert_eq!( 49 | &expected_offset, 50 | local.offset(), 51 | "expected: {:?} local: {:?}", 52 | expected_offset, 53 | local.offset(), 54 | ); 55 | } 56 | 57 | #[wasm_bindgen_test] 58 | fn from_is_exact() { 59 | let now = js_sys::Date::new_0(); 60 | 61 | let dt = DateTime::::from(now.clone()); 62 | 63 | assert_eq!(now.get_time() as i64, dt.timestamp_millis()); 64 | } 65 | 66 | #[wasm_bindgen_test] 67 | fn local_from_local_datetime() { 68 | let now = Local::now(); 69 | let ndt = now.naive_local(); 70 | let res = match Local.from_local_datetime(&ndt).single() { 71 | Some(v) => v, 72 | None => panic! {"Required for test!"}, 73 | }; 74 | assert_eq!(now, res); 75 | } 76 | 77 | #[wasm_bindgen_test] 78 | fn convert_all_parts_with_milliseconds() { 79 | let time: DateTime = "2020-12-01T03:01:55.974Z".parse().unwrap(); 80 | let js_date = js_sys::Date::from(time); 81 | 82 | assert_eq!(js_date.get_utc_full_year(), 2020); 83 | assert_eq!(js_date.get_utc_month(), 11); // months are numbered 0..=11 84 | assert_eq!(js_date.get_utc_date(), 1); 85 | assert_eq!(js_date.get_utc_hours(), 3); 86 | assert_eq!(js_date.get_utc_minutes(), 1); 87 | assert_eq!(js_date.get_utc_seconds(), 55); 88 | assert_eq!(js_date.get_utc_milliseconds(), 974); 89 | } 90 | -------------------------------------------------------------------------------- /src/format/locales.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "unstable-locales")] 2 | mod localized { 3 | use pure_rust_locales::{Locale, locale_match}; 4 | 5 | pub(crate) const fn default_locale() -> Locale { 6 | Locale::POSIX 7 | } 8 | 9 | pub(crate) const fn short_months(locale: Locale) -> &'static [&'static str] { 10 | locale_match!(locale => LC_TIME::ABMON) 11 | } 12 | 13 | pub(crate) const fn long_months(locale: Locale) -> &'static [&'static str] { 14 | locale_match!(locale => LC_TIME::MON) 15 | } 16 | 17 | pub(crate) const fn short_weekdays(locale: Locale) -> &'static [&'static str] { 18 | locale_match!(locale => LC_TIME::ABDAY) 19 | } 20 | 21 | pub(crate) const fn long_weekdays(locale: Locale) -> &'static [&'static str] { 22 | locale_match!(locale => LC_TIME::DAY) 23 | } 24 | 25 | pub(crate) const fn am_pm(locale: Locale) -> &'static [&'static str] { 26 | locale_match!(locale => LC_TIME::AM_PM) 27 | } 28 | 29 | pub(crate) const fn decimal_point(locale: Locale) -> &'static str { 30 | locale_match!(locale => LC_NUMERIC::DECIMAL_POINT) 31 | } 32 | 33 | pub(crate) const fn d_fmt(locale: Locale) -> &'static str { 34 | locale_match!(locale => LC_TIME::D_FMT) 35 | } 36 | 37 | pub(crate) const fn d_t_fmt(locale: Locale) -> &'static str { 38 | locale_match!(locale => LC_TIME::D_T_FMT) 39 | } 40 | 41 | pub(crate) const fn t_fmt(locale: Locale) -> &'static str { 42 | locale_match!(locale => LC_TIME::T_FMT) 43 | } 44 | 45 | pub(crate) const fn t_fmt_ampm(locale: Locale) -> &'static str { 46 | locale_match!(locale => LC_TIME::T_FMT_AMPM) 47 | } 48 | } 49 | 50 | #[cfg(feature = "unstable-locales")] 51 | pub(crate) use localized::*; 52 | #[cfg(feature = "unstable-locales")] 53 | pub use pure_rust_locales::Locale; 54 | 55 | #[cfg(not(feature = "unstable-locales"))] 56 | mod unlocalized { 57 | #[derive(Copy, Clone, Debug)] 58 | pub(crate) struct Locale; 59 | 60 | pub(crate) const fn default_locale() -> Locale { 61 | Locale 62 | } 63 | 64 | pub(crate) const fn short_months(_locale: Locale) -> &'static [&'static str] { 65 | &["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] 66 | } 67 | 68 | pub(crate) const fn long_months(_locale: Locale) -> &'static [&'static str] { 69 | &[ 70 | "January", 71 | "February", 72 | "March", 73 | "April", 74 | "May", 75 | "June", 76 | "July", 77 | "August", 78 | "September", 79 | "October", 80 | "November", 81 | "December", 82 | ] 83 | } 84 | 85 | pub(crate) const fn short_weekdays(_locale: Locale) -> &'static [&'static str] { 86 | &["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"] 87 | } 88 | 89 | pub(crate) const fn long_weekdays(_locale: Locale) -> &'static [&'static str] { 90 | &["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"] 91 | } 92 | 93 | pub(crate) const fn am_pm(_locale: Locale) -> &'static [&'static str] { 94 | &["AM", "PM"] 95 | } 96 | 97 | pub(crate) const fn decimal_point(_locale: Locale) -> &'static str { 98 | "." 99 | } 100 | } 101 | 102 | #[cfg(not(feature = "unstable-locales"))] 103 | pub(crate) use unlocalized::*; 104 | -------------------------------------------------------------------------------- /src/offset/local/tz_info/mod.rs: -------------------------------------------------------------------------------- 1 | #![deny(missing_docs)] 2 | #![allow(dead_code)] 3 | #![warn(unreachable_pub)] 4 | 5 | use std::num::ParseIntError; 6 | use std::str::Utf8Error; 7 | use std::time::SystemTimeError; 8 | use std::{error, fmt, io}; 9 | 10 | mod timezone; 11 | pub(crate) use timezone::TimeZone; 12 | 13 | mod parser; 14 | mod rule; 15 | 16 | /// Unified error type for everything in the crate 17 | #[derive(Debug)] 18 | pub(crate) enum Error { 19 | /// Date time error 20 | DateTime(&'static str), 21 | /// Local time type search error 22 | FindLocalTimeType(&'static str), 23 | /// Local time type error 24 | LocalTimeType(&'static str), 25 | /// Invalid slice for integer conversion 26 | InvalidSlice(&'static str), 27 | /// Invalid Tzif file 28 | InvalidTzFile(&'static str), 29 | /// Invalid TZ string 30 | InvalidTzString(&'static str), 31 | /// I/O error 32 | Io(io::Error), 33 | /// Out of range error 34 | OutOfRange(&'static str), 35 | /// Integer parsing error 36 | ParseInt(ParseIntError), 37 | /// Date time projection error 38 | ProjectDateTime(&'static str), 39 | /// System time error 40 | SystemTime(SystemTimeError), 41 | /// Time zone error 42 | TimeZone(&'static str), 43 | /// Transition rule error 44 | TransitionRule(&'static str), 45 | /// Unsupported Tzif file 46 | UnsupportedTzFile(&'static str), 47 | /// Unsupported TZ string 48 | UnsupportedTzString(&'static str), 49 | /// UTF-8 error 50 | Utf8(Utf8Error), 51 | } 52 | 53 | impl fmt::Display for Error { 54 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 55 | use Error::*; 56 | match self { 57 | DateTime(error) => write!(f, "invalid date time: {error}"), 58 | FindLocalTimeType(error) => error.fmt(f), 59 | LocalTimeType(error) => write!(f, "invalid local time type: {error}"), 60 | InvalidSlice(error) => error.fmt(f), 61 | InvalidTzString(error) => write!(f, "invalid TZ string: {error}"), 62 | InvalidTzFile(error) => error.fmt(f), 63 | Io(error) => error.fmt(f), 64 | OutOfRange(error) => error.fmt(f), 65 | ParseInt(error) => error.fmt(f), 66 | ProjectDateTime(error) => error.fmt(f), 67 | SystemTime(error) => error.fmt(f), 68 | TransitionRule(error) => write!(f, "invalid transition rule: {error}"), 69 | TimeZone(error) => write!(f, "invalid time zone: {error}"), 70 | UnsupportedTzFile(error) => error.fmt(f), 71 | UnsupportedTzString(error) => write!(f, "unsupported TZ string: {error}"), 72 | Utf8(error) => error.fmt(f), 73 | } 74 | } 75 | } 76 | 77 | impl error::Error for Error {} 78 | 79 | impl From for Error { 80 | fn from(error: io::Error) -> Self { 81 | Error::Io(error) 82 | } 83 | } 84 | 85 | impl From for Error { 86 | fn from(error: ParseIntError) -> Self { 87 | Error::ParseInt(error) 88 | } 89 | } 90 | 91 | impl From for Error { 92 | fn from(error: SystemTimeError) -> Self { 93 | Error::SystemTime(error) 94 | } 95 | } 96 | 97 | impl From for Error { 98 | fn from(error: Utf8Error) -> Self { 99 | Error::Utf8(error) 100 | } 101 | } 102 | 103 | /// Number of hours in one day 104 | const HOURS_PER_DAY: i64 = 24; 105 | /// Number of seconds in one hour 106 | const SECONDS_PER_HOUR: i64 = 3600; 107 | /// Number of seconds in one day 108 | const SECONDS_PER_DAY: i64 = SECONDS_PER_HOUR * HOURS_PER_DAY; 109 | /// Number of days in one week 110 | const DAYS_PER_WEEK: i64 = 7; 111 | 112 | /// Month days in a normal year 113 | const DAY_IN_MONTHS_NORMAL_YEAR: [i64; 12] = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; 114 | /// Cumulated month days in a normal year 115 | const CUMUL_DAY_IN_MONTHS_NORMAL_YEAR: [i64; 12] = 116 | [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334]; 117 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [Chrono][docsrs]: Timezone-aware date and time handling 2 | ======================================== 3 | 4 | [![Chrono GitHub Actions][gh-image]][gh-checks] 5 | [![Chrono on crates.io][cratesio-image]][cratesio] 6 | [![Chrono on docs.rs][docsrs-image]][docsrs] 7 | [![Chat][discord-image]][discord] 8 | [![codecov.io][codecov-img]][codecov-link] 9 | 10 | [gh-image]: https://github.com/chronotope/chrono/actions/workflows/test.yml/badge.svg?branch=main 11 | [gh-checks]: https://github.com/chronotope/chrono/actions/workflows/test.yml?query=branch%3Amain 12 | [cratesio-image]: https://img.shields.io/crates/v/chrono.svg 13 | [cratesio]: https://crates.io/crates/chrono 14 | [docsrs-image]: https://docs.rs/chrono/badge.svg 15 | [docsrs]: https://docs.rs/chrono 16 | [discord-image]: https://img.shields.io/discord/976380008299917365?logo=discord 17 | [discord]: https://discord.gg/sXpav4PS7M 18 | [codecov-img]: https://img.shields.io/codecov/c/github/chronotope/chrono?logo=codecov 19 | [codecov-link]: https://codecov.io/gh/chronotope/chrono 20 | 21 | Chrono aims to provide all functionality needed to do correct operations on dates and times in the 22 | [proleptic Gregorian calendar](https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar): 23 | 24 | * The [`DateTime`](https://docs.rs/chrono/latest/chrono/struct.DateTime.html) type is timezone-aware 25 | by default, with separate timezone-naive types. 26 | * Operations that may produce an invalid or ambiguous date and time return `Option` or 27 | [`MappedLocalTime`](https://docs.rs/chrono/latest/chrono/offset/enum.MappedLocalTime.html). 28 | * Configurable parsing and formatting with an `strftime` inspired date and time formatting syntax. 29 | * The [`Local`](https://docs.rs/chrono/latest/chrono/offset/struct.Local.html) timezone works with 30 | the current timezone of the OS. 31 | * Types and operations are implemented to be reasonably efficient. 32 | 33 | Timezone data is not shipped with chrono by default to limit binary sizes. Use the companion crate 34 | [Chrono-TZ](https://crates.io/crates/chrono-tz) or [`tzfile`](https://crates.io/crates/tzfile) for 35 | full timezone support. 36 | 37 | ## Documentation 38 | 39 | See [docs.rs](https://docs.rs/chrono/latest/chrono/) for the API reference. 40 | 41 | ## Limitations 42 | 43 | * Only the proleptic Gregorian calendar (i.e. extended to support older dates) is supported. 44 | * Date types are limited to about +/- 262,000 years from the common epoch. 45 | * Time types are limited to nanosecond accuracy. 46 | * Leap seconds can be represented, but Chrono does not fully support them. 47 | See [Leap Second Handling](https://docs.rs/chrono/latest/chrono/naive/struct.NaiveTime.html#leap-second-handling). 48 | 49 | ## Crate features 50 | 51 | Default features: 52 | 53 | * `alloc`: Enable features that depend on allocation (primarily string formatting). 54 | * `std`: Enables functionality that depends on the standard library. This is a superset of `alloc` 55 | and adds interoperation with standard library types and traits. 56 | * `clock`: Enables reading the local timezone (`Local`). This is a superset of `now`. 57 | * `now`: Enables reading the system time (`now`). 58 | * `wasmbind`: Interface with the JS Date API for the `wasm32` target. 59 | 60 | Optional features: 61 | 62 | * `serde`: Enable serialization/deserialization via [serde]. 63 | * `rkyv`: Deprecated, use the `rkyv-*` features. 64 | * `rkyv-16`: Enable serialization/deserialization via [rkyv], using 16-bit integers for integral `*size` types. 65 | * `rkyv-32`: Enable serialization/deserialization via [rkyv], using 32-bit integers for integral `*size` types. 66 | * `rkyv-64`: Enable serialization/deserialization via [rkyv], using 64-bit integers for integral `*size` types. 67 | * `rkyv-validation`: Enable rkyv validation support using `bytecheck`. 68 | * `arbitrary`: Construct arbitrary instances of a type with the Arbitrary crate. 69 | * `unstable-locales`: Enable localization. This adds various methods with a `_localized` suffix. 70 | The implementation and API may change or even be removed in a patch release. Feedback welcome. 71 | * `oldtime`: This feature no longer has any effect; it used to offer compatibility with the `time` 0.1 crate. 72 | 73 | Note: The `rkyv{,-16,-32,-64}` features are mutually exclusive. 74 | 75 | [serde]: https://github.com/serde-rs/serde 76 | [rkyv]: https://github.com/rkyv/rkyv 77 | 78 | ## Rust version requirements 79 | 80 | The Minimum Supported Rust Version (MSRV) is currently **Rust 1.61.0**. 81 | 82 | The MSRV is explicitly tested in CI. It may be bumped in minor releases, but this is not done 83 | lightly. 84 | 85 | ## License 86 | 87 | This project is licensed under either of 88 | 89 | * [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0) 90 | * [MIT License](https://opensource.org/licenses/MIT) 91 | 92 | at your option. 93 | -------------------------------------------------------------------------------- /src/offset/utc.rs: -------------------------------------------------------------------------------- 1 | // This is a part of Chrono. 2 | // See README.md and LICENSE.txt for details. 3 | 4 | //! The UTC (Coordinated Universal Time) time zone. 5 | 6 | use core::fmt; 7 | #[cfg(all( 8 | feature = "now", 9 | not(all( 10 | target_arch = "wasm32", 11 | feature = "wasmbind", 12 | not(any(target_os = "emscripten", target_os = "wasi", target_os = "linux")) 13 | )) 14 | ))] 15 | use std::time::{SystemTime, UNIX_EPOCH}; 16 | 17 | #[cfg(any(feature = "rkyv", feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"))] 18 | use rkyv::{Archive, Deserialize, Serialize}; 19 | 20 | use super::{FixedOffset, MappedLocalTime, Offset, TimeZone}; 21 | use crate::naive::{NaiveDate, NaiveDateTime}; 22 | #[cfg(feature = "now")] 23 | #[allow(deprecated)] 24 | use crate::{Date, DateTime}; 25 | 26 | /// The UTC time zone. This is the most efficient time zone when you don't need the local time. 27 | /// It is also used as an offset (which is also a dummy type). 28 | /// 29 | /// Using the [`TimeZone`](./trait.TimeZone.html) methods 30 | /// on the UTC struct is the preferred way to construct `DateTime` 31 | /// instances. 32 | /// 33 | /// # Example 34 | /// 35 | /// ``` 36 | /// use chrono::{DateTime, TimeZone, Utc}; 37 | /// 38 | /// let dt = DateTime::from_timestamp(61, 0).unwrap(); 39 | /// 40 | /// assert_eq!(Utc.timestamp_opt(61, 0).unwrap(), dt); 41 | /// assert_eq!(Utc.with_ymd_and_hms(1970, 1, 1, 0, 1, 1).unwrap(), dt); 42 | /// ``` 43 | #[derive(Copy, Clone, PartialEq, Eq, Hash)] 44 | #[cfg_attr( 45 | any(feature = "rkyv", feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"), 46 | derive(Archive, Deserialize, Serialize), 47 | archive(compare(PartialEq)), 48 | archive_attr(derive(Clone, Copy, PartialEq, Eq, Debug, Hash)) 49 | )] 50 | #[cfg_attr(feature = "rkyv-validation", archive(check_bytes))] 51 | #[cfg_attr(all(feature = "arbitrary", feature = "std"), derive(arbitrary::Arbitrary))] 52 | pub struct Utc; 53 | 54 | #[cfg(feature = "now")] 55 | impl Utc { 56 | /// Returns a `Date` which corresponds to the current date. 57 | #[deprecated( 58 | since = "0.4.23", 59 | note = "use `Utc::now()` instead, potentially with `.date_naive()`" 60 | )] 61 | #[allow(deprecated)] 62 | #[must_use] 63 | pub fn today() -> Date { 64 | Utc::now().date() 65 | } 66 | 67 | /// Returns a `DateTime` which corresponds to the current date and time in UTC. 68 | /// 69 | /// See also the similar [`Local::now()`] which returns `DateTime`, i.e. the local date 70 | /// and time including offset from UTC. 71 | /// 72 | /// [`Local::now()`]: crate::Local::now 73 | /// 74 | /// # Example 75 | /// 76 | /// ``` 77 | /// # #![allow(unused_variables)] 78 | /// # use chrono::{FixedOffset, Utc}; 79 | /// // Current time in UTC 80 | /// let now_utc = Utc::now(); 81 | /// 82 | /// // Current date in UTC 83 | /// let today_utc = now_utc.date_naive(); 84 | /// 85 | /// // Current time in some timezone (let's use +05:00) 86 | /// let offset = FixedOffset::east_opt(5 * 60 * 60).unwrap(); 87 | /// let now_with_offset = Utc::now().with_timezone(&offset); 88 | /// ``` 89 | #[cfg(not(all( 90 | target_arch = "wasm32", 91 | feature = "wasmbind", 92 | not(any(target_os = "emscripten", target_os = "wasi", target_os = "linux")) 93 | )))] 94 | #[must_use] 95 | pub fn now() -> DateTime { 96 | let now = 97 | SystemTime::now().duration_since(UNIX_EPOCH).expect("system time before Unix epoch"); 98 | DateTime::from_timestamp(now.as_secs() as i64, now.subsec_nanos()).unwrap() 99 | } 100 | 101 | /// Returns a `DateTime` which corresponds to the current date and time. 102 | #[cfg(all( 103 | target_arch = "wasm32", 104 | feature = "wasmbind", 105 | not(any(target_os = "emscripten", target_os = "wasi", target_os = "linux")) 106 | ))] 107 | #[must_use] 108 | pub fn now() -> DateTime { 109 | let now = js_sys::Date::new_0(); 110 | DateTime::::from(now) 111 | } 112 | } 113 | 114 | impl TimeZone for Utc { 115 | type Offset = Utc; 116 | 117 | fn from_offset(_state: &Utc) -> Utc { 118 | Utc 119 | } 120 | 121 | fn offset_from_local_date(&self, _local: &NaiveDate) -> MappedLocalTime { 122 | MappedLocalTime::Single(Utc) 123 | } 124 | fn offset_from_local_datetime(&self, _local: &NaiveDateTime) -> MappedLocalTime { 125 | MappedLocalTime::Single(Utc) 126 | } 127 | 128 | fn offset_from_utc_date(&self, _utc: &NaiveDate) -> Utc { 129 | Utc 130 | } 131 | fn offset_from_utc_datetime(&self, _utc: &NaiveDateTime) -> Utc { 132 | Utc 133 | } 134 | } 135 | 136 | impl Offset for Utc { 137 | fn fix(&self) -> FixedOffset { 138 | FixedOffset::east_opt(0).unwrap() 139 | } 140 | } 141 | 142 | impl fmt::Debug for Utc { 143 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 144 | write!(f, "Z") 145 | } 146 | } 147 | 148 | impl fmt::Display for Utc { 149 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 150 | write!(f, "UTC") 151 | } 152 | } 153 | 154 | #[cfg(feature = "defmt")] 155 | impl defmt::Format for Utc { 156 | fn format(&self, fmt: defmt::Formatter) { 157 | defmt::write!(fmt, "Z"); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/naive/time/serde.rs: -------------------------------------------------------------------------------- 1 | use super::NaiveTime; 2 | use core::fmt; 3 | use serde::{de, ser}; 4 | 5 | // TODO not very optimized for space (binary formats would want something better) 6 | // TODO round-trip for general leap seconds (not just those with second = 60) 7 | 8 | impl ser::Serialize for NaiveTime { 9 | fn serialize(&self, serializer: S) -> Result 10 | where 11 | S: ser::Serializer, 12 | { 13 | serializer.collect_str(&self) 14 | } 15 | } 16 | 17 | struct NaiveTimeVisitor; 18 | 19 | impl de::Visitor<'_> for NaiveTimeVisitor { 20 | type Value = NaiveTime; 21 | 22 | fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { 23 | formatter.write_str("a formatted time string") 24 | } 25 | 26 | fn visit_str(self, value: &str) -> Result 27 | where 28 | E: de::Error, 29 | { 30 | value.parse().map_err(E::custom) 31 | } 32 | } 33 | 34 | impl<'de> de::Deserialize<'de> for NaiveTime { 35 | fn deserialize(deserializer: D) -> Result 36 | where 37 | D: de::Deserializer<'de>, 38 | { 39 | deserializer.deserialize_str(NaiveTimeVisitor) 40 | } 41 | } 42 | 43 | #[cfg(test)] 44 | mod tests { 45 | use crate::NaiveTime; 46 | 47 | #[test] 48 | fn test_serde_serialize() { 49 | assert_eq!( 50 | serde_json::to_string(&NaiveTime::from_hms_opt(0, 0, 0).unwrap()).ok(), 51 | Some(r#""00:00:00""#.into()) 52 | ); 53 | assert_eq!( 54 | serde_json::to_string(&NaiveTime::from_hms_milli_opt(0, 0, 0, 950).unwrap()).ok(), 55 | Some(r#""00:00:00.950""#.into()) 56 | ); 57 | assert_eq!( 58 | serde_json::to_string(&NaiveTime::from_hms_milli_opt(0, 0, 59, 1_000).unwrap()).ok(), 59 | Some(r#""00:00:60""#.into()) 60 | ); 61 | assert_eq!( 62 | serde_json::to_string(&NaiveTime::from_hms_opt(0, 1, 2).unwrap()).ok(), 63 | Some(r#""00:01:02""#.into()) 64 | ); 65 | assert_eq!( 66 | serde_json::to_string(&NaiveTime::from_hms_nano_opt(3, 5, 7, 98765432).unwrap()).ok(), 67 | Some(r#""03:05:07.098765432""#.into()) 68 | ); 69 | assert_eq!( 70 | serde_json::to_string(&NaiveTime::from_hms_opt(7, 8, 9).unwrap()).ok(), 71 | Some(r#""07:08:09""#.into()) 72 | ); 73 | assert_eq!( 74 | serde_json::to_string(&NaiveTime::from_hms_micro_opt(12, 34, 56, 789).unwrap()).ok(), 75 | Some(r#""12:34:56.000789""#.into()) 76 | ); 77 | let leap = NaiveTime::from_hms_nano_opt(23, 59, 59, 1_999_999_999).unwrap(); 78 | assert_eq!(serde_json::to_string(&leap).ok(), Some(r#""23:59:60.999999999""#.into())); 79 | } 80 | 81 | #[test] 82 | fn test_serde_deserialize() { 83 | let from_str = serde_json::from_str::; 84 | 85 | assert_eq!(from_str(r#""00:00:00""#).ok(), Some(NaiveTime::from_hms_opt(0, 0, 0).unwrap())); 86 | assert_eq!(from_str(r#""0:0:0""#).ok(), Some(NaiveTime::from_hms_opt(0, 0, 0).unwrap())); 87 | assert_eq!( 88 | from_str(r#""00:00:00.950""#).ok(), 89 | Some(NaiveTime::from_hms_milli_opt(0, 0, 0, 950).unwrap()) 90 | ); 91 | assert_eq!( 92 | from_str(r#""0:0:0.95""#).ok(), 93 | Some(NaiveTime::from_hms_milli_opt(0, 0, 0, 950).unwrap()) 94 | ); 95 | assert_eq!( 96 | from_str(r#""00:00:60""#).ok(), 97 | Some(NaiveTime::from_hms_milli_opt(0, 0, 59, 1_000).unwrap()) 98 | ); 99 | assert_eq!(from_str(r#""00:01:02""#).ok(), Some(NaiveTime::from_hms_opt(0, 1, 2).unwrap())); 100 | assert_eq!( 101 | from_str(r#""03:05:07.098765432""#).ok(), 102 | Some(NaiveTime::from_hms_nano_opt(3, 5, 7, 98765432).unwrap()) 103 | ); 104 | assert_eq!(from_str(r#""07:08:09""#).ok(), Some(NaiveTime::from_hms_opt(7, 8, 9).unwrap())); 105 | assert_eq!( 106 | from_str(r#""12:34:56.000789""#).ok(), 107 | Some(NaiveTime::from_hms_micro_opt(12, 34, 56, 789).unwrap()) 108 | ); 109 | assert_eq!( 110 | from_str(r#""23:59:60.999999999""#).ok(), 111 | Some(NaiveTime::from_hms_nano_opt(23, 59, 59, 1_999_999_999).unwrap()) 112 | ); 113 | assert_eq!( 114 | from_str(r#""23:59:60.9999999999997""#).ok(), // excess digits are ignored 115 | Some(NaiveTime::from_hms_nano_opt(23, 59, 59, 1_999_999_999).unwrap()) 116 | ); 117 | 118 | // bad formats 119 | assert!(from_str(r#""""#).is_err()); 120 | assert!(from_str(r#""000000""#).is_err()); 121 | assert!(from_str(r#""00:00:61""#).is_err()); 122 | assert!(from_str(r#""00:60:00""#).is_err()); 123 | assert!(from_str(r#""24:00:00""#).is_err()); 124 | assert!(from_str(r#""23:59:59,1""#).is_err()); 125 | assert!(from_str(r#""012:34:56""#).is_err()); 126 | assert!(from_str(r#""hh:mm:ss""#).is_err()); 127 | assert!(from_str(r#"0"#).is_err()); 128 | assert!(from_str(r#"86399"#).is_err()); 129 | assert!(from_str(r#"{}"#).is_err()); 130 | } 131 | 132 | #[test] 133 | fn test_serde_bincode() { 134 | // Bincode is relevant to test separately from JSON because 135 | // it is not self-describing. 136 | use bincode::{deserialize, serialize}; 137 | 138 | let t = NaiveTime::from_hms_nano_opt(3, 5, 7, 98765432).unwrap(); 139 | let encoded = serialize(&t).unwrap(); 140 | let decoded: NaiveTime = deserialize(&encoded).unwrap(); 141 | assert_eq!(t, decoded); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /tests/dateutils.rs: -------------------------------------------------------------------------------- 1 | #![cfg(all(unix, feature = "clock", feature = "std"))] 2 | 3 | use std::{path, process, thread}; 4 | 5 | #[cfg(target_os = "linux")] 6 | use chrono::Days; 7 | use chrono::{Datelike, Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Timelike}; 8 | 9 | fn verify_against_date_command_local(path: &'static str, dt: NaiveDateTime) { 10 | let output = process::Command::new(path) 11 | .arg("-d") 12 | .arg(format!("{}-{:02}-{:02} {:02}:05:01", dt.year(), dt.month(), dt.day(), dt.hour())) 13 | .arg("+%Y-%m-%d %H:%M:%S %:z") 14 | .output() 15 | .unwrap(); 16 | 17 | let date_command_str = String::from_utf8(output.stdout).unwrap(); 18 | 19 | // The below would be preferred. At this stage neither earliest() or latest() 20 | // seems to be consistent with the output of the `date` command, so we simply 21 | // compare both. 22 | // let local = Local 23 | // .with_ymd_and_hms(year, month, day, hour, 5, 1) 24 | // // looks like the "date" command always returns a given time when it is ambiguous 25 | // .earliest(); 26 | 27 | // if let Some(local) = local { 28 | // assert_eq!(format!("{}\n", local), date_command_str); 29 | // } else { 30 | // // we are in a "Spring forward gap" due to DST, and so date also returns "" 31 | // assert_eq!("", date_command_str); 32 | // } 33 | 34 | // This is used while a decision is made whether the `date` output needs to 35 | // be exactly matched, or whether MappedLocalTime::Ambiguous should be handled 36 | // differently 37 | 38 | let date = NaiveDate::from_ymd_opt(dt.year(), dt.month(), dt.day()).unwrap(); 39 | match Local.from_local_datetime(&date.and_hms_opt(dt.hour(), 5, 1).unwrap()) { 40 | chrono::MappedLocalTime::Ambiguous(a, b) => { 41 | assert!(format!("{a}\n") == date_command_str || format!("{b}\n") == date_command_str) 42 | } 43 | chrono::MappedLocalTime::Single(a) => { 44 | assert_eq!(format!("{a}\n"), date_command_str); 45 | } 46 | chrono::MappedLocalTime::None => { 47 | assert_eq!("", date_command_str); 48 | } 49 | } 50 | } 51 | 52 | /// path to Unix `date` command. Should work on most Linux and Unixes. Not the 53 | /// path for MacOS (/bin/date) which uses a different version of `date` with 54 | /// different arguments (so it won't run which is okay). 55 | /// for testing only 56 | #[allow(dead_code)] 57 | #[cfg(not(target_os = "aix"))] 58 | const DATE_PATH: &str = "/usr/bin/date"; 59 | #[allow(dead_code)] 60 | #[cfg(target_os = "aix")] 61 | const DATE_PATH: &str = "/opt/freeware/bin/date"; 62 | 63 | #[cfg(test)] 64 | /// test helper to sanity check the date command behaves as expected 65 | /// asserts the command succeeded 66 | fn assert_run_date_version() { 67 | // note environment variable `LANG` 68 | match std::env::var_os("LANG") { 69 | Some(lang) => eprintln!("LANG: {lang:?}"), 70 | None => eprintln!("LANG not set"), 71 | } 72 | let out = process::Command::new(DATE_PATH).arg("--version").output().unwrap(); 73 | let stdout = String::from_utf8(out.stdout).unwrap(); 74 | let stderr = String::from_utf8(out.stderr).unwrap(); 75 | // note the `date` binary version 76 | eprintln!("command: {DATE_PATH:?} --version\nstdout: {stdout:?}\nstderr: {stderr:?}"); 77 | assert!(out.status.success(), "command failed: {DATE_PATH:?} --version"); 78 | } 79 | 80 | #[test] 81 | fn try_verify_against_date_command() { 82 | if !path::Path::new(DATE_PATH).exists() { 83 | eprintln!("date command {DATE_PATH:?} not found, skipping"); 84 | return; 85 | } 86 | assert_run_date_version(); 87 | 88 | eprintln!("Run command {DATE_PATH:?} for every hour from 1975 to 2077, skipping some years...",); 89 | 90 | let mut children = vec![]; 91 | for year in [1975, 1976, 1977, 2020, 2021, 2022, 2073, 2074, 2075, 2076, 2077].iter() { 92 | children.push(thread::spawn(|| { 93 | let mut date = NaiveDate::from_ymd_opt(*year, 1, 1).unwrap().and_time(NaiveTime::MIN); 94 | let end = NaiveDate::from_ymd_opt(*year + 1, 1, 1).unwrap().and_time(NaiveTime::MIN); 95 | while date <= end { 96 | verify_against_date_command_local(DATE_PATH, date); 97 | date += chrono::TimeDelta::try_hours(1).unwrap(); 98 | } 99 | })); 100 | } 101 | for child in children { 102 | // Wait for the thread to finish. Returns a result. 103 | let _ = child.join(); 104 | } 105 | } 106 | 107 | #[cfg(target_os = "linux")] 108 | fn verify_against_date_command_format_local(path: &'static str, dt: NaiveDateTime) { 109 | let required_format = 110 | "d%d D%D F%F H%H I%I j%j k%k l%l m%m M%M q%q S%S T%T u%u U%U w%w W%W X%X y%y Y%Y z%:z"; 111 | // a%a - depends from localization 112 | // A%A - depends from localization 113 | // b%b - depends from localization 114 | // B%B - depends from localization 115 | // h%h - depends from localization 116 | // c%c - depends from localization 117 | // p%p - depends from localization 118 | // r%r - depends from localization 119 | // x%x - fails, date is dd/mm/yyyy, chrono is dd/mm/yy, same as %D 120 | // Z%Z - too many ways to represent it, will most likely fail 121 | 122 | let output = process::Command::new(path) 123 | .env("LANG", "c") 124 | .env("LC_ALL", "c") 125 | .arg("-d") 126 | .arg(format!( 127 | "{}-{:02}-{:02} {:02}:{:02}:{:02}", 128 | dt.year(), 129 | dt.month(), 130 | dt.day(), 131 | dt.hour(), 132 | dt.minute(), 133 | dt.second() 134 | )) 135 | .arg(format!("+{required_format}")) 136 | .output() 137 | .unwrap(); 138 | 139 | let date_command_str = String::from_utf8(output.stdout).unwrap(); 140 | let date = NaiveDate::from_ymd_opt(dt.year(), dt.month(), dt.day()).unwrap(); 141 | let ldt = Local 142 | .from_local_datetime(&date.and_hms_opt(dt.hour(), dt.minute(), dt.second()).unwrap()) 143 | .unwrap(); 144 | let formatted_date = format!("{}\n", ldt.format(required_format)); 145 | assert_eq!(date_command_str, formatted_date); 146 | } 147 | 148 | #[test] 149 | #[cfg(target_os = "linux")] 150 | fn try_verify_against_date_command_format() { 151 | if !path::Path::new(DATE_PATH).exists() { 152 | eprintln!("date command {DATE_PATH:?} not found, skipping"); 153 | return; 154 | } 155 | assert_run_date_version(); 156 | 157 | let mut date = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap().and_hms_opt(12, 11, 13).unwrap(); 158 | while date.year() < 2008 { 159 | verify_against_date_command_format_local(DATE_PATH, date); 160 | date = date + Days::new(55); 161 | } 162 | } 163 | -------------------------------------------------------------------------------- /src/offset/local/unix.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2012-2014 The Rust Project Developers. See the COPYRIGHT 2 | // file at the top-level directory of this distribution and at 3 | // http://rust-lang.org/COPYRIGHT. 4 | // 5 | // Licensed under the Apache License, Version 2.0 or the MIT license 7 | // , at your 8 | // option. This file may not be copied, modified, or distributed 9 | // except according to those terms. 10 | 11 | use std::{cell::RefCell, collections::hash_map, env, fs, hash::Hasher, time::SystemTime}; 12 | 13 | use super::tz_info::TimeZone; 14 | use super::{FixedOffset, NaiveDateTime}; 15 | use crate::MappedLocalTime; 16 | 17 | pub(super) fn offset_from_utc_datetime(utc: &NaiveDateTime) -> MappedLocalTime { 18 | offset(utc, false) 19 | } 20 | 21 | pub(super) fn offset_from_local_datetime(local: &NaiveDateTime) -> MappedLocalTime { 22 | offset(local, true) 23 | } 24 | 25 | fn offset(d: &NaiveDateTime, local: bool) -> MappedLocalTime { 26 | TZ_INFO.with(|maybe_cache| { 27 | maybe_cache.borrow_mut().get_or_insert_with(Cache::default).offset(*d, local) 28 | }) 29 | } 30 | 31 | // we have to store the `Cache` in an option as it can't 32 | // be initialized in a static context. 33 | thread_local! { 34 | static TZ_INFO: RefCell> = Default::default(); 35 | } 36 | 37 | enum Source { 38 | LocalTime { mtime: SystemTime }, 39 | Environment { hash: u64 }, 40 | } 41 | 42 | impl Source { 43 | fn new(env_tz: Option<&str>) -> Source { 44 | match env_tz { 45 | Some(tz) => { 46 | let mut hasher = hash_map::DefaultHasher::new(); 47 | hasher.write(tz.as_bytes()); 48 | let hash = hasher.finish(); 49 | Source::Environment { hash } 50 | } 51 | None => match fs::symlink_metadata("/etc/localtime") { 52 | Ok(data) => Source::LocalTime { 53 | // we have to pick a sensible default when the mtime fails 54 | // by picking SystemTime::now() we raise the probability of 55 | // the cache being invalidated if/when the mtime starts working 56 | mtime: data.modified().unwrap_or_else(|_| SystemTime::now()), 57 | }, 58 | Err(_) => { 59 | // as above, now() should be a better default than some constant 60 | // TODO: see if we can improve caching in the case where the fallback is a valid timezone 61 | Source::LocalTime { mtime: SystemTime::now() } 62 | } 63 | }, 64 | } 65 | } 66 | } 67 | 68 | struct Cache { 69 | zone: TimeZone, 70 | source: Source, 71 | last_checked: SystemTime, 72 | } 73 | 74 | #[cfg(target_os = "aix")] 75 | const TZDB_LOCATION: &str = "/usr/share/lib/zoneinfo"; 76 | 77 | #[cfg(not(any(target_os = "android", target_os = "aix", target_env = "ohos")))] 78 | const TZDB_LOCATION: &str = "/usr/share/zoneinfo"; 79 | 80 | fn fallback_timezone() -> Option { 81 | let tz_name = iana_time_zone::get_timezone().ok()?; 82 | #[cfg(not(any(target_os = "android", target_env = "ohos")))] 83 | let bytes = fs::read(format!("{TZDB_LOCATION}/{tz_name}")).ok()?; 84 | #[cfg(any(target_os = "android", target_env = "ohos"))] 85 | let bytes = crate::offset::local::tz_data::for_zone(&tz_name).ok()??; 86 | TimeZone::from_tz_data(&bytes).ok() 87 | } 88 | 89 | impl Default for Cache { 90 | fn default() -> Cache { 91 | // default to UTC if no local timezone can be found 92 | let env_tz = env::var("TZ").ok(); 93 | let env_ref = env_tz.as_deref(); 94 | Cache { 95 | last_checked: SystemTime::now(), 96 | source: Source::new(env_ref), 97 | zone: current_zone(env_ref), 98 | } 99 | } 100 | } 101 | 102 | fn current_zone(var: Option<&str>) -> TimeZone { 103 | TimeZone::local(var).ok().or_else(fallback_timezone).unwrap_or_else(TimeZone::utc) 104 | } 105 | 106 | impl Cache { 107 | fn offset(&mut self, d: NaiveDateTime, local: bool) -> MappedLocalTime { 108 | let now = SystemTime::now(); 109 | 110 | match now.duration_since(self.last_checked) { 111 | // If the cache has been around for less than a second then we reuse it 112 | // unconditionally. This is a reasonable tradeoff because the timezone 113 | // generally won't be changing _that_ often, but if the time zone does 114 | // change, it will reflect sufficiently quickly from an application 115 | // user's perspective. 116 | Ok(d) if d.as_secs() < 1 => (), 117 | Ok(_) | Err(_) => { 118 | let env_tz = env::var("TZ").ok(); 119 | let env_ref = env_tz.as_deref(); 120 | let new_source = Source::new(env_ref); 121 | 122 | let out_of_date = match (&self.source, &new_source) { 123 | // change from env to file or file to env, must recreate the zone 124 | (Source::Environment { .. }, Source::LocalTime { .. }) 125 | | (Source::LocalTime { .. }, Source::Environment { .. }) => true, 126 | // stay as file, but mtime has changed 127 | (Source::LocalTime { mtime: old_mtime }, Source::LocalTime { mtime }) 128 | if old_mtime != mtime => 129 | { 130 | true 131 | } 132 | // stay as env, but hash of variable has changed 133 | (Source::Environment { hash: old_hash }, Source::Environment { hash }) 134 | if old_hash != hash => 135 | { 136 | true 137 | } 138 | // cache can be reused 139 | _ => false, 140 | }; 141 | 142 | if out_of_date { 143 | self.zone = current_zone(env_ref); 144 | } 145 | 146 | self.last_checked = now; 147 | self.source = new_source; 148 | } 149 | } 150 | 151 | if !local { 152 | let offset = self 153 | .zone 154 | .find_local_time_type(d.and_utc().timestamp()) 155 | .expect("unable to select local time type") 156 | .offset(); 157 | 158 | return match FixedOffset::east_opt(offset) { 159 | Some(offset) => MappedLocalTime::Single(offset), 160 | None => MappedLocalTime::None, 161 | }; 162 | } 163 | 164 | // we pass through the year as the year of a local point in time must either be valid in that locale, or 165 | // the entire time was skipped in which case we will return MappedLocalTime::None anyway. 166 | self.zone 167 | .find_local_time_type_from_local(d) 168 | .expect("unable to select local time type") 169 | .and_then(|o| FixedOffset::east_opt(o.offset())) 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: All Tests and Builds 2 | 3 | env: 4 | # It's really `--all-features`, but not adding the mutually exclusive features from rkyv 5 | ALL_NON_EXCLUSIVE_FEATURES: --features "default unstable-locales rkyv-32 rkyv-validation serde arbitrary" 6 | 7 | on: 8 | push: 9 | branches: [main, 0.5.x] 10 | pull_request: 11 | 12 | jobs: 13 | timezones: 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest, macos-latest, windows-latest] 17 | tz: ["ACST-9:30", "EST4", "UTC0", "Asia/Katmandu"] 18 | runs-on: ${{ matrix.os }} 19 | steps: 20 | - uses: actions/checkout@v6 21 | - uses: dtolnay/rust-toolchain@stable 22 | with: 23 | components: rustfmt 24 | - uses: Swatinem/rust-cache@v2 25 | - run: cargo test ${{ env.ALL_NON_EXCLUSIVE_FEATURES }} --color=always -- --color=always 26 | 27 | # later this may be able to be included with the below 28 | # kept separate for now as the following don't compile on 1.60 29 | # * arbitrary (requires 1.63 as of v1.3.0) 30 | rust_msrv: 31 | strategy: 32 | matrix: 33 | os: [ubuntu-latest] 34 | runs-on: ${{ matrix.os }} 35 | steps: 36 | - uses: actions/checkout@v6 37 | - uses: dtolnay/rust-toolchain@master 38 | with: 39 | toolchain: "1.62.0" 40 | - uses: Swatinem/rust-cache@v2 41 | # run --lib and --doc to avoid the long running integration tests 42 | # which are run elsewhere 43 | - run: | 44 | cargo check --lib --features unstable-locales,wasmbind,oldtime,clock,winapi,serde 45 | 46 | rust_versions: 47 | strategy: 48 | matrix: 49 | os: [ubuntu-latest] 50 | rust_version: ["stable", "beta", "nightly"] 51 | runs-on: ${{ matrix.os }} 52 | steps: 53 | - uses: actions/checkout@v6 54 | - uses: dtolnay/rust-toolchain@master 55 | with: 56 | toolchain: ${{ matrix.rust_version }} 57 | components: rustfmt 58 | - uses: Swatinem/rust-cache@v2 59 | - run: cargo check --manifest-path bench/Cargo.toml --benches 60 | - run: cargo check --manifest-path fuzz/Cargo.toml --all-targets 61 | # run --lib and --doc to avoid the long running integration tests 62 | # which are run elsewhere 63 | - run: cargo test --lib ${{ env.ALL_NON_EXCLUSIVE_FEATURES }} --color=always -- --color=always 64 | - run: cargo test --doc ${{ env.ALL_NON_EXCLUSIVE_FEATURES }} --color=always -- --color=always 65 | 66 | features_check: 67 | strategy: 68 | matrix: 69 | os: [ubuntu-latest] 70 | runs-on: ${{ matrix.os }} 71 | steps: 72 | - uses: actions/checkout@v6 73 | - uses: dtolnay/rust-toolchain@stable 74 | with: 75 | components: rustfmt 76 | - uses: taiki-e/install-action@cargo-hack 77 | - uses: Swatinem/rust-cache@v2 78 | - run: | 79 | cargo hack check --feature-powerset --optional-deps arbitrary,serde \ 80 | --skip __internal_bench,iana-time-zone,oldtime,pure-rust-locales,libc,winapi,rkyv-validation,wasmbind \ 81 | --mutually-exclusive-features arbitrary,rkyv,rkyv-16,rkyv-32,rkyv-64,serde \ 82 | --all-targets 83 | # run using `bash` on all platforms for consistent 84 | # line-continuation marks 85 | shell: bash 86 | env: 87 | RUSTFLAGS: "-D warnings" 88 | - run: cargo test --no-default-features 89 | - run: cargo test --no-default-features --features=alloc 90 | - run: cargo test --no-default-features --features=unstable-locales 91 | - run: cargo test --no-default-features --features=alloc,unstable-locales 92 | - run: cargo test --no-default-features --features=now 93 | 94 | no_std: 95 | strategy: 96 | matrix: 97 | os: [ubuntu-latest] 98 | target: [thumbv6m-none-eabi, x86_64-fortanix-unknown-sgx] 99 | runs-on: ${{ matrix.os }} 100 | steps: 101 | - uses: actions/checkout@v6 102 | - uses: dtolnay/rust-toolchain@stable 103 | with: 104 | targets: ${{ matrix.target }} 105 | - uses: Swatinem/rust-cache@v2 106 | - run: cargo build --target ${{ matrix.target }} --color=always 107 | working-directory: ./ci/core-test 108 | 109 | alternative_targets: 110 | strategy: 111 | matrix: 112 | os: [ubuntu-latest] 113 | target: 114 | [ 115 | wasm32-unknown-emscripten, 116 | aarch64-apple-ios, 117 | aarch64-linux-android, 118 | ] 119 | runs-on: ${{ matrix.os }} 120 | steps: 121 | - uses: actions/checkout@v6 122 | - uses: dtolnay/rust-toolchain@stable 123 | with: 124 | targets: ${{ matrix.target }} 125 | - uses: Swatinem/rust-cache@v2 126 | - run: cargo build --target ${{ matrix.target }} --color=always 127 | 128 | test_wasm: 129 | strategy: 130 | matrix: 131 | os: [ubuntu-latest] 132 | runs-on: ${{ matrix.os }} 133 | steps: 134 | - uses: actions/checkout@v6 135 | - uses: dtolnay/rust-toolchain@stable 136 | with: 137 | targets: wasm32-unknown-unknown 138 | components: rustfmt 139 | - uses: Swatinem/rust-cache@v2 140 | - uses: actions/setup-node@v6 141 | - uses: jetli/wasm-pack-action@v0.4.0 142 | # The `TZ` and `NOW` variables are used to compare the results inside the WASM environment 143 | # with the host system. 144 | - run: TZ="$(date +%z)" NOW="$(date +%s)" wasm-pack test --node -- --features wasmbind 145 | 146 | test_wasip1: 147 | strategy: 148 | matrix: 149 | os: [ubuntu-latest] 150 | target: [wasm32-wasip1] 151 | runs-on: ${{ matrix.os }} 152 | steps: 153 | - uses: actions/checkout@v6 154 | - uses: dtolnay/rust-toolchain@stable 155 | with: 156 | targets: ${{ matrix.target }} 157 | - uses: Swatinem/rust-cache@v2 158 | # We can't use `--all-features` because rkyv uses the mutually-exclusive-feature pattern 159 | - run: cargo check --target ${{ matrix.target }} --all-targets --features=serde,unstable-locales 160 | 161 | cross-targets: 162 | strategy: 163 | matrix: 164 | target: 165 | - x86_64-unknown-illumos 166 | runs-on: ubuntu-latest 167 | steps: 168 | - uses: actions/checkout@v6 169 | - run: cargo install cross 170 | - uses: Swatinem/rust-cache@v2 171 | - run: cross check --target ${{ matrix.target }} 172 | 173 | cross-tests: 174 | strategy: 175 | matrix: 176 | os: [ubuntu-latest] 177 | runs-on: ${{ matrix.os }} 178 | steps: 179 | - uses: actions/checkout@v6 180 | - run: cargo install cross 181 | - uses: Swatinem/rust-cache@v2 182 | - run: cross test --lib ${{ env.ALL_NON_EXCLUSIVE_FEATURES }} --target i686-unknown-linux-gnu --color=always 183 | - run: cross test --doc ${{ env.ALL_NON_EXCLUSIVE_FEATURES }} --target i686-unknown-linux-gnu --color=always 184 | - run: cross test --lib ${{ env.ALL_NON_EXCLUSIVE_FEATURES }} --target i686-unknown-linux-musl --color=always 185 | - run: cross test --doc ${{ env.ALL_NON_EXCLUSIVE_FEATURES }} --target i686-unknown-linux-musl --color=always 186 | 187 | check-docs: 188 | runs-on: ubuntu-latest 189 | steps: 190 | - uses: actions/checkout@v6 191 | - uses: dtolnay/rust-toolchain@nightly 192 | - run: cargo +nightly doc ${{ env.ALL_NON_EXCLUSIVE_FEATURES }} --no-deps 193 | env: 194 | RUSTDOCFLAGS: "-D warnings --cfg docsrs" 195 | -------------------------------------------------------------------------------- /src/naive/isoweek.rs: -------------------------------------------------------------------------------- 1 | // This is a part of Chrono. 2 | // See README.md and LICENSE.txt for details. 3 | 4 | //! ISO 8601 week. 5 | 6 | use core::fmt; 7 | 8 | use super::internals::YearFlags; 9 | 10 | #[cfg(any(feature = "rkyv", feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"))] 11 | use rkyv::{Archive, Deserialize, Serialize}; 12 | 13 | /// ISO 8601 week. 14 | /// 15 | /// This type, combined with [`Weekday`](../enum.Weekday.html), 16 | /// constitutes the ISO 8601 [week date](./struct.NaiveDate.html#week-date). 17 | /// One can retrieve this type from the existing [`Datelike`](../trait.Datelike.html) types 18 | /// via the [`Datelike::iso_week`](../trait.Datelike.html#tymethod.iso_week) method. 19 | #[derive(PartialEq, Eq, PartialOrd, Ord, Copy, Clone, Hash)] 20 | #[cfg_attr( 21 | any(feature = "rkyv", feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"), 22 | derive(Archive, Deserialize, Serialize), 23 | archive(compare(PartialEq, PartialOrd)), 24 | archive_attr(derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)) 25 | )] 26 | #[cfg_attr(feature = "rkyv-validation", archive(check_bytes))] 27 | pub struct IsoWeek { 28 | // Note that this allows for larger year range than `NaiveDate`. 29 | // This is crucial because we have an edge case for the first and last week supported, 30 | // which year number might not match the calendar year number. 31 | ywf: i32, // (year << 10) | (week << 4) | flag 32 | } 33 | 34 | impl IsoWeek { 35 | /// Returns the corresponding `IsoWeek` from the year and the `Of` internal value. 36 | // 37 | // Internal use only. We don't expose the public constructor for `IsoWeek` for now 38 | // because the year range for the week date and the calendar date do not match, and 39 | // it is confusing to have a date that is out of range in one and not in another. 40 | // Currently we sidestep this issue by making `IsoWeek` fully dependent of `Datelike`. 41 | pub(super) fn from_yof(year: i32, ordinal: u32, year_flags: YearFlags) -> Self { 42 | let rawweek = (ordinal + year_flags.isoweek_delta()) / 7; 43 | let (year, week) = if rawweek < 1 { 44 | // previous year 45 | let prevlastweek = YearFlags::from_year(year - 1).nisoweeks(); 46 | (year - 1, prevlastweek) 47 | } else { 48 | let lastweek = year_flags.nisoweeks(); 49 | if rawweek > lastweek { 50 | // next year 51 | (year + 1, 1) 52 | } else { 53 | (year, rawweek) 54 | } 55 | }; 56 | let flags = YearFlags::from_year(year); 57 | IsoWeek { ywf: (year << 10) | (week << 4) as i32 | i32::from(flags.0) } 58 | } 59 | 60 | /// Returns the year number for this ISO week. 61 | /// 62 | /// # Example 63 | /// 64 | /// ``` 65 | /// use chrono::{Datelike, NaiveDate, Weekday}; 66 | /// 67 | /// let d = NaiveDate::from_isoywd_opt(2015, 1, Weekday::Mon).unwrap(); 68 | /// assert_eq!(d.iso_week().year(), 2015); 69 | /// ``` 70 | /// 71 | /// This year number might not match the calendar year number. 72 | /// Continuing the example... 73 | /// 74 | /// ``` 75 | /// # use chrono::{NaiveDate, Datelike, Weekday}; 76 | /// # let d = NaiveDate::from_isoywd_opt(2015, 1, Weekday::Mon).unwrap(); 77 | /// assert_eq!(d.year(), 2014); 78 | /// assert_eq!(d, NaiveDate::from_ymd_opt(2014, 12, 29).unwrap()); 79 | /// ``` 80 | #[inline] 81 | pub const fn year(&self) -> i32 { 82 | self.ywf >> 10 83 | } 84 | 85 | /// Returns the ISO week number starting from 1. 86 | /// 87 | /// The return value ranges from 1 to 53. (The last week of year differs by years.) 88 | /// 89 | /// # Example 90 | /// 91 | /// ``` 92 | /// use chrono::{Datelike, NaiveDate, Weekday}; 93 | /// 94 | /// let d = NaiveDate::from_isoywd_opt(2015, 15, Weekday::Mon).unwrap(); 95 | /// assert_eq!(d.iso_week().week(), 15); 96 | /// ``` 97 | #[inline] 98 | pub const fn week(&self) -> u32 { 99 | ((self.ywf >> 4) & 0x3f) as u32 100 | } 101 | 102 | /// Returns the ISO week number starting from 0. 103 | /// 104 | /// The return value ranges from 0 to 52. (The last week of year differs by years.) 105 | /// 106 | /// # Example 107 | /// 108 | /// ``` 109 | /// use chrono::{Datelike, NaiveDate, Weekday}; 110 | /// 111 | /// let d = NaiveDate::from_isoywd_opt(2015, 15, Weekday::Mon).unwrap(); 112 | /// assert_eq!(d.iso_week().week0(), 14); 113 | /// ``` 114 | #[inline] 115 | pub const fn week0(&self) -> u32 { 116 | ((self.ywf >> 4) & 0x3f) as u32 - 1 117 | } 118 | } 119 | 120 | /// The `Debug` output of the ISO week `w` is the same as 121 | /// [`d.format("%G-W%V")`](../format/strftime/index.html) 122 | /// where `d` is any `NaiveDate` value in that week. 123 | /// 124 | /// # Example 125 | /// 126 | /// ``` 127 | /// use chrono::{Datelike, NaiveDate}; 128 | /// 129 | /// assert_eq!( 130 | /// format!("{:?}", NaiveDate::from_ymd_opt(2015, 9, 5).unwrap().iso_week()), 131 | /// "2015-W36" 132 | /// ); 133 | /// assert_eq!(format!("{:?}", NaiveDate::from_ymd_opt(0, 1, 3).unwrap().iso_week()), "0000-W01"); 134 | /// assert_eq!( 135 | /// format!("{:?}", NaiveDate::from_ymd_opt(9999, 12, 31).unwrap().iso_week()), 136 | /// "9999-W52" 137 | /// ); 138 | /// ``` 139 | /// 140 | /// ISO 8601 requires an explicit sign for years before 1 BCE or after 9999 CE. 141 | /// 142 | /// ``` 143 | /// # use chrono::{NaiveDate, Datelike}; 144 | /// assert_eq!(format!("{:?}", NaiveDate::from_ymd_opt(0, 1, 2).unwrap().iso_week()), "-0001-W52"); 145 | /// assert_eq!( 146 | /// format!("{:?}", NaiveDate::from_ymd_opt(10000, 12, 31).unwrap().iso_week()), 147 | /// "+10000-W52" 148 | /// ); 149 | /// ``` 150 | impl fmt::Debug for IsoWeek { 151 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 152 | let year = self.year(); 153 | let week = self.week(); 154 | if (0..=9999).contains(&year) { 155 | write!(f, "{year:04}-W{week:02}") 156 | } else { 157 | // ISO 8601 requires the explicit sign for out-of-range years 158 | write!(f, "{year:+05}-W{week:02}") 159 | } 160 | } 161 | } 162 | 163 | #[cfg(feature = "defmt")] 164 | impl defmt::Format for IsoWeek { 165 | fn format(&self, fmt: defmt::Formatter) { 166 | let year = self.year(); 167 | let week = self.week(); 168 | if (0..=9999).contains(&year) { 169 | defmt::write!(fmt, "{:04}-W{:02}", year, week) 170 | } else { 171 | // ISO 8601 requires the explicit sign for out-of-range years 172 | let sign = ['+', '-'][(year < 0) as usize]; 173 | defmt::write!(fmt, "{}{:05}-W{:02}", sign, year.abs(), week) 174 | } 175 | } 176 | } 177 | 178 | #[cfg(test)] 179 | mod tests { 180 | #[cfg(feature = "rkyv-validation")] 181 | use super::IsoWeek; 182 | use crate::Datelike; 183 | use crate::naive::date::{self, NaiveDate}; 184 | 185 | #[test] 186 | fn test_iso_week_extremes() { 187 | let minweek = NaiveDate::MIN.iso_week(); 188 | let maxweek = NaiveDate::MAX.iso_week(); 189 | 190 | assert_eq!(minweek.year(), date::MIN_YEAR); 191 | assert_eq!(minweek.week(), 1); 192 | assert_eq!(minweek.week0(), 0); 193 | #[cfg(feature = "alloc")] 194 | assert_eq!(format!("{minweek:?}"), NaiveDate::MIN.format("%G-W%V").to_string()); 195 | 196 | assert_eq!(maxweek.year(), date::MAX_YEAR + 1); 197 | assert_eq!(maxweek.week(), 1); 198 | assert_eq!(maxweek.week0(), 0); 199 | #[cfg(feature = "alloc")] 200 | assert_eq!(format!("{maxweek:?}"), NaiveDate::MAX.format("%G-W%V").to_string()); 201 | } 202 | 203 | #[test] 204 | fn test_iso_week_equivalence_for_first_week() { 205 | let monday = NaiveDate::from_ymd_opt(2024, 12, 30).unwrap(); 206 | let friday = NaiveDate::from_ymd_opt(2025, 1, 3).unwrap(); 207 | 208 | assert_eq!(monday.iso_week(), friday.iso_week()); 209 | } 210 | 211 | #[test] 212 | fn test_iso_week_equivalence_for_last_week() { 213 | let monday = NaiveDate::from_ymd_opt(2026, 12, 28).unwrap(); 214 | let friday = NaiveDate::from_ymd_opt(2027, 1, 1).unwrap(); 215 | 216 | assert_eq!(monday.iso_week(), friday.iso_week()); 217 | } 218 | 219 | #[test] 220 | fn test_iso_week_ordering_for_first_week() { 221 | let monday = NaiveDate::from_ymd_opt(2024, 12, 30).unwrap(); 222 | let friday = NaiveDate::from_ymd_opt(2025, 1, 3).unwrap(); 223 | 224 | assert!(monday.iso_week() >= friday.iso_week()); 225 | assert!(monday.iso_week() <= friday.iso_week()); 226 | } 227 | 228 | #[test] 229 | fn test_iso_week_ordering_for_last_week() { 230 | let monday = NaiveDate::from_ymd_opt(2026, 12, 28).unwrap(); 231 | let friday = NaiveDate::from_ymd_opt(2027, 1, 1).unwrap(); 232 | 233 | assert!(monday.iso_week() >= friday.iso_week()); 234 | assert!(monday.iso_week() <= friday.iso_week()); 235 | } 236 | 237 | #[test] 238 | #[cfg(feature = "rkyv-validation")] 239 | fn test_rkyv_validation() { 240 | let minweek = NaiveDate::MIN.iso_week(); 241 | let bytes = rkyv::to_bytes::<_, 4>(&minweek).unwrap(); 242 | assert_eq!(rkyv::from_bytes::(&bytes).unwrap(), minweek); 243 | 244 | let maxweek = NaiveDate::MAX.iso_week(); 245 | let bytes = rkyv::to_bytes::<_, 4>(&maxweek).unwrap(); 246 | assert_eq!(rkyv::from_bytes::(&bytes).unwrap(), maxweek); 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /src/offset/fixed.rs: -------------------------------------------------------------------------------- 1 | // This is a part of Chrono. 2 | // See README.md and LICENSE.txt for details. 3 | 4 | //! The time zone which has a fixed offset from UTC. 5 | 6 | use core::fmt; 7 | use core::str::FromStr; 8 | 9 | #[cfg(any(feature = "rkyv", feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"))] 10 | use rkyv::{Archive, Deserialize, Serialize}; 11 | 12 | use super::{MappedLocalTime, Offset, TimeZone}; 13 | use crate::format::{OUT_OF_RANGE, ParseError, scan}; 14 | use crate::naive::{NaiveDate, NaiveDateTime}; 15 | 16 | /// The time zone with fixed offset, from UTC-23:59:59 to UTC+23:59:59. 17 | /// 18 | /// Using the [`TimeZone`](./trait.TimeZone.html) methods 19 | /// on a `FixedOffset` struct is the preferred way to construct 20 | /// `DateTime` instances. See the [`east_opt`](#method.east_opt) and 21 | /// [`west_opt`](#method.west_opt) methods for examples. 22 | #[derive(PartialEq, Eq, Hash, Copy, Clone)] 23 | #[cfg_attr( 24 | any(feature = "rkyv", feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"), 25 | derive(Archive, Deserialize, Serialize), 26 | archive(compare(PartialEq)), 27 | archive_attr(derive(Clone, Copy, PartialEq, Eq, Hash, Debug)) 28 | )] 29 | #[cfg_attr(feature = "rkyv-validation", archive(check_bytes))] 30 | pub struct FixedOffset { 31 | local_minus_utc: i32, 32 | } 33 | 34 | impl FixedOffset { 35 | /// Makes a new `FixedOffset` for the Eastern Hemisphere with given timezone difference. 36 | /// The negative `secs` means the Western Hemisphere. 37 | /// 38 | /// Panics on the out-of-bound `secs`. 39 | #[deprecated(since = "0.4.23", note = "use `east_opt()` instead")] 40 | #[must_use] 41 | pub fn east(secs: i32) -> FixedOffset { 42 | FixedOffset::east_opt(secs).expect("FixedOffset::east out of bounds") 43 | } 44 | 45 | /// Makes a new `FixedOffset` for the Eastern Hemisphere with given timezone difference. 46 | /// The negative `secs` means the Western Hemisphere. 47 | /// 48 | /// Returns `None` on the out-of-bound `secs`. 49 | /// 50 | /// # Example 51 | /// 52 | /// ``` 53 | /// # #[cfg(feature = "alloc")] { 54 | /// use chrono::{FixedOffset, TimeZone}; 55 | /// let hour = 3600; 56 | /// let datetime = 57 | /// FixedOffset::east_opt(5 * hour).unwrap().with_ymd_and_hms(2016, 11, 08, 0, 0, 0).unwrap(); 58 | /// assert_eq!(&datetime.to_rfc3339(), "2016-11-08T00:00:00+05:00") 59 | /// # } 60 | /// ``` 61 | #[must_use] 62 | pub const fn east_opt(secs: i32) -> Option { 63 | if -86_400 < secs && secs < 86_400 { 64 | Some(FixedOffset { local_minus_utc: secs }) 65 | } else { 66 | None 67 | } 68 | } 69 | 70 | /// Makes a new `FixedOffset` for the Western Hemisphere with given timezone difference. 71 | /// The negative `secs` means the Eastern Hemisphere. 72 | /// 73 | /// Panics on the out-of-bound `secs`. 74 | #[deprecated(since = "0.4.23", note = "use `west_opt()` instead")] 75 | #[must_use] 76 | pub fn west(secs: i32) -> FixedOffset { 77 | FixedOffset::west_opt(secs).expect("FixedOffset::west out of bounds") 78 | } 79 | 80 | /// Makes a new `FixedOffset` for the Western Hemisphere with given timezone difference. 81 | /// The negative `secs` means the Eastern Hemisphere. 82 | /// 83 | /// Returns `None` on the out-of-bound `secs`. 84 | /// 85 | /// # Example 86 | /// 87 | /// ``` 88 | /// # #[cfg(feature = "alloc")] { 89 | /// use chrono::{FixedOffset, TimeZone}; 90 | /// let hour = 3600; 91 | /// let datetime = 92 | /// FixedOffset::west_opt(5 * hour).unwrap().with_ymd_and_hms(2016, 11, 08, 0, 0, 0).unwrap(); 93 | /// assert_eq!(&datetime.to_rfc3339(), "2016-11-08T00:00:00-05:00") 94 | /// # } 95 | /// ``` 96 | #[must_use] 97 | pub const fn west_opt(secs: i32) -> Option { 98 | if -86_400 < secs && secs < 86_400 { 99 | Some(FixedOffset { local_minus_utc: -secs }) 100 | } else { 101 | None 102 | } 103 | } 104 | 105 | /// Returns the number of seconds to add to convert from UTC to the local time. 106 | #[inline] 107 | pub const fn local_minus_utc(&self) -> i32 { 108 | self.local_minus_utc 109 | } 110 | 111 | /// Returns the number of seconds to add to convert from the local time to UTC. 112 | #[inline] 113 | pub const fn utc_minus_local(&self) -> i32 { 114 | -self.local_minus_utc 115 | } 116 | } 117 | 118 | /// Parsing a `str` into a `FixedOffset` uses the format [`%z`](crate::format::strftime). 119 | impl FromStr for FixedOffset { 120 | type Err = ParseError; 121 | fn from_str(s: &str) -> Result { 122 | let (_, offset) = scan::timezone_offset(s, scan::colon_or_space, false, false, true)?; 123 | Self::east_opt(offset).ok_or(OUT_OF_RANGE) 124 | } 125 | } 126 | 127 | impl TimeZone for FixedOffset { 128 | type Offset = FixedOffset; 129 | 130 | fn from_offset(offset: &FixedOffset) -> FixedOffset { 131 | *offset 132 | } 133 | 134 | fn offset_from_local_date(&self, _local: &NaiveDate) -> MappedLocalTime { 135 | MappedLocalTime::Single(*self) 136 | } 137 | fn offset_from_local_datetime(&self, _local: &NaiveDateTime) -> MappedLocalTime { 138 | MappedLocalTime::Single(*self) 139 | } 140 | 141 | fn offset_from_utc_date(&self, _utc: &NaiveDate) -> FixedOffset { 142 | *self 143 | } 144 | fn offset_from_utc_datetime(&self, _utc: &NaiveDateTime) -> FixedOffset { 145 | *self 146 | } 147 | } 148 | 149 | impl Offset for FixedOffset { 150 | fn fix(&self) -> FixedOffset { 151 | *self 152 | } 153 | } 154 | 155 | impl fmt::Debug for FixedOffset { 156 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 157 | let offset = self.local_minus_utc; 158 | let (sign, offset) = if offset < 0 { ('-', -offset) } else { ('+', offset) }; 159 | let sec = offset.rem_euclid(60); 160 | let mins = offset.div_euclid(60); 161 | let min = mins.rem_euclid(60); 162 | let hour = mins.div_euclid(60); 163 | if sec == 0 { 164 | write!(f, "{sign}{hour:02}:{min:02}") 165 | } else { 166 | write!(f, "{sign}{hour:02}:{min:02}:{sec:02}") 167 | } 168 | } 169 | } 170 | 171 | impl fmt::Display for FixedOffset { 172 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 173 | fmt::Debug::fmt(self, f) 174 | } 175 | } 176 | 177 | #[cfg(feature = "defmt")] 178 | impl defmt::Format for FixedOffset { 179 | fn format(&self, f: defmt::Formatter) { 180 | let offset = self.local_minus_utc; 181 | let (sign, offset) = if offset < 0 { ('-', -offset) } else { ('+', offset) }; 182 | let sec = offset.rem_euclid(60); 183 | let mins = offset.div_euclid(60); 184 | let min = mins.rem_euclid(60); 185 | let hour = mins.div_euclid(60); 186 | if sec == 0 { 187 | defmt::write!(f, "{}{:02}:{:02}", sign, hour, min) 188 | } else { 189 | defmt::write!(f, "{}{:02}:{:02}:{:02}", sign, hour, min, sec) 190 | } 191 | } 192 | } 193 | 194 | #[cfg(all(feature = "arbitrary", feature = "std"))] 195 | impl arbitrary::Arbitrary<'_> for FixedOffset { 196 | fn arbitrary(u: &mut arbitrary::Unstructured) -> arbitrary::Result { 197 | let secs = u.int_in_range(-86_399..=86_399)?; 198 | let fixed_offset = FixedOffset::east_opt(secs) 199 | .expect("Could not generate a valid chrono::FixedOffset. It looks like implementation of Arbitrary for FixedOffset is erroneous."); 200 | Ok(fixed_offset) 201 | } 202 | } 203 | 204 | #[cfg(test)] 205 | mod tests { 206 | use super::FixedOffset; 207 | use crate::offset::TimeZone; 208 | use std::str::FromStr; 209 | 210 | #[test] 211 | fn test_date_extreme_offset() { 212 | // starting from 0.3 we don't have an offset exceeding one day. 213 | // this makes everything easier! 214 | let offset = FixedOffset::east_opt(86399).unwrap(); 215 | assert_eq!( 216 | format!("{:?}", offset.with_ymd_and_hms(2012, 2, 29, 5, 6, 7).unwrap()), 217 | "2012-02-29T05:06:07+23:59:59" 218 | ); 219 | let offset = FixedOffset::east_opt(-86399).unwrap(); 220 | assert_eq!( 221 | format!("{:?}", offset.with_ymd_and_hms(2012, 2, 29, 5, 6, 7).unwrap()), 222 | "2012-02-29T05:06:07-23:59:59" 223 | ); 224 | let offset = FixedOffset::west_opt(86399).unwrap(); 225 | assert_eq!( 226 | format!("{:?}", offset.with_ymd_and_hms(2012, 3, 4, 5, 6, 7).unwrap()), 227 | "2012-03-04T05:06:07-23:59:59" 228 | ); 229 | let offset = FixedOffset::west_opt(-86399).unwrap(); 230 | assert_eq!( 231 | format!("{:?}", offset.with_ymd_and_hms(2012, 3, 4, 5, 6, 7).unwrap()), 232 | "2012-03-04T05:06:07+23:59:59" 233 | ); 234 | } 235 | 236 | #[test] 237 | fn test_parse_offset() { 238 | let offset = FixedOffset::from_str("-0500").unwrap(); 239 | assert_eq!(offset.local_minus_utc, -5 * 3600); 240 | let offset = FixedOffset::from_str("-08:00").unwrap(); 241 | assert_eq!(offset.local_minus_utc, -8 * 3600); 242 | let offset = FixedOffset::from_str("+06:30").unwrap(); 243 | assert_eq!(offset.local_minus_utc, (6 * 3600) + 1800); 244 | } 245 | 246 | #[test] 247 | #[cfg(feature = "rkyv-validation")] 248 | fn test_rkyv_validation() { 249 | let offset = FixedOffset::from_str("-0500").unwrap(); 250 | let bytes = rkyv::to_bytes::<_, 4>(&offset).unwrap(); 251 | assert_eq!(rkyv::from_bytes::(&bytes).unwrap(), offset); 252 | } 253 | } 254 | -------------------------------------------------------------------------------- /bench/benches/chrono.rs: -------------------------------------------------------------------------------- 1 | //! Benchmarks for chrono that just depend on std 2 | 3 | use criterion::{BenchmarkId, Criterion, black_box, criterion_group, criterion_main}; 4 | 5 | #[cfg(feature = "unstable-locales")] 6 | use chrono::Locale; 7 | use chrono::format::StrftimeItems; 8 | use chrono::prelude::*; 9 | use chrono::{__BenchYearFlags, DateTime, FixedOffset, Local, TimeDelta, Utc}; 10 | 11 | fn bench_date_from_ymd(c: &mut Criterion) { 12 | c.bench_function("bench_date_from_ymd", |b| { 13 | let expected = NaiveDate::from_ymd_opt(2024, 2, 12); 14 | b.iter(|| { 15 | let (y, m, d) = black_box((2024, 2, 12)); 16 | assert_eq!(NaiveDate::from_ymd_opt(y, m, d), expected) 17 | }) 18 | }); 19 | } 20 | 21 | fn bench_datetime_parse_from_rfc2822(c: &mut Criterion) { 22 | c.bench_function("bench_datetime_parse_from_rfc2822", |b| { 23 | b.iter(|| { 24 | let str = black_box("Wed, 18 Feb 2015 23:16:09 +0000"); 25 | DateTime::parse_from_rfc2822(str).unwrap() 26 | }) 27 | }); 28 | } 29 | 30 | fn bench_datetime_parse_from_rfc3339(c: &mut Criterion) { 31 | c.bench_function("bench_datetime_parse_from_rfc3339", |b| { 32 | b.iter(|| { 33 | let str = black_box("2015-02-18T23:59:60.234567+05:00"); 34 | DateTime::parse_from_rfc3339(str).unwrap() 35 | }) 36 | }); 37 | } 38 | 39 | fn bench_datetime_from_str(c: &mut Criterion) { 40 | c.bench_function("bench_datetime_from_str", |b| { 41 | b.iter(|| { 42 | use std::str::FromStr; 43 | let str = black_box("2019-03-30T18:46:57.193Z"); 44 | DateTime::::from_str(str).unwrap() 45 | }) 46 | }); 47 | } 48 | 49 | fn bench_datetime_to_rfc2822(c: &mut Criterion) { 50 | let pst = FixedOffset::east_opt(8 * 60 * 60).unwrap(); 51 | let dt = pst 52 | .from_local_datetime( 53 | &NaiveDate::from_ymd_opt(2018, 1, 11) 54 | .unwrap() 55 | .and_hms_nano_opt(10, 5, 13, 84_660_000) 56 | .unwrap(), 57 | ) 58 | .unwrap(); 59 | c.bench_function("bench_datetime_to_rfc2822", |b| b.iter(|| black_box(dt).to_rfc2822())); 60 | } 61 | 62 | fn bench_datetime_to_rfc3339(c: &mut Criterion) { 63 | let pst = FixedOffset::east_opt(8 * 60 * 60).unwrap(); 64 | let dt = pst 65 | .from_local_datetime( 66 | &NaiveDate::from_ymd_opt(2018, 1, 11) 67 | .unwrap() 68 | .and_hms_nano_opt(10, 5, 13, 84_660_000) 69 | .unwrap(), 70 | ) 71 | .unwrap(); 72 | c.bench_function("bench_datetime_to_rfc3339", |b| b.iter(|| black_box(dt).to_rfc3339())); 73 | } 74 | 75 | fn bench_datetime_to_rfc3339_opts(c: &mut Criterion) { 76 | let pst = FixedOffset::east_opt(8 * 60 * 60).unwrap(); 77 | let dt = pst 78 | .from_local_datetime( 79 | &NaiveDate::from_ymd_opt(2018, 1, 11) 80 | .unwrap() 81 | .and_hms_nano_opt(10, 5, 13, 84_660_000) 82 | .unwrap(), 83 | ) 84 | .unwrap(); 85 | c.bench_function("bench_datetime_to_rfc3339_opts", |b| { 86 | b.iter(|| black_box(dt).to_rfc3339_opts(SecondsFormat::Nanos, true)) 87 | }); 88 | } 89 | 90 | fn bench_year_flags_from_year(c: &mut Criterion) { 91 | c.bench_function("bench_year_flags_from_year", |b| { 92 | b.iter(|| { 93 | for year in -999i32..1000 { 94 | let _ = __BenchYearFlags::from_year(black_box(year)); 95 | } 96 | }) 97 | }); 98 | } 99 | 100 | fn bench_get_local_time(c: &mut Criterion) { 101 | c.bench_function("bench_get_local_time", |b| { 102 | b.iter(|| { 103 | let _ = Local::now(); 104 | }) 105 | }); 106 | } 107 | 108 | /// Returns the number of multiples of `div` in the range `start..end`. 109 | /// 110 | /// If the range `start..end` is back-to-front, i.e. `start` is greater than `end`, the 111 | /// behaviour is defined by the following equation: 112 | /// `in_between(start, end, div) == - in_between(end, start, div)`. 113 | /// 114 | /// When `div` is 1, this is equivalent to `end - start`, i.e. the length of `start..end`. 115 | /// 116 | /// # Panics 117 | /// 118 | /// Panics if `div` is not positive. 119 | fn in_between(start: i32, end: i32, div: i32) -> i32 { 120 | assert!(div > 0, "in_between: nonpositive div = {div}"); 121 | let start = (start.div_euclid(div), start.rem_euclid(div)); 122 | let end = (end.div_euclid(div), end.rem_euclid(div)); 123 | // The lowest multiple of `div` greater than or equal to `start`, divided. 124 | let start = start.0 + (start.1 != 0) as i32; 125 | // The lowest multiple of `div` greater than or equal to `end`, divided. 126 | let end = end.0 + (end.1 != 0) as i32; 127 | end - start 128 | } 129 | 130 | /// Alternative implementation to `Datelike::num_days_from_ce` 131 | fn num_days_from_ce_alt(date: &Date) -> i32 { 132 | let year = date.year(); 133 | let diff = move |div| in_between(1, year, div); 134 | // 365 days a year, one more in leap years. In the gregorian calendar, leap years are all 135 | // the multiples of 4 except multiples of 100 but including multiples of 400. 136 | date.ordinal() as i32 + 365 * diff(1) + diff(4) - diff(100) + diff(400) 137 | } 138 | 139 | fn bench_num_days_from_ce(c: &mut Criterion) { 140 | let mut group = c.benchmark_group("num_days_from_ce"); 141 | for year in &[1, 500, 2000, 2019] { 142 | let d = NaiveDate::from_ymd_opt(*year, 1, 1).unwrap(); 143 | group.bench_with_input(BenchmarkId::new("new", year), &d, |b, y| { 144 | b.iter(|| num_days_from_ce_alt(y)) 145 | }); 146 | group.bench_with_input(BenchmarkId::new("classic", year), &d, |b, y| { 147 | b.iter(|| y.num_days_from_ce()) 148 | }); 149 | } 150 | } 151 | 152 | fn bench_parse_strftime(c: &mut Criterion) { 153 | c.bench_function("bench_parse_strftime", |b| { 154 | b.iter(|| { 155 | let str = black_box("%a, %d %b %Y %H:%M:%S GMT"); 156 | let items = StrftimeItems::new(str); 157 | black_box(items.collect::>()); 158 | }) 159 | }); 160 | } 161 | 162 | #[cfg(feature = "unstable-locales")] 163 | fn bench_parse_strftime_localized(c: &mut Criterion) { 164 | c.bench_function("bench_parse_strftime_localized", |b| { 165 | b.iter(|| { 166 | let str = black_box("%a, %d %b %Y %H:%M:%S GMT"); 167 | let items = StrftimeItems::new_with_locale(str, Locale::nl_NL); 168 | black_box(items.collect::>()); 169 | }) 170 | }); 171 | } 172 | 173 | fn bench_format(c: &mut Criterion) { 174 | let dt = Local::now(); 175 | c.bench_function("bench_format", |b| { 176 | b.iter(|| format!("{}", black_box(dt).format("%Y-%m-%dT%H:%M:%S%.f%:z"))) 177 | }); 178 | } 179 | 180 | fn bench_format_with_items(c: &mut Criterion) { 181 | let dt = Local::now(); 182 | let items: Vec<_> = StrftimeItems::new("%Y-%m-%dT%H:%M:%S%.f%:z").collect(); 183 | c.bench_function("bench_format_with_items", |b| { 184 | b.iter(|| format!("{}", black_box(dt).format_with_items(items.iter()))) 185 | }); 186 | } 187 | 188 | fn benches_delayed_format(c: &mut Criterion) { 189 | let mut group = c.benchmark_group("delayed_format"); 190 | let dt = Local::now(); 191 | group.bench_function(BenchmarkId::new("with_display", dt), |b| { 192 | b.iter_batched( 193 | || dt.format("%Y-%m-%dT%H:%M:%S%.f%:z"), 194 | |df| black_box(df).to_string(), 195 | criterion::BatchSize::SmallInput, 196 | ) 197 | }); 198 | group.bench_function(BenchmarkId::new("with_string_buffer", dt), |b| { 199 | b.iter_batched( 200 | || (dt.format("%Y-%m-%dT%H:%M:%S%.f%:z"), String::with_capacity(256)), 201 | |(df, string)| black_box(df).write_to(&mut black_box(string)), 202 | criterion::BatchSize::SmallInput, 203 | ) 204 | }); 205 | } 206 | 207 | fn bench_format_manual(c: &mut Criterion) { 208 | let dt = Local::now(); 209 | c.bench_function("bench_format_manual", |b| { 210 | b.iter(|| { 211 | black_box(dt); 212 | format!( 213 | "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:09}{:+02}:{:02}", 214 | dt.year(), 215 | dt.month(), 216 | dt.day(), 217 | dt.hour(), 218 | dt.minute(), 219 | dt.second(), 220 | dt.nanosecond(), 221 | dt.offset().fix().local_minus_utc() / 3600, 222 | dt.offset().fix().local_minus_utc() / 60, 223 | ) 224 | }) 225 | }); 226 | } 227 | 228 | fn bench_naivedate_add_signed(c: &mut Criterion) { 229 | let date = NaiveDate::from_ymd_opt(2023, 7, 29).unwrap(); 230 | let extra = TimeDelta::try_days(25).unwrap(); 231 | c.bench_function("bench_naivedate_add_signed", |b| { 232 | b.iter(|| black_box(date).checked_add_signed(extra).unwrap()) 233 | }); 234 | } 235 | 236 | fn bench_datetime_with(c: &mut Criterion) { 237 | let dt = FixedOffset::east_opt(3600).unwrap().with_ymd_and_hms(2023, 9, 23, 7, 36, 0).unwrap(); 238 | c.bench_function("bench_datetime_with", |b| { 239 | b.iter(|| black_box(black_box(dt).with_hour(12)).unwrap()) 240 | }); 241 | } 242 | 243 | criterion_group!( 244 | benches, 245 | bench_date_from_ymd, 246 | bench_datetime_parse_from_rfc2822, 247 | bench_datetime_parse_from_rfc3339, 248 | bench_datetime_from_str, 249 | bench_datetime_to_rfc2822, 250 | bench_datetime_to_rfc3339, 251 | bench_datetime_to_rfc3339_opts, 252 | bench_year_flags_from_year, 253 | bench_num_days_from_ce, 254 | bench_get_local_time, 255 | bench_parse_strftime, 256 | bench_format, 257 | bench_format_with_items, 258 | bench_format_manual, 259 | benches_delayed_format, 260 | bench_naivedate_add_signed, 261 | bench_datetime_with, 262 | ); 263 | 264 | #[cfg(feature = "unstable-locales")] 265 | criterion_group!(unstable_locales, bench_parse_strftime_localized,); 266 | 267 | #[cfg(not(feature = "unstable-locales"))] 268 | criterion_main!(benches); 269 | #[cfg(feature = "unstable-locales")] 270 | criterion_main!(benches, unstable_locales); 271 | -------------------------------------------------------------------------------- /src/offset/local/tz_data.rs: -------------------------------------------------------------------------------- 1 | //! Rust parser of ZoneInfoDb(`tzdata`) on Android and OpenHarmony 2 | //! 3 | //! Ported from: https://android.googlesource.com/platform/prebuilts/fullsdk/sources/+/refs/heads/androidx-appcompat-release/android-34/com/android/i18n/timezone/ZoneInfoDb.java 4 | use std::{ 5 | ffi::CStr, 6 | fmt::Debug, 7 | fs::File, 8 | io::{Error, ErrorKind, Read, Result, Seek, SeekFrom}, 9 | }; 10 | 11 | /// Get timezone data from the `tzdata` file of HarmonyOS NEXT. 12 | #[cfg(target_env = "ohos")] 13 | pub(crate) fn for_zone(tz_string: &str) -> Result>> { 14 | let mut file = File::open("/system/etc/zoneinfo/tzdata")?; 15 | find_tz_data::(&mut file, tz_string.as_bytes()) 16 | } 17 | 18 | /// Get timezone data from the `tzdata` file of Android. 19 | #[cfg(target_os = "android")] 20 | pub(crate) fn for_zone(tz_string: &str) -> Result>> { 21 | let mut file = open_android_tz_data_file()?; 22 | find_tz_data::(&mut file, tz_string.as_bytes()) 23 | } 24 | 25 | /// Open the `tzdata` file of Android from the environment variables. 26 | #[cfg(target_os = "android")] 27 | fn open_android_tz_data_file() -> Result { 28 | for (env_var, path) in 29 | [("ANDROID_DATA", "/misc/zoneinfo"), ("ANDROID_ROOT", "/usr/share/zoneinfo")] 30 | { 31 | if let Ok(env_value) = std::env::var(env_var) { 32 | if let Ok(file) = File::open(format!("{}{}/tzdata", env_value, path)) { 33 | return Ok(file); 34 | } 35 | } 36 | } 37 | Err(Error::from(ErrorKind::NotFound)) 38 | } 39 | 40 | /// Get timezone data from the `tzdata` file reader 41 | #[cfg(any(test, target_env = "ohos", target_os = "android"))] 42 | fn find_tz_data( 43 | mut reader: impl Read + Seek, 44 | tz_name: &[u8], 45 | ) -> Result>> { 46 | let header = TzDataHeader::new(&mut reader)?; 47 | let index = TzDataIndexes::new::(&mut reader, &header)?; 48 | Ok(if let Some(entry) = index.find_timezone(tz_name) { 49 | Some(index.find_tzdata(reader, &header, entry)?) 50 | } else { 51 | None 52 | }) 53 | } 54 | 55 | /// Header of the `tzdata` file. 56 | #[derive(Debug, Clone, Copy, PartialEq, Eq)] 57 | struct TzDataHeader { 58 | version: [u8; 5], 59 | index_offset: u32, 60 | data_offset: u32, 61 | zonetab_offset: u32, 62 | } 63 | 64 | impl TzDataHeader { 65 | /// Parse the header of the `tzdata` file. 66 | fn new(mut data: impl Read) -> Result { 67 | let version = { 68 | let mut magic = [0; TZDATA_VERSION_LEN]; 69 | data.read_exact(&mut magic)?; 70 | if !magic.starts_with(b"tzdata") || magic[TZDATA_VERSION_LEN - 1] != 0 { 71 | return Err(Error::new(ErrorKind::Other, "invalid tzdata header magic")); 72 | } 73 | let mut version = [0; 5]; 74 | version.copy_from_slice(&magic[6..11]); 75 | version 76 | }; 77 | 78 | let mut offset = [0; 4]; 79 | data.read_exact(&mut offset)?; 80 | let index_offset = u32::from_be_bytes(offset); 81 | data.read_exact(&mut offset)?; 82 | let data_offset = u32::from_be_bytes(offset); 83 | data.read_exact(&mut offset)?; 84 | let zonetab_offset = u32::from_be_bytes(offset); 85 | 86 | Ok(Self { version, index_offset, data_offset, zonetab_offset }) 87 | } 88 | } 89 | 90 | /// Indexes of the `tzdata` file. 91 | struct TzDataIndexes { 92 | indexes: Vec, 93 | } 94 | 95 | impl TzDataIndexes { 96 | /// Create a new `TzDataIndexes` from the `tzdata` file reader. 97 | fn new(mut reader: impl Read, header: &TzDataHeader) -> Result { 98 | let mut buf = vec![0; header.data_offset.saturating_sub(header.index_offset) as usize]; 99 | reader.read_exact(&mut buf)?; 100 | // replace chunks with array_chunks when it's stable 101 | Ok(TzDataIndexes { 102 | indexes: buf 103 | .chunks(ENTRY_LEN) 104 | .filter_map(|chunk| { 105 | from_bytes_until_nul(&chunk[..TZ_NAME_LEN]).map(|name| { 106 | let name = name.to_bytes().to_vec().into_boxed_slice(); 107 | let offset = u32::from_be_bytes( 108 | chunk[TZ_NAME_LEN..TZ_NAME_LEN + 4].try_into().unwrap(), 109 | ); 110 | let length = u32::from_be_bytes( 111 | chunk[TZ_NAME_LEN + 4..TZ_NAME_LEN + 8].try_into().unwrap(), 112 | ); 113 | TzDataIndex { name, offset, length } 114 | }) 115 | }) 116 | .collect(), 117 | }) 118 | } 119 | 120 | /// Find a timezone by name. 121 | fn find_timezone(&self, timezone: &[u8]) -> Option<&TzDataIndex> { 122 | // timezones in tzdata are sorted by name. 123 | self.indexes.binary_search_by_key(&timezone, |x| &x.name).map(|x| &self.indexes[x]).ok() 124 | } 125 | 126 | /// Retrieve a chunk of timezone data by the index. 127 | fn find_tzdata( 128 | &self, 129 | mut reader: impl Read + Seek, 130 | header: &TzDataHeader, 131 | index: &TzDataIndex, 132 | ) -> Result> { 133 | reader.seek(SeekFrom::Start(index.offset as u64 + header.data_offset as u64))?; 134 | let mut buffer = vec![0; index.length as usize]; 135 | reader.read_exact(&mut buffer)?; 136 | Ok(buffer) 137 | } 138 | } 139 | 140 | /// Index entry of the `tzdata` file. 141 | struct TzDataIndex { 142 | name: Box<[u8]>, 143 | offset: u32, 144 | length: u32, 145 | } 146 | 147 | /// TODO: Change this `CStr::from_bytes_until_nul` once MSRV was bumped above 1.72.0 148 | fn from_bytes_until_nul(bytes: &[u8]) -> Option<&CStr> { 149 | let nul_pos = bytes.iter().position(|&b| b == 0)?; 150 | // SAFETY: 151 | // 1. nul_pos + 1 <= bytes.len() 152 | // 2. We know there is a nul byte at nul_pos, so this slice (ending at the nul byte) is a well-formed C string. 153 | Some(unsafe { CStr::from_bytes_with_nul_unchecked(&bytes[..=nul_pos]) }) 154 | } 155 | 156 | /// Ohos tzdata index entry size: `name + offset + length` 157 | #[cfg(any(test, target_env = "ohos"))] 158 | const OHOS_ENTRY_LEN: usize = TZ_NAME_LEN + 2 * size_of::(); 159 | /// Android tzdata index entry size: `name + offset + length + raw_utc_offset(legacy)`: 160 | /// [reference](https://android.googlesource.com/platform/prebuilts/fullsdk/sources/+/refs/heads/androidx-appcompat-release/android-34/com/android/i18n/timezone/ZoneInfoDb.java#271) 161 | #[cfg(any(test, target_os = "android"))] 162 | const ANDROID_ENTRY_LEN: usize = TZ_NAME_LEN + 3 * size_of::(); 163 | /// The database reserves 40 bytes for each id. 164 | const TZ_NAME_LEN: usize = 40; 165 | /// Size of the version string in the header of `tzdata` file. 166 | /// e.g. `tzdata2024b\0` 167 | const TZDATA_VERSION_LEN: usize = 12; 168 | 169 | #[cfg(test)] 170 | mod tests { 171 | use super::*; 172 | 173 | #[test] 174 | fn test_ohos_tzdata_header_and_index() { 175 | let file = File::open("./tests/ohos/tzdata").unwrap(); 176 | let header = TzDataHeader::new(&file).unwrap(); 177 | assert_eq!(header.version, *b"2024a"); 178 | assert_eq!(header.index_offset, 24); 179 | assert_eq!(header.data_offset, 21240); 180 | assert_eq!(header.zonetab_offset, 272428); 181 | 182 | let iter = TzDataIndexes::new::(&file, &header).unwrap(); 183 | assert_eq!(iter.indexes.len(), 442); 184 | assert!(iter.find_timezone(b"Asia/Shanghai").is_some()); 185 | assert!(iter.find_timezone(b"Pacific/Noumea").is_some()); 186 | } 187 | 188 | #[test] 189 | fn test_ohos_tzdata_loading() { 190 | let file = File::open("./tests/ohos/tzdata").unwrap(); 191 | let header = TzDataHeader::new(&file).unwrap(); 192 | let iter = TzDataIndexes::new::(&file, &header).unwrap(); 193 | let timezone = iter.find_timezone(b"Asia/Shanghai").unwrap(); 194 | let tzdata = iter.find_tzdata(&file, &header, timezone).unwrap(); 195 | assert_eq!(tzdata.len(), 393); 196 | } 197 | 198 | #[test] 199 | fn test_invalid_tzdata_header() { 200 | TzDataHeader::new(&b"tzdaaa2024aaaaaaaaaaaaaaa\0"[..]).unwrap_err(); 201 | } 202 | 203 | #[test] 204 | fn test_android_tzdata_header_and_index() { 205 | let file = File::open("./tests/android/tzdata").unwrap(); 206 | let header = TzDataHeader::new(&file).unwrap(); 207 | assert_eq!(header.version, *b"2021a"); 208 | assert_eq!(header.index_offset, 24); 209 | assert_eq!(header.data_offset, 30860); 210 | assert_eq!(header.zonetab_offset, 491837); 211 | 212 | let iter = TzDataIndexes::new::(&file, &header).unwrap(); 213 | assert_eq!(iter.indexes.len(), 593); 214 | assert!(iter.find_timezone(b"Asia/Shanghai").is_some()); 215 | assert!(iter.find_timezone(b"Pacific/Noumea").is_some()); 216 | } 217 | 218 | #[test] 219 | fn test_android_tzdata_loading() { 220 | let file = File::open("./tests/android/tzdata").unwrap(); 221 | let header = TzDataHeader::new(&file).unwrap(); 222 | let iter = TzDataIndexes::new::(&file, &header).unwrap(); 223 | let timezone = iter.find_timezone(b"Asia/Shanghai").unwrap(); 224 | let tzdata = iter.find_tzdata(&file, &header, timezone).unwrap(); 225 | assert_eq!(tzdata.len(), 573); 226 | } 227 | 228 | #[test] 229 | fn test_ohos_tzdata_find() { 230 | let file = File::open("./tests/ohos/tzdata").unwrap(); 231 | let tzdata = find_tz_data::(file, b"Asia/Shanghai").unwrap().unwrap(); 232 | assert_eq!(tzdata.len(), 393); 233 | } 234 | 235 | #[test] 236 | fn test_ohos_tzdata_find_missing() { 237 | let file = File::open("./tests/ohos/tzdata").unwrap(); 238 | assert!(find_tz_data::(file, b"Asia/Sjasdfai").unwrap().is_none()); 239 | } 240 | 241 | #[test] 242 | fn test_android_tzdata_find() { 243 | let file = File::open("./tests/android/tzdata").unwrap(); 244 | let tzdata = find_tz_data::(file, b"Asia/Shanghai").unwrap().unwrap(); 245 | assert_eq!(tzdata.len(), 573); 246 | } 247 | 248 | #[test] 249 | fn test_android_tzdata_find_missing() { 250 | let file = File::open("./tests/android/tzdata").unwrap(); 251 | assert!(find_tz_data::(file, b"Asia/S000000i").unwrap().is_none()); 252 | } 253 | 254 | #[cfg(target_env = "ohos")] 255 | #[test] 256 | fn test_ohos_machine_tz_data_loading() { 257 | let tzdata = for_zone(b"Asia/Shanghai").unwrap().unwrap(); 258 | assert!(!tzdata.is_empty()); 259 | } 260 | 261 | #[cfg(target_os = "android")] 262 | #[test] 263 | fn test_android_machine_tz_data_loading() { 264 | let tzdata = for_zone(b"Asia/Shanghai").unwrap().unwrap(); 265 | assert!(!tzdata.is_empty()); 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /src/naive/mod.rs: -------------------------------------------------------------------------------- 1 | //! Date and time types unconcerned with timezones. 2 | //! 3 | //! They are primarily building blocks for other types 4 | //! (e.g. [`TimeZone`](../offset/trait.TimeZone.html)), 5 | //! but can be also used for the simpler date and time handling. 6 | 7 | use core::hash::{Hash, Hasher}; 8 | use core::ops::RangeInclusive; 9 | 10 | use crate::Weekday; 11 | use crate::expect; 12 | 13 | pub(crate) mod date; 14 | pub(crate) mod datetime; 15 | mod internals; 16 | pub(crate) mod isoweek; 17 | pub(crate) mod time; 18 | 19 | #[allow(deprecated)] 20 | pub use self::date::{MAX_DATE, MIN_DATE}; 21 | pub use self::date::{NaiveDate, NaiveDateDaysIterator, NaiveDateWeeksIterator}; 22 | #[allow(deprecated)] 23 | pub use self::datetime::{MAX_DATETIME, MIN_DATETIME, NaiveDateTime}; 24 | pub use self::isoweek::IsoWeek; 25 | pub use self::time::NaiveTime; 26 | 27 | #[cfg(feature = "__internal_bench")] 28 | #[doc(hidden)] 29 | pub use self::internals::YearFlags as __BenchYearFlags; 30 | 31 | /// A week represented by a [`NaiveDate`] and a [`Weekday`] which is the first 32 | /// day of the week. 33 | #[derive(Clone, Copy, Debug, Eq)] 34 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 35 | pub struct NaiveWeek { 36 | date: NaiveDate, 37 | start: Weekday, 38 | } 39 | 40 | impl NaiveWeek { 41 | /// Create a new `NaiveWeek` 42 | pub(crate) const fn new(date: NaiveDate, start: Weekday) -> Self { 43 | Self { date, start } 44 | } 45 | 46 | /// Returns a date representing the first day of the week. 47 | /// 48 | /// # Panics 49 | /// 50 | /// Panics if the first day of the week happens to fall just out of range of `NaiveDate` 51 | /// (more than ca. 262,000 years away from common era). 52 | /// 53 | /// # Examples 54 | /// 55 | /// ``` 56 | /// use chrono::{NaiveDate, Weekday}; 57 | /// 58 | /// let date = NaiveDate::from_ymd_opt(2022, 4, 18).unwrap(); 59 | /// let week = date.week(Weekday::Mon); 60 | /// assert!(week.first_day() <= date); 61 | /// ``` 62 | #[inline] 63 | #[must_use] 64 | pub const fn first_day(&self) -> NaiveDate { 65 | expect(self.checked_first_day(), "first weekday out of range for `NaiveDate`") 66 | } 67 | 68 | /// Returns a date representing the first day of the week or 69 | /// `None` if the date is out of `NaiveDate`'s range 70 | /// (more than ca. 262,000 years away from common era). 71 | /// 72 | /// # Examples 73 | /// 74 | /// ``` 75 | /// use chrono::{NaiveDate, Weekday}; 76 | /// 77 | /// let date = NaiveDate::MIN; 78 | /// let week = date.week(Weekday::Mon); 79 | /// if let Some(first_day) = week.checked_first_day() { 80 | /// assert!(first_day == date); 81 | /// } else { 82 | /// // error handling code 83 | /// return; 84 | /// }; 85 | /// ``` 86 | #[inline] 87 | #[must_use] 88 | pub const fn checked_first_day(&self) -> Option { 89 | let start = self.start.num_days_from_monday() as i32; 90 | let ref_day = self.date.weekday().num_days_from_monday() as i32; 91 | // Calculate the number of days to subtract from `self.date`. 92 | // Do not construct an intermediate date beyond `self.date`, because that may be out of 93 | // range if `date` is close to `NaiveDate::MAX`. 94 | let days = start - ref_day - if start > ref_day { 7 } else { 0 }; 95 | self.date.add_days(days) 96 | } 97 | 98 | /// Returns a date representing the last day of the week. 99 | /// 100 | /// # Panics 101 | /// 102 | /// Panics if the last day of the week happens to fall just out of range of `NaiveDate` 103 | /// (more than ca. 262,000 years away from common era). 104 | /// 105 | /// # Examples 106 | /// 107 | /// ``` 108 | /// use chrono::{NaiveDate, Weekday}; 109 | /// 110 | /// let date = NaiveDate::from_ymd_opt(2022, 4, 18).unwrap(); 111 | /// let week = date.week(Weekday::Mon); 112 | /// assert!(week.last_day() >= date); 113 | /// ``` 114 | #[inline] 115 | #[must_use] 116 | pub const fn last_day(&self) -> NaiveDate { 117 | expect(self.checked_last_day(), "last weekday out of range for `NaiveDate`") 118 | } 119 | 120 | /// Returns a date representing the last day of the week or 121 | /// `None` if the date is out of `NaiveDate`'s range 122 | /// (more than ca. 262,000 years away from common era). 123 | /// 124 | /// # Examples 125 | /// 126 | /// ``` 127 | /// use chrono::{NaiveDate, Weekday}; 128 | /// 129 | /// let date = NaiveDate::MAX; 130 | /// let week = date.week(Weekday::Mon); 131 | /// if let Some(last_day) = week.checked_last_day() { 132 | /// assert!(last_day == date); 133 | /// } else { 134 | /// // error handling code 135 | /// return; 136 | /// }; 137 | /// ``` 138 | #[inline] 139 | #[must_use] 140 | pub const fn checked_last_day(&self) -> Option { 141 | let end = self.start.pred().num_days_from_monday() as i32; 142 | let ref_day = self.date.weekday().num_days_from_monday() as i32; 143 | // Calculate the number of days to add to `self.date`. 144 | // Do not construct an intermediate date before `self.date` (like with `first_day()`), 145 | // because that may be out of range if `date` is close to `NaiveDate::MIN`. 146 | let days = end - ref_day + if end < ref_day { 7 } else { 0 }; 147 | self.date.add_days(days) 148 | } 149 | 150 | /// Returns a [`RangeInclusive`] representing the whole week bounded by 151 | /// [first_day](NaiveWeek::first_day) and [last_day](NaiveWeek::last_day) functions. 152 | /// 153 | /// # Panics 154 | /// 155 | /// Panics if the either the first or last day of the week happens to fall just out of range of 156 | /// `NaiveDate` (more than ca. 262,000 years away from common era). 157 | /// 158 | /// # Examples 159 | /// 160 | /// ``` 161 | /// use chrono::{NaiveDate, Weekday}; 162 | /// 163 | /// let date = NaiveDate::from_ymd_opt(2022, 4, 18).unwrap(); 164 | /// let week = date.week(Weekday::Mon); 165 | /// let days = week.days(); 166 | /// assert!(days.contains(&date)); 167 | /// ``` 168 | #[inline] 169 | #[must_use] 170 | pub const fn days(&self) -> RangeInclusive { 171 | // `expect` doesn't work because `RangeInclusive` is not `Copy` 172 | match self.checked_days() { 173 | Some(val) => val, 174 | None => panic!("{}", "first or last weekday is out of range for `NaiveDate`"), 175 | } 176 | } 177 | 178 | /// Returns an [`Option>`] representing the whole week bounded by 179 | /// [checked_first_day](NaiveWeek::checked_first_day) and 180 | /// [checked_last_day](NaiveWeek::checked_last_day) functions. 181 | /// 182 | /// Returns `None` if either of the boundaries are out of `NaiveDate`'s range 183 | /// (more than ca. 262,000 years away from common era). 184 | /// 185 | /// 186 | /// # Examples 187 | /// 188 | /// ``` 189 | /// use chrono::{NaiveDate, Weekday}; 190 | /// 191 | /// let date = NaiveDate::MAX; 192 | /// let week = date.week(Weekday::Mon); 193 | /// let _days = match week.checked_days() { 194 | /// Some(d) => d, 195 | /// None => { 196 | /// // error handling code 197 | /// return; 198 | /// } 199 | /// }; 200 | /// ``` 201 | #[inline] 202 | #[must_use] 203 | pub const fn checked_days(&self) -> Option> { 204 | match (self.checked_first_day(), self.checked_last_day()) { 205 | (Some(first), Some(last)) => Some(first..=last), 206 | (_, _) => None, 207 | } 208 | } 209 | } 210 | 211 | impl PartialEq for NaiveWeek { 212 | fn eq(&self, other: &Self) -> bool { 213 | self.first_day() == other.first_day() 214 | } 215 | } 216 | 217 | impl Hash for NaiveWeek { 218 | fn hash(&self, state: &mut H) { 219 | self.first_day().hash(state); 220 | } 221 | } 222 | 223 | /// A duration in calendar days. 224 | /// 225 | /// This is useful because when using `TimeDelta` it is possible that adding `TimeDelta::days(1)` 226 | /// doesn't increment the day value as expected due to it being a fixed number of seconds. This 227 | /// difference applies only when dealing with `DateTime` data types and in other cases 228 | /// `TimeDelta::days(n)` and `Days::new(n)` are equivalent. 229 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] 230 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 231 | pub struct Days(pub(crate) u64); 232 | 233 | impl Days { 234 | /// Construct a new `Days` from a number of days 235 | pub const fn new(num: u64) -> Self { 236 | Self(num) 237 | } 238 | } 239 | 240 | /// Serialization/Deserialization of `NaiveDateTime` in alternate formats 241 | /// 242 | /// The various modules in here are intended to be used with serde's [`with` annotation] to 243 | /// serialize as something other than the default ISO 8601 format. 244 | /// 245 | /// [`with` annotation]: https://serde.rs/field-attrs.html#with 246 | #[cfg(feature = "serde")] 247 | pub mod serde { 248 | pub use super::datetime::serde::*; 249 | } 250 | 251 | #[cfg(test)] 252 | mod test { 253 | use crate::{NaiveDate, NaiveWeek, Weekday}; 254 | use std::hash::{DefaultHasher, Hash, Hasher}; 255 | #[test] 256 | fn test_naiveweek() { 257 | let date = NaiveDate::from_ymd_opt(2022, 5, 18).unwrap(); 258 | let asserts = [ 259 | (Weekday::Mon, "Mon 2022-05-16", "Sun 2022-05-22"), 260 | (Weekday::Tue, "Tue 2022-05-17", "Mon 2022-05-23"), 261 | (Weekday::Wed, "Wed 2022-05-18", "Tue 2022-05-24"), 262 | (Weekday::Thu, "Thu 2022-05-12", "Wed 2022-05-18"), 263 | (Weekday::Fri, "Fri 2022-05-13", "Thu 2022-05-19"), 264 | (Weekday::Sat, "Sat 2022-05-14", "Fri 2022-05-20"), 265 | (Weekday::Sun, "Sun 2022-05-15", "Sat 2022-05-21"), 266 | ]; 267 | for (start, first_day, last_day) in asserts { 268 | let week = date.week(start); 269 | let days = week.days(); 270 | assert_eq!(Ok(week.first_day()), NaiveDate::parse_from_str(first_day, "%a %Y-%m-%d")); 271 | assert_eq!(Ok(week.last_day()), NaiveDate::parse_from_str(last_day, "%a %Y-%m-%d")); 272 | assert!(days.contains(&date)); 273 | } 274 | } 275 | 276 | #[test] 277 | fn test_naiveweek_min_max() { 278 | let date_max = NaiveDate::MAX; 279 | assert!(date_max.week(Weekday::Mon).first_day() <= date_max); 280 | let date_min = NaiveDate::MIN; 281 | assert!(date_min.week(Weekday::Mon).last_day() >= date_min); 282 | } 283 | 284 | #[test] 285 | fn test_naiveweek_checked_no_panic() { 286 | let date_max = NaiveDate::MAX; 287 | if let Some(last) = date_max.week(Weekday::Mon).checked_last_day() { 288 | assert!(last == date_max); 289 | } 290 | let date_min = NaiveDate::MIN; 291 | if let Some(first) = date_min.week(Weekday::Mon).checked_first_day() { 292 | assert!(first == date_min); 293 | } 294 | let _ = date_min.week(Weekday::Mon).checked_days(); 295 | let _ = date_max.week(Weekday::Mon).checked_days(); 296 | } 297 | 298 | #[test] 299 | fn test_naiveweek_eq() { 300 | let a = 301 | NaiveWeek { date: NaiveDate::from_ymd_opt(2025, 4, 3).unwrap(), start: Weekday::Mon }; 302 | let b = 303 | NaiveWeek { date: NaiveDate::from_ymd_opt(2025, 4, 4).unwrap(), start: Weekday::Mon }; 304 | assert_eq!(a, b); 305 | 306 | let c = 307 | NaiveWeek { date: NaiveDate::from_ymd_opt(2025, 4, 3).unwrap(), start: Weekday::Sun }; 308 | assert_ne!(a, c); 309 | assert_ne!(b, c); 310 | } 311 | 312 | #[test] 313 | fn test_naiveweek_hash() { 314 | let a = 315 | NaiveWeek { date: NaiveDate::from_ymd_opt(2025, 4, 3).unwrap(), start: Weekday::Mon }; 316 | let b = 317 | NaiveWeek { date: NaiveDate::from_ymd_opt(2025, 4, 4).unwrap(), start: Weekday::Mon }; 318 | let c = 319 | NaiveWeek { date: NaiveDate::from_ymd_opt(2025, 4, 3).unwrap(), start: Weekday::Sun }; 320 | 321 | let mut hasher = DefaultHasher::default(); 322 | a.hash(&mut hasher); 323 | let a_hash = hasher.finish(); 324 | 325 | hasher = DefaultHasher::default(); 326 | b.hash(&mut hasher); 327 | let b_hash = hasher.finish(); 328 | 329 | hasher = DefaultHasher::default(); 330 | c.hash(&mut hasher); 331 | let c_hash = hasher.finish(); 332 | 333 | assert_eq!(a_hash, b_hash); 334 | assert_ne!(b_hash, c_hash); 335 | assert_ne!(a_hash, c_hash); 336 | } 337 | } 338 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Rust-chrono is dual-licensed under The MIT License [1] and 2 | Apache 2.0 License [2]. Copyright (c) 2014--2025, Kang Seonghoon and 3 | contributors. 4 | 5 | Nota Bene: This is same as the Rust Project's own license. 6 | 7 | 8 | [1]: , which is reproduced below: 9 | 10 | ~~~~ 11 | The MIT License (MIT) 12 | 13 | Copyright (c) 2014, Kang Seonghoon. 14 | 15 | Permission is hereby granted, free of charge, to any person obtaining a copy 16 | of this software and associated documentation files (the "Software"), to deal 17 | in the Software without restriction, including without limitation the rights 18 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 19 | copies of the Software, and to permit persons to whom the Software is 20 | furnished to do so, subject to the following conditions: 21 | 22 | The above copyright notice and this permission notice shall be included in 23 | all copies or substantial portions of the Software. 24 | 25 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 26 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 27 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 28 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 29 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 30 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 31 | THE SOFTWARE. 32 | ~~~~ 33 | 34 | 35 | [2]: , which is reproduced below: 36 | 37 | ~~~~ 38 | Apache License 39 | Version 2.0, January 2004 40 | http://www.apache.org/licenses/ 41 | 42 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 43 | 44 | 1. Definitions. 45 | 46 | "License" shall mean the terms and conditions for use, reproduction, 47 | and distribution as defined by Sections 1 through 9 of this document. 48 | 49 | "Licensor" shall mean the copyright owner or entity authorized by 50 | the copyright owner that is granting the License. 51 | 52 | "Legal Entity" shall mean the union of the acting entity and all 53 | other entities that control, are controlled by, or are under common 54 | control with that entity. For the purposes of this definition, 55 | "control" means (i) the power, direct or indirect, to cause the 56 | direction or management of such entity, whether by contract or 57 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 58 | outstanding shares, or (iii) beneficial ownership of such entity. 59 | 60 | "You" (or "Your") shall mean an individual or Legal Entity 61 | exercising permissions granted by this License. 62 | 63 | "Source" form shall mean the preferred form for making modifications, 64 | including but not limited to software source code, documentation 65 | source, and configuration files. 66 | 67 | "Object" form shall mean any form resulting from mechanical 68 | transformation or translation of a Source form, including but 69 | not limited to compiled object code, generated documentation, 70 | and conversions to other media types. 71 | 72 | "Work" shall mean the work of authorship, whether in Source or 73 | Object form, made available under the License, as indicated by a 74 | copyright notice that is included in or attached to the work 75 | (an example is provided in the Appendix below). 76 | 77 | "Derivative Works" shall mean any work, whether in Source or Object 78 | form, that is based on (or derived from) the Work and for which the 79 | editorial revisions, annotations, elaborations, or other modifications 80 | represent, as a whole, an original work of authorship. For the purposes 81 | of this License, Derivative Works shall not include works that remain 82 | separable from, or merely link (or bind by name) to the interfaces of, 83 | the Work and Derivative Works thereof. 84 | 85 | "Contribution" shall mean any work of authorship, including 86 | the original version of the Work and any modifications or additions 87 | to that Work or Derivative Works thereof, that is intentionally 88 | submitted to Licensor for inclusion in the Work by the copyright owner 89 | or by an individual or Legal Entity authorized to submit on behalf of 90 | the copyright owner. For the purposes of this definition, "submitted" 91 | means any form of electronic, verbal, or written communication sent 92 | to the Licensor or its representatives, including but not limited to 93 | communication on electronic mailing lists, source code control systems, 94 | and issue tracking systems that are managed by, or on behalf of, the 95 | Licensor for the purpose of discussing and improving the Work, but 96 | excluding communication that is conspicuously marked or otherwise 97 | designated in writing by the copyright owner as "Not a Contribution." 98 | 99 | "Contributor" shall mean Licensor and any individual or Legal Entity 100 | on behalf of whom a Contribution has been received by Licensor and 101 | subsequently incorporated within the Work. 102 | 103 | 2. Grant of Copyright License. Subject to the terms and conditions of 104 | this License, each Contributor hereby grants to You a perpetual, 105 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 106 | copyright license to reproduce, prepare Derivative Works of, 107 | publicly display, publicly perform, sublicense, and distribute the 108 | Work and such Derivative Works in Source or Object form. 109 | 110 | 3. Grant of Patent License. Subject to the terms and conditions of 111 | this License, each Contributor hereby grants to You a perpetual, 112 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 113 | (except as stated in this section) patent license to make, have made, 114 | use, offer to sell, sell, import, and otherwise transfer the Work, 115 | where such license applies only to those patent claims licensable 116 | by such Contributor that are necessarily infringed by their 117 | Contribution(s) alone or by combination of their Contribution(s) 118 | with the Work to which such Contribution(s) was submitted. If You 119 | institute patent litigation against any entity (including a 120 | cross-claim or counterclaim in a lawsuit) alleging that the Work 121 | or a Contribution incorporated within the Work constitutes direct 122 | or contributory patent infringement, then any patent licenses 123 | granted to You under this License for that Work shall terminate 124 | as of the date such litigation is filed. 125 | 126 | 4. Redistribution. You may reproduce and distribute copies of the 127 | Work or Derivative Works thereof in any medium, with or without 128 | modifications, and in Source or Object form, provided that You 129 | meet the following conditions: 130 | 131 | (a) You must give any other recipients of the Work or 132 | Derivative Works a copy of this License; and 133 | 134 | (b) You must cause any modified files to carry prominent notices 135 | stating that You changed the files; and 136 | 137 | (c) You must retain, in the Source form of any Derivative Works 138 | that You distribute, all copyright, patent, trademark, and 139 | attribution notices from the Source form of the Work, 140 | excluding those notices that do not pertain to any part of 141 | the Derivative Works; and 142 | 143 | (d) If the Work includes a "NOTICE" text file as part of its 144 | distribution, then any Derivative Works that You distribute must 145 | include a readable copy of the attribution notices contained 146 | within such NOTICE file, excluding those notices that do not 147 | pertain to any part of the Derivative Works, in at least one 148 | of the following places: within a NOTICE text file distributed 149 | as part of the Derivative Works; within the Source form or 150 | documentation, if provided along with the Derivative Works; or, 151 | within a display generated by the Derivative Works, if and 152 | wherever such third-party notices normally appear. The contents 153 | of the NOTICE file are for informational purposes only and 154 | do not modify the License. You may add Your own attribution 155 | notices within Derivative Works that You distribute, alongside 156 | or as an addendum to the NOTICE text from the Work, provided 157 | that such additional attribution notices cannot be construed 158 | as modifying the License. 159 | 160 | You may add Your own copyright statement to Your modifications and 161 | may provide additional or different license terms and conditions 162 | for use, reproduction, or distribution of Your modifications, or 163 | for any such Derivative Works as a whole, provided Your use, 164 | reproduction, and distribution of the Work otherwise complies with 165 | the conditions stated in this License. 166 | 167 | 5. Submission of Contributions. Unless You explicitly state otherwise, 168 | any Contribution intentionally submitted for inclusion in the Work 169 | by You to the Licensor shall be under the terms and conditions of 170 | this License, without any additional terms or conditions. 171 | Notwithstanding the above, nothing herein shall supersede or modify 172 | the terms of any separate license agreement you may have executed 173 | with Licensor regarding such Contributions. 174 | 175 | 6. Trademarks. This License does not grant permission to use the trade 176 | names, trademarks, service marks, or product names of the Licensor, 177 | except as required for reasonable and customary use in describing the 178 | origin of the Work and reproducing the content of the NOTICE file. 179 | 180 | 7. Disclaimer of Warranty. Unless required by applicable law or 181 | agreed to in writing, Licensor provides the Work (and each 182 | Contributor provides its Contributions) on an "AS IS" BASIS, 183 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 184 | implied, including, without limitation, any warranties or conditions 185 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 186 | PARTICULAR PURPOSE. You are solely responsible for determining the 187 | appropriateness of using or redistributing the Work and assume any 188 | risks associated with Your exercise of permissions under this License. 189 | 190 | 8. Limitation of Liability. In no event and under no legal theory, 191 | whether in tort (including negligence), contract, or otherwise, 192 | unless required by applicable law (such as deliberate and grossly 193 | negligent acts) or agreed to in writing, shall any Contributor be 194 | liable to You for damages, including any direct, indirect, special, 195 | incidental, or consequential damages of any character arising as a 196 | result of this License or out of the use or inability to use the 197 | Work (including but not limited to damages for loss of goodwill, 198 | work stoppage, computer failure or malfunction, or any and all 199 | other commercial damages or losses), even if such Contributor 200 | has been advised of the possibility of such damages. 201 | 202 | 9. Accepting Warranty or Additional Liability. While redistributing 203 | the Work or Derivative Works thereof, You may choose to offer, 204 | and charge a fee for, acceptance of support, warranty, indemnity, 205 | or other liability obligations and/or rights consistent with this 206 | License. However, in accepting such obligations, You may act only 207 | on Your own behalf and on Your sole responsibility, not on behalf 208 | of any other Contributor, and only if You agree to indemnify, 209 | defend, and hold each Contributor harmless for any liability 210 | incurred by, or claims asserted against, such Contributor by reason 211 | of your accepting any such warranty or additional liability. 212 | 213 | END OF TERMS AND CONDITIONS 214 | 215 | APPENDIX: How to apply the Apache License to your work. 216 | 217 | To apply the Apache License to your work, attach the following 218 | boilerplate notice, with the fields enclosed by brackets "[]" 219 | replaced with your own identifying information. (Don't include 220 | the brackets!) The text should be enclosed in the appropriate 221 | comment syntax for the file format. We also recommend that a 222 | file or class name and description of purpose be included on the 223 | same "printed page" as the copyright notice for easier 224 | identification within third-party archives. 225 | 226 | Copyright [yyyy] [name of copyright owner] 227 | 228 | Licensed under the Apache License, Version 2.0 (the "License"); 229 | you may not use this file except in compliance with the License. 230 | You may obtain a copy of the License at 231 | 232 | http://www.apache.org/licenses/LICENSE-2.0 233 | 234 | Unless required by applicable law or agreed to in writing, software 235 | distributed under the License is distributed on an "AS IS" BASIS, 236 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 237 | See the License for the specific language governing permissions and 238 | limitations under the License. 239 | ~~~~ 240 | 241 | -------------------------------------------------------------------------------- /src/offset/local/tz_info/parser.rs: -------------------------------------------------------------------------------- 1 | use std::io::{self, ErrorKind}; 2 | use std::iter; 3 | use std::num::ParseIntError; 4 | use std::str::{self, FromStr}; 5 | 6 | use super::Error; 7 | use super::rule::TransitionRule; 8 | use super::timezone::{LeapSecond, LocalTimeType, TimeZone, Transition}; 9 | 10 | pub(super) fn parse(bytes: &[u8]) -> Result { 11 | let mut cursor = Cursor::new(bytes); 12 | let state = State::new(&mut cursor, true)?; 13 | let (state, footer) = match state.header.version { 14 | Version::V1 => match cursor.is_empty() { 15 | true => (state, None), 16 | false => { 17 | return Err(Error::InvalidTzFile("remaining data after end of TZif v1 data block")); 18 | } 19 | }, 20 | Version::V2 | Version::V3 => { 21 | let state = State::new(&mut cursor, false)?; 22 | (state, Some(cursor.remaining())) 23 | } 24 | }; 25 | 26 | let mut transitions = Vec::with_capacity(state.header.transition_count); 27 | for (arr_time, &local_time_type_index) in 28 | state.transition_times.chunks_exact(state.time_size).zip(state.transition_types) 29 | { 30 | let unix_leap_time = 31 | state.parse_time(&arr_time[0..state.time_size], state.header.version)?; 32 | let local_time_type_index = local_time_type_index as usize; 33 | transitions.push(Transition::new(unix_leap_time, local_time_type_index)); 34 | } 35 | 36 | let mut local_time_types = Vec::with_capacity(state.header.type_count); 37 | for arr in state.local_time_types.chunks_exact(6) { 38 | let ut_offset = read_be_i32(&arr[..4])?; 39 | 40 | let is_dst = match arr[4] { 41 | 0 => false, 42 | 1 => true, 43 | _ => return Err(Error::InvalidTzFile("invalid DST indicator")), 44 | }; 45 | 46 | let char_index = arr[5] as usize; 47 | if char_index >= state.header.char_count { 48 | return Err(Error::InvalidTzFile("invalid time zone name char index")); 49 | } 50 | 51 | let position = match state.names[char_index..].iter().position(|&c| c == b'\0') { 52 | Some(position) => position, 53 | None => return Err(Error::InvalidTzFile("invalid time zone name char index")), 54 | }; 55 | 56 | let name = &state.names[char_index..char_index + position]; 57 | let name = if !name.is_empty() { Some(name) } else { None }; 58 | local_time_types.push(LocalTimeType::new(ut_offset, is_dst, name)?); 59 | } 60 | 61 | let mut leap_seconds = Vec::with_capacity(state.header.leap_count); 62 | for arr in state.leap_seconds.chunks_exact(state.time_size + 4) { 63 | let unix_leap_time = state.parse_time(&arr[0..state.time_size], state.header.version)?; 64 | let correction = read_be_i32(&arr[state.time_size..state.time_size + 4])?; 65 | leap_seconds.push(LeapSecond::new(unix_leap_time, correction)); 66 | } 67 | 68 | let std_walls_iter = state.std_walls.iter().copied().chain(iter::repeat(0)); 69 | let ut_locals_iter = state.ut_locals.iter().copied().chain(iter::repeat(0)); 70 | if std_walls_iter.zip(ut_locals_iter).take(state.header.type_count).any(|pair| pair == (0, 1)) { 71 | return Err(Error::InvalidTzFile( 72 | "invalid couple of standard/wall and UT/local indicators", 73 | )); 74 | } 75 | 76 | let extra_rule = match footer { 77 | Some(footer) => { 78 | let footer = str::from_utf8(footer)?; 79 | if !(footer.starts_with('\n') && footer.ends_with('\n')) { 80 | return Err(Error::InvalidTzFile("invalid footer")); 81 | } 82 | 83 | let tz_string = footer.trim_matches(|c: char| c.is_ascii_whitespace()); 84 | if tz_string.starts_with(':') || tz_string.contains('\0') { 85 | return Err(Error::InvalidTzFile("invalid footer")); 86 | } 87 | 88 | match tz_string.is_empty() { 89 | true => None, 90 | false => Some(TransitionRule::from_tz_string( 91 | tz_string.as_bytes(), 92 | state.header.version == Version::V3, 93 | )?), 94 | } 95 | } 96 | None => None, 97 | }; 98 | 99 | TimeZone::new(transitions, local_time_types, leap_seconds, extra_rule) 100 | } 101 | 102 | /// TZif data blocks 103 | struct State<'a> { 104 | header: Header, 105 | /// Time size in bytes 106 | time_size: usize, 107 | /// Transition times data block 108 | transition_times: &'a [u8], 109 | /// Transition types data block 110 | transition_types: &'a [u8], 111 | /// Local time types data block 112 | local_time_types: &'a [u8], 113 | /// Time zone names data block 114 | names: &'a [u8], 115 | /// Leap seconds data block 116 | leap_seconds: &'a [u8], 117 | /// UT/local indicators data block 118 | std_walls: &'a [u8], 119 | /// Standard/wall indicators data block 120 | ut_locals: &'a [u8], 121 | } 122 | 123 | impl<'a> State<'a> { 124 | /// Read TZif data blocks 125 | fn new(cursor: &mut Cursor<'a>, first: bool) -> Result { 126 | let header = Header::new(cursor)?; 127 | let time_size = match first { 128 | true => 4, // We always parse V1 first 129 | false => 8, 130 | }; 131 | 132 | Ok(Self { 133 | time_size, 134 | transition_times: cursor.read_exact(header.transition_count * time_size)?, 135 | transition_types: cursor.read_exact(header.transition_count)?, 136 | local_time_types: cursor.read_exact(header.type_count * 6)?, 137 | names: cursor.read_exact(header.char_count)?, 138 | leap_seconds: cursor.read_exact(header.leap_count * (time_size + 4))?, 139 | std_walls: cursor.read_exact(header.std_wall_count)?, 140 | ut_locals: cursor.read_exact(header.ut_local_count)?, 141 | header, 142 | }) 143 | } 144 | 145 | /// Parse time values 146 | fn parse_time(&self, arr: &[u8], version: Version) -> Result { 147 | match version { 148 | Version::V1 => Ok(read_be_i32(&arr[..4])?.into()), 149 | Version::V2 | Version::V3 => read_be_i64(arr), 150 | } 151 | } 152 | } 153 | 154 | /// TZif header 155 | #[derive(Debug)] 156 | struct Header { 157 | /// TZif version 158 | version: Version, 159 | /// Number of UT/local indicators 160 | ut_local_count: usize, 161 | /// Number of standard/wall indicators 162 | std_wall_count: usize, 163 | /// Number of leap-second records 164 | leap_count: usize, 165 | /// Number of transition times 166 | transition_count: usize, 167 | /// Number of local time type records 168 | type_count: usize, 169 | /// Number of time zone names bytes 170 | char_count: usize, 171 | } 172 | 173 | impl Header { 174 | fn new(cursor: &mut Cursor) -> Result { 175 | let magic = cursor.read_exact(4)?; 176 | if magic != *b"TZif" { 177 | return Err(Error::InvalidTzFile("invalid magic number")); 178 | } 179 | 180 | let version = match cursor.read_exact(1)? { 181 | [0x00] => Version::V1, 182 | [0x32] => Version::V2, 183 | [0x33] => Version::V3, 184 | _ => return Err(Error::UnsupportedTzFile("unsupported TZif version")), 185 | }; 186 | 187 | cursor.read_exact(15)?; 188 | let ut_local_count = cursor.read_be_u32()?; 189 | let std_wall_count = cursor.read_be_u32()?; 190 | let leap_count = cursor.read_be_u32()?; 191 | let transition_count = cursor.read_be_u32()?; 192 | let type_count = cursor.read_be_u32()?; 193 | let char_count = cursor.read_be_u32()?; 194 | 195 | if !(type_count != 0 196 | && char_count != 0 197 | && (ut_local_count == 0 || ut_local_count == type_count) 198 | && (std_wall_count == 0 || std_wall_count == type_count)) 199 | { 200 | return Err(Error::InvalidTzFile("invalid header")); 201 | } 202 | 203 | Ok(Self { 204 | version, 205 | ut_local_count: ut_local_count as usize, 206 | std_wall_count: std_wall_count as usize, 207 | leap_count: leap_count as usize, 208 | transition_count: transition_count as usize, 209 | type_count: type_count as usize, 210 | char_count: char_count as usize, 211 | }) 212 | } 213 | } 214 | 215 | /// A `Cursor` contains a slice of a buffer and a read count. 216 | #[derive(Debug, Eq, PartialEq)] 217 | pub(crate) struct Cursor<'a> { 218 | /// Slice representing the remaining data to be read 219 | remaining: &'a [u8], 220 | /// Number of already read bytes 221 | read_count: usize, 222 | } 223 | 224 | impl<'a> Cursor<'a> { 225 | /// Construct a new `Cursor` from remaining data 226 | pub(crate) const fn new(remaining: &'a [u8]) -> Self { 227 | Self { remaining, read_count: 0 } 228 | } 229 | 230 | pub(crate) fn peek(&self) -> Option<&u8> { 231 | self.remaining().first() 232 | } 233 | 234 | /// Returns remaining data 235 | pub(crate) const fn remaining(&self) -> &'a [u8] { 236 | self.remaining 237 | } 238 | 239 | /// Returns `true` if data is remaining 240 | pub(crate) const fn is_empty(&self) -> bool { 241 | self.remaining.is_empty() 242 | } 243 | 244 | pub(crate) fn read_be_u32(&mut self) -> Result { 245 | let mut buf = [0; 4]; 246 | buf.copy_from_slice(self.read_exact(4)?); 247 | Ok(u32::from_be_bytes(buf)) 248 | } 249 | 250 | #[cfg(target_env = "ohos")] 251 | pub(crate) fn seek_after(&mut self, offset: usize) -> Result { 252 | if offset < self.read_count { 253 | return Err(io::Error::from(ErrorKind::UnexpectedEof)); 254 | } 255 | match self.remaining.get((offset - self.read_count)..) { 256 | Some(remaining) => { 257 | self.remaining = remaining; 258 | self.read_count = offset; 259 | Ok(offset) 260 | } 261 | _ => Err(io::Error::from(ErrorKind::UnexpectedEof)), 262 | } 263 | } 264 | 265 | /// Read exactly `count` bytes, reducing remaining data and incrementing read count 266 | pub(crate) fn read_exact(&mut self, count: usize) -> Result<&'a [u8], io::Error> { 267 | match (self.remaining.get(..count), self.remaining.get(count..)) { 268 | (Some(result), Some(remaining)) => { 269 | self.remaining = remaining; 270 | self.read_count += count; 271 | Ok(result) 272 | } 273 | _ => Err(io::Error::from(ErrorKind::UnexpectedEof)), 274 | } 275 | } 276 | 277 | /// Read bytes and compare them to the provided tag 278 | pub(crate) fn read_tag(&mut self, tag: &[u8]) -> Result<(), io::Error> { 279 | if self.read_exact(tag.len())? == tag { 280 | Ok(()) 281 | } else { 282 | Err(io::Error::from(ErrorKind::InvalidData)) 283 | } 284 | } 285 | 286 | /// Read bytes if the remaining data is prefixed by the provided tag 287 | pub(crate) fn read_optional_tag(&mut self, tag: &[u8]) -> Result { 288 | if self.remaining.starts_with(tag) { 289 | self.read_exact(tag.len())?; 290 | Ok(true) 291 | } else { 292 | Ok(false) 293 | } 294 | } 295 | 296 | /// Read bytes as long as the provided predicate is true 297 | pub(crate) fn read_while bool>(&mut self, f: F) -> Result<&'a [u8], io::Error> { 298 | match self.remaining.iter().position(|x| !f(x)) { 299 | None => self.read_exact(self.remaining.len()), 300 | Some(position) => self.read_exact(position), 301 | } 302 | } 303 | 304 | // Parse an integer out of the ASCII digits 305 | pub(crate) fn read_int>(&mut self) -> Result { 306 | let bytes = self.read_while(u8::is_ascii_digit)?; 307 | Ok(str::from_utf8(bytes)?.parse()?) 308 | } 309 | 310 | /// Read bytes until the provided predicate is true 311 | pub(crate) fn read_until bool>(&mut self, f: F) -> Result<&'a [u8], io::Error> { 312 | match self.remaining.iter().position(f) { 313 | None => self.read_exact(self.remaining.len()), 314 | Some(position) => self.read_exact(position), 315 | } 316 | } 317 | } 318 | 319 | pub(crate) fn read_be_i32(bytes: &[u8]) -> Result { 320 | if bytes.len() != 4 { 321 | return Err(Error::InvalidSlice("too short for i32")); 322 | } 323 | 324 | let mut buf = [0; 4]; 325 | buf.copy_from_slice(bytes); 326 | Ok(i32::from_be_bytes(buf)) 327 | } 328 | 329 | pub(crate) fn read_be_i64(bytes: &[u8]) -> Result { 330 | if bytes.len() != 8 { 331 | return Err(Error::InvalidSlice("too short for i64")); 332 | } 333 | 334 | let mut buf = [0; 8]; 335 | buf.copy_from_slice(bytes); 336 | Ok(i64::from_be_bytes(buf)) 337 | } 338 | 339 | /// TZif version 340 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 341 | enum Version { 342 | /// Version 1 343 | V1, 344 | /// Version 2 345 | V2, 346 | /// Version 3 347 | V3, 348 | } 349 | -------------------------------------------------------------------------------- /src/offset/local/windows.rs: -------------------------------------------------------------------------------- 1 | // Copyright 2012-2014 The Rust Project Developers. See the COPYRIGHT 2 | // file at the top-level directory of this distribution and at 3 | // http://rust-lang.org/COPYRIGHT. 4 | // 5 | // Licensed under the Apache License, Version 2.0 or the MIT license 7 | // , at your 8 | // option. This file may not be copied, modified, or distributed 9 | // except according to those terms. 10 | 11 | use std::cmp::Ordering; 12 | use std::mem::MaybeUninit; 13 | use std::ptr; 14 | 15 | use super::win_bindings::{GetTimeZoneInformationForYear, SYSTEMTIME, TIME_ZONE_INFORMATION}; 16 | 17 | use crate::offset::local::{Transition, lookup_with_dst_transitions}; 18 | use crate::{Datelike, FixedOffset, MappedLocalTime, NaiveDate, NaiveDateTime, NaiveTime, Weekday}; 19 | 20 | // We don't use `SystemTimeToTzSpecificLocalTime` because it doesn't support the same range of dates 21 | // as Chrono. Also it really isn't that difficult to work out the correct offset from the provided 22 | // DST rules. 23 | // 24 | // This method uses `overflowing_sub_offset` because it is no problem if the transition time in UTC 25 | // falls a couple of hours inside the buffer space around the `NaiveDateTime` range (although it is 26 | // very theoretical to have a transition at midnight around `NaiveDate::(MIN|MAX)`. 27 | pub(super) fn offset_from_utc_datetime(utc: &NaiveDateTime) -> MappedLocalTime { 28 | // Using a `TzInfo` based on the year of an UTC datetime is technically wrong, we should be 29 | // using the rules for the year of the corresponding local time. But this matches what 30 | // `SystemTimeToTzSpecificLocalTime` is documented to do. 31 | let tz_info = match TzInfo::for_year(utc.year()) { 32 | Some(tz_info) => tz_info, 33 | None => return MappedLocalTime::None, 34 | }; 35 | let offset = match (tz_info.std_transition, tz_info.dst_transition) { 36 | (Some(std_transition), Some(dst_transition)) => { 37 | let std_transition_utc = std_transition.overflowing_sub_offset(tz_info.dst_offset); 38 | let dst_transition_utc = dst_transition.overflowing_sub_offset(tz_info.std_offset); 39 | if dst_transition_utc < std_transition_utc { 40 | match utc >= &dst_transition_utc && utc < &std_transition_utc { 41 | true => tz_info.dst_offset, 42 | false => tz_info.std_offset, 43 | } 44 | } else { 45 | match utc >= &std_transition_utc && utc < &dst_transition_utc { 46 | true => tz_info.std_offset, 47 | false => tz_info.dst_offset, 48 | } 49 | } 50 | } 51 | (Some(std_transition), None) => { 52 | let std_transition_utc = std_transition.overflowing_sub_offset(tz_info.dst_offset); 53 | match utc < &std_transition_utc { 54 | true => tz_info.dst_offset, 55 | false => tz_info.std_offset, 56 | } 57 | } 58 | (None, Some(dst_transition)) => { 59 | let dst_transition_utc = dst_transition.overflowing_sub_offset(tz_info.std_offset); 60 | match utc < &dst_transition_utc { 61 | true => tz_info.std_offset, 62 | false => tz_info.dst_offset, 63 | } 64 | } 65 | (None, None) => tz_info.std_offset, 66 | }; 67 | MappedLocalTime::Single(offset) 68 | } 69 | 70 | // We don't use `TzSpecificLocalTimeToSystemTime` because it doesn't let us choose how to handle 71 | // ambiguous cases (during a DST transition). Instead we get the timezone information for the 72 | // current year and compute it ourselves, like we do on Unix. 73 | pub(super) fn offset_from_local_datetime(local: &NaiveDateTime) -> MappedLocalTime { 74 | let tz_info = match TzInfo::for_year(local.year()) { 75 | Some(tz_info) => tz_info, 76 | None => return MappedLocalTime::None, 77 | }; 78 | // Create a sorted slice of transitions and use `lookup_with_dst_transitions`. 79 | match (tz_info.std_transition, tz_info.dst_transition) { 80 | (Some(std_transition), Some(dst_transition)) => { 81 | let std_transition = 82 | Transition::new(std_transition, tz_info.dst_offset, tz_info.std_offset); 83 | let dst_transition = 84 | Transition::new(dst_transition, tz_info.std_offset, tz_info.dst_offset); 85 | let transitions = match std_transition.cmp(&dst_transition) { 86 | Ordering::Less => [std_transition, dst_transition], 87 | Ordering::Greater => [dst_transition, std_transition], 88 | Ordering::Equal => { 89 | // This doesn't make sense. Let's just return the standard offset. 90 | return MappedLocalTime::Single(tz_info.std_offset); 91 | } 92 | }; 93 | lookup_with_dst_transitions(&transitions, *local) 94 | } 95 | (Some(std_transition), None) => { 96 | let transitions = 97 | [Transition::new(std_transition, tz_info.dst_offset, tz_info.std_offset)]; 98 | lookup_with_dst_transitions(&transitions, *local) 99 | } 100 | (None, Some(dst_transition)) => { 101 | let transitions = 102 | [Transition::new(dst_transition, tz_info.std_offset, tz_info.dst_offset)]; 103 | lookup_with_dst_transitions(&transitions, *local) 104 | } 105 | (None, None) => MappedLocalTime::Single(tz_info.std_offset), 106 | } 107 | } 108 | 109 | // The basis for Windows timezone and DST support has been in place since Windows 2000. It does not 110 | // allow for complex rules like the IANA timezone database: 111 | // - A timezone has the same base offset the whole year. 112 | // - There seem to be either zero or two DST transitions (but we support having just one). 113 | // - As of Vista(?) only years from 2004 until a few years into the future are supported. 114 | // - All other years get the base settings, which seem to be that of the current year. 115 | // 116 | // These details don't matter much, we just work with the offsets and transition dates Windows 117 | // returns through `GetTimeZoneInformationForYear` for a particular year. 118 | struct TzInfo { 119 | // Offset from UTC during standard time. 120 | std_offset: FixedOffset, 121 | // Offset from UTC during daylight saving time. 122 | dst_offset: FixedOffset, 123 | // Transition from standard time to daylight saving time, given in local standard time. 124 | std_transition: Option, 125 | // Transition from daylight saving time to standard time, given in local daylight saving time. 126 | dst_transition: Option, 127 | } 128 | 129 | impl TzInfo { 130 | fn for_year(year: i32) -> Option { 131 | // The API limits years to 1601..=30827. 132 | // Working with timezones and daylight saving time this far into the past or future makes 133 | // little sense. But whatever is extrapolated for 1601 or 30827 is what can be extrapolated 134 | // for years beyond. 135 | let ref_year = year.clamp(1601, 30827) as u16; 136 | let tz_info = unsafe { 137 | let mut tz_info = MaybeUninit::::uninit(); 138 | if GetTimeZoneInformationForYear(ref_year, ptr::null_mut(), tz_info.as_mut_ptr()) == 0 { 139 | return None; 140 | } 141 | tz_info.assume_init() 142 | }; 143 | let std_offset = (tz_info.Bias) 144 | .checked_add(tz_info.StandardBias) 145 | .and_then(|o| o.checked_mul(60)) 146 | .and_then(FixedOffset::west_opt)?; 147 | let dst_offset = (tz_info.Bias) 148 | .checked_add(tz_info.DaylightBias) 149 | .and_then(|o| o.checked_mul(60)) 150 | .and_then(FixedOffset::west_opt)?; 151 | Some(TzInfo { 152 | std_offset, 153 | dst_offset, 154 | std_transition: naive_date_time_from_system_time(tz_info.StandardDate, year).ok()?, 155 | dst_transition: naive_date_time_from_system_time(tz_info.DaylightDate, year).ok()?, 156 | }) 157 | } 158 | } 159 | 160 | /// Resolve a `SYSTEMTIME` object to an `Option`. 161 | /// 162 | /// A `SYSTEMTIME` within a `TIME_ZONE_INFORMATION` struct can be zero to indicate there is no 163 | /// transition. 164 | /// If it has year, month and day values it is a concrete date. 165 | /// If the year is missing the `SYSTEMTIME` is a rule, which this method resolves for the provided 166 | /// year. A rule has a month, weekday, and nth weekday of the month as components. 167 | /// 168 | /// Returns `Err` if any of the values is invalid, which should never happen. 169 | fn naive_date_time_from_system_time( 170 | st: SYSTEMTIME, 171 | year: i32, 172 | ) -> Result, ()> { 173 | if st.wYear == 0 && st.wMonth == 0 { 174 | return Ok(None); 175 | } 176 | let time = NaiveTime::from_hms_milli_opt( 177 | st.wHour as u32, 178 | st.wMinute as u32, 179 | st.wSecond as u32, 180 | st.wMilliseconds as u32, 181 | ) 182 | .ok_or(())?; 183 | 184 | if st.wYear != 0 { 185 | // We have a concrete date. 186 | let date = 187 | NaiveDate::from_ymd_opt(st.wYear as i32, st.wMonth as u32, st.wDay as u32).ok_or(())?; 188 | return Ok(Some(date.and_time(time))); 189 | } 190 | 191 | // Resolve a rule with month, weekday, and nth weekday of the month to a date in the current 192 | // year. 193 | let weekday = match st.wDayOfWeek { 194 | 0 => Weekday::Sun, 195 | 1 => Weekday::Mon, 196 | 2 => Weekday::Tue, 197 | 3 => Weekday::Wed, 198 | 4 => Weekday::Thu, 199 | 5 => Weekday::Fri, 200 | 6 => Weekday::Sat, 201 | _ => return Err(()), 202 | }; 203 | let nth_day = match st.wDay { 204 | 1..=5 => st.wDay as u8, 205 | _ => return Err(()), 206 | }; 207 | let date = NaiveDate::from_weekday_of_month_opt(year, st.wMonth as u32, weekday, nth_day) 208 | .or_else(|| NaiveDate::from_weekday_of_month_opt(year, st.wMonth as u32, weekday, 4)) 209 | .ok_or(())?; // `st.wMonth` must be invalid 210 | Ok(Some(date.and_time(time))) 211 | } 212 | 213 | #[cfg(test)] 214 | mod tests { 215 | use crate::offset::local::win_bindings::{ 216 | FILETIME, SYSTEMTIME, SystemTimeToFileTime, TzSpecificLocalTimeToSystemTime, 217 | }; 218 | use crate::{DateTime, FixedOffset, Local, NaiveDate, NaiveDateTime, TimeDelta}; 219 | use crate::{Datelike, TimeZone, Timelike}; 220 | use std::mem::MaybeUninit; 221 | use std::ptr; 222 | 223 | #[test] 224 | fn verify_against_tz_specific_local_time_to_system_time() { 225 | // The implementation in Windows itself is the source of truth on how to work with the OS 226 | // timezone information. This test compares for every hour over a period of 125 years our 227 | // implementation to `TzSpecificLocalTimeToSystemTime`. 228 | // 229 | // This uses parts of a previous Windows `Local` implementation in chrono. 230 | fn from_local_time(dt: &NaiveDateTime) -> DateTime { 231 | let st = system_time_from_naive_date_time(dt); 232 | let utc_time = local_to_utc_time(&st); 233 | let utc_secs = system_time_as_unix_seconds(&utc_time); 234 | let local_secs = system_time_as_unix_seconds(&st); 235 | let offset = (local_secs - utc_secs) as i32; 236 | let offset = FixedOffset::east_opt(offset).unwrap(); 237 | DateTime::from_naive_utc_and_offset(*dt - offset, offset) 238 | } 239 | fn system_time_from_naive_date_time(dt: &NaiveDateTime) -> SYSTEMTIME { 240 | SYSTEMTIME { 241 | // Valid values: 1601-30827 242 | wYear: dt.year() as u16, 243 | // Valid values:1-12 244 | wMonth: dt.month() as u16, 245 | // Valid values: 0-6, starting Sunday. 246 | // NOTE: enum returns 1-7, starting Monday, so we are 247 | // off here, but this is not currently used in local. 248 | wDayOfWeek: dt.weekday() as u16, 249 | // Valid values: 1-31 250 | wDay: dt.day() as u16, 251 | // Valid values: 0-23 252 | wHour: dt.hour() as u16, 253 | // Valid values: 0-59 254 | wMinute: dt.minute() as u16, 255 | // Valid values: 0-59 256 | wSecond: dt.second() as u16, 257 | // Valid values: 0-999 258 | wMilliseconds: 0, 259 | } 260 | } 261 | fn local_to_utc_time(local: &SYSTEMTIME) -> SYSTEMTIME { 262 | let mut sys_time = MaybeUninit::::uninit(); 263 | unsafe { TzSpecificLocalTimeToSystemTime(ptr::null(), local, sys_time.as_mut_ptr()) }; 264 | // SAFETY: TzSpecificLocalTimeToSystemTime must have succeeded at this point, so we can 265 | // assume the value is initialized. 266 | unsafe { sys_time.assume_init() } 267 | } 268 | const HECTONANOSECS_IN_SEC: i64 = 10_000_000; 269 | const HECTONANOSEC_TO_UNIX_EPOCH: i64 = 11_644_473_600 * HECTONANOSECS_IN_SEC; 270 | fn system_time_as_unix_seconds(st: &SYSTEMTIME) -> i64 { 271 | let mut init = MaybeUninit::::uninit(); 272 | unsafe { 273 | SystemTimeToFileTime(st, init.as_mut_ptr()); 274 | } 275 | // SystemTimeToFileTime must have succeeded at this point, so we can assume the value is 276 | // initialized. 277 | let filetime = unsafe { init.assume_init() }; 278 | let bit_shift = 279 | ((filetime.dwHighDateTime as u64) << 32) | (filetime.dwLowDateTime as u64); 280 | (bit_shift as i64 - HECTONANOSEC_TO_UNIX_EPOCH) / HECTONANOSECS_IN_SEC 281 | } 282 | 283 | let mut date = NaiveDate::from_ymd_opt(1975, 1, 1).unwrap().and_hms_opt(0, 30, 0).unwrap(); 284 | 285 | while date.year() < 2078 { 286 | // Windows doesn't handle non-existing dates, it just treats it as valid. 287 | if let Some(our_result) = Local.from_local_datetime(&date).earliest() { 288 | assert_eq!(from_local_time(&date), our_result); 289 | } 290 | date += TimeDelta::try_hours(1).unwrap(); 291 | } 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /src/weekday.rs: -------------------------------------------------------------------------------- 1 | use core::fmt; 2 | 3 | #[cfg(any(feature = "rkyv", feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"))] 4 | use rkyv::{Archive, Deserialize, Serialize}; 5 | 6 | use crate::OutOfRange; 7 | 8 | /// The day of week. 9 | /// 10 | /// The order of the days of week depends on the context. 11 | /// (This is why this type does *not* implement `PartialOrd` or `Ord` traits.) 12 | /// One should prefer `*_from_monday` or `*_from_sunday` methods to get the correct result. 13 | /// 14 | /// # Example 15 | /// ``` 16 | /// use chrono::Weekday; 17 | /// 18 | /// let monday = "Monday".parse::().unwrap(); 19 | /// assert_eq!(monday, Weekday::Mon); 20 | /// 21 | /// let sunday = Weekday::try_from(6).unwrap(); 22 | /// assert_eq!(sunday, Weekday::Sun); 23 | /// 24 | /// assert_eq!(sunday.num_days_from_monday(), 6); // starts counting with Monday = 0 25 | /// assert_eq!(sunday.number_from_monday(), 7); // starts counting with Monday = 1 26 | /// assert_eq!(sunday.num_days_from_sunday(), 0); // starts counting with Sunday = 0 27 | /// assert_eq!(sunday.number_from_sunday(), 1); // starts counting with Sunday = 1 28 | /// 29 | /// assert_eq!(sunday.succ(), monday); 30 | /// assert_eq!(sunday.pred(), Weekday::Sat); 31 | /// ``` 32 | #[derive(PartialEq, Eq, Copy, Clone, Debug, Hash)] 33 | #[cfg_attr( 34 | any(feature = "rkyv", feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"), 35 | derive(Archive, Deserialize, Serialize), 36 | archive(compare(PartialEq)), 37 | archive_attr(derive(Clone, Copy, PartialEq, Eq, Debug, Hash)) 38 | )] 39 | #[cfg_attr(feature = "rkyv-validation", archive(check_bytes))] 40 | #[cfg_attr(all(feature = "arbitrary", feature = "std"), derive(arbitrary::Arbitrary))] 41 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 42 | pub enum Weekday { 43 | /// Monday. 44 | Mon = 0, 45 | /// Tuesday. 46 | Tue = 1, 47 | /// Wednesday. 48 | Wed = 2, 49 | /// Thursday. 50 | Thu = 3, 51 | /// Friday. 52 | Fri = 4, 53 | /// Saturday. 54 | Sat = 5, 55 | /// Sunday. 56 | Sun = 6, 57 | } 58 | 59 | impl Weekday { 60 | /// The next day in the week. 61 | /// 62 | /// `w`: | `Mon` | `Tue` | `Wed` | `Thu` | `Fri` | `Sat` | `Sun` 63 | /// ----------- | ----- | ----- | ----- | ----- | ----- | ----- | ----- 64 | /// `w.succ()`: | `Tue` | `Wed` | `Thu` | `Fri` | `Sat` | `Sun` | `Mon` 65 | #[inline] 66 | #[must_use] 67 | pub const fn succ(&self) -> Weekday { 68 | match *self { 69 | Weekday::Mon => Weekday::Tue, 70 | Weekday::Tue => Weekday::Wed, 71 | Weekday::Wed => Weekday::Thu, 72 | Weekday::Thu => Weekday::Fri, 73 | Weekday::Fri => Weekday::Sat, 74 | Weekday::Sat => Weekday::Sun, 75 | Weekday::Sun => Weekday::Mon, 76 | } 77 | } 78 | 79 | /// The previous day in the week. 80 | /// 81 | /// `w`: | `Mon` | `Tue` | `Wed` | `Thu` | `Fri` | `Sat` | `Sun` 82 | /// ----------- | ----- | ----- | ----- | ----- | ----- | ----- | ----- 83 | /// `w.pred()`: | `Sun` | `Mon` | `Tue` | `Wed` | `Thu` | `Fri` | `Sat` 84 | #[inline] 85 | #[must_use] 86 | pub const fn pred(&self) -> Weekday { 87 | match *self { 88 | Weekday::Mon => Weekday::Sun, 89 | Weekday::Tue => Weekday::Mon, 90 | Weekday::Wed => Weekday::Tue, 91 | Weekday::Thu => Weekday::Wed, 92 | Weekday::Fri => Weekday::Thu, 93 | Weekday::Sat => Weekday::Fri, 94 | Weekday::Sun => Weekday::Sat, 95 | } 96 | } 97 | 98 | /// Returns a day-of-week number starting from Monday = 1. (ISO 8601 weekday number) 99 | /// 100 | /// `w`: | `Mon` | `Tue` | `Wed` | `Thu` | `Fri` | `Sat` | `Sun` 101 | /// ------------------------- | ----- | ----- | ----- | ----- | ----- | ----- | ----- 102 | /// `w.number_from_monday()`: | 1 | 2 | 3 | 4 | 5 | 6 | 7 103 | #[inline] 104 | pub const fn number_from_monday(&self) -> u32 { 105 | self.days_since(Weekday::Mon) + 1 106 | } 107 | 108 | /// Returns a day-of-week number starting from Sunday = 1. 109 | /// 110 | /// `w`: | `Mon` | `Tue` | `Wed` | `Thu` | `Fri` | `Sat` | `Sun` 111 | /// ------------------------- | ----- | ----- | ----- | ----- | ----- | ----- | ----- 112 | /// `w.number_from_sunday()`: | 2 | 3 | 4 | 5 | 6 | 7 | 1 113 | #[inline] 114 | pub const fn number_from_sunday(&self) -> u32 { 115 | self.days_since(Weekday::Sun) + 1 116 | } 117 | 118 | /// Returns a day-of-week number starting from Monday = 0. 119 | /// 120 | /// `w`: | `Mon` | `Tue` | `Wed` | `Thu` | `Fri` | `Sat` | `Sun` 121 | /// --------------------------- | ----- | ----- | ----- | ----- | ----- | ----- | ----- 122 | /// `w.num_days_from_monday()`: | 0 | 1 | 2 | 3 | 4 | 5 | 6 123 | /// 124 | /// # Example 125 | /// 126 | /// ``` 127 | /// # #[cfg(feature = "clock")] { 128 | /// # use chrono::{Local, Datelike}; 129 | /// // MTWRFSU is occasionally used as a single-letter abbreviation of the weekdays. 130 | /// // Use `num_days_from_monday` to index into the array. 131 | /// const MTWRFSU: [char; 7] = ['M', 'T', 'W', 'R', 'F', 'S', 'U']; 132 | /// 133 | /// let today = Local::now().weekday(); 134 | /// println!("{}", MTWRFSU[today.num_days_from_monday() as usize]); 135 | /// # } 136 | /// ``` 137 | #[inline] 138 | pub const fn num_days_from_monday(&self) -> u32 { 139 | self.days_since(Weekday::Mon) 140 | } 141 | 142 | /// Returns a day-of-week number starting from Sunday = 0. 143 | /// 144 | /// `w`: | `Mon` | `Tue` | `Wed` | `Thu` | `Fri` | `Sat` | `Sun` 145 | /// --------------------------- | ----- | ----- | ----- | ----- | ----- | ----- | ----- 146 | /// `w.num_days_from_sunday()`: | 1 | 2 | 3 | 4 | 5 | 6 | 0 147 | #[inline] 148 | pub const fn num_days_from_sunday(&self) -> u32 { 149 | self.days_since(Weekday::Sun) 150 | } 151 | 152 | /// The number of days since the given day. 153 | /// 154 | /// # Examples 155 | /// 156 | /// ``` 157 | /// use chrono::Weekday::*; 158 | /// assert_eq!(Mon.days_since(Mon), 0); 159 | /// assert_eq!(Sun.days_since(Tue), 5); 160 | /// assert_eq!(Wed.days_since(Sun), 3); 161 | /// ``` 162 | pub const fn days_since(&self, other: Weekday) -> u32 { 163 | let lhs = *self as u32; 164 | let rhs = other as u32; 165 | if lhs < rhs { 7 + lhs - rhs } else { lhs - rhs } 166 | } 167 | } 168 | 169 | impl fmt::Display for Weekday { 170 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 171 | f.pad(match *self { 172 | Weekday::Mon => "Mon", 173 | Weekday::Tue => "Tue", 174 | Weekday::Wed => "Wed", 175 | Weekday::Thu => "Thu", 176 | Weekday::Fri => "Fri", 177 | Weekday::Sat => "Sat", 178 | Weekday::Sun => "Sun", 179 | }) 180 | } 181 | } 182 | 183 | /// Any weekday can be represented as an integer from 0 to 6, which equals to 184 | /// [`Weekday::num_days_from_monday`](#method.num_days_from_monday) in this implementation. 185 | /// Do not heavily depend on this though; use explicit methods whenever possible. 186 | impl TryFrom for Weekday { 187 | type Error = OutOfRange; 188 | 189 | fn try_from(value: u8) -> Result { 190 | match value { 191 | 0 => Ok(Weekday::Mon), 192 | 1 => Ok(Weekday::Tue), 193 | 2 => Ok(Weekday::Wed), 194 | 3 => Ok(Weekday::Thu), 195 | 4 => Ok(Weekday::Fri), 196 | 5 => Ok(Weekday::Sat), 197 | 6 => Ok(Weekday::Sun), 198 | _ => Err(OutOfRange::new()), 199 | } 200 | } 201 | } 202 | 203 | /// Any weekday can be represented as an integer from 0 to 6, which equals to 204 | /// [`Weekday::num_days_from_monday`](#method.num_days_from_monday) in this implementation. 205 | /// Do not heavily depend on this though; use explicit methods whenever possible. 206 | impl num_traits::FromPrimitive for Weekday { 207 | #[inline] 208 | fn from_i64(n: i64) -> Option { 209 | match n { 210 | 0 => Some(Weekday::Mon), 211 | 1 => Some(Weekday::Tue), 212 | 2 => Some(Weekday::Wed), 213 | 3 => Some(Weekday::Thu), 214 | 4 => Some(Weekday::Fri), 215 | 5 => Some(Weekday::Sat), 216 | 6 => Some(Weekday::Sun), 217 | _ => None, 218 | } 219 | } 220 | 221 | #[inline] 222 | fn from_u64(n: u64) -> Option { 223 | match n { 224 | 0 => Some(Weekday::Mon), 225 | 1 => Some(Weekday::Tue), 226 | 2 => Some(Weekday::Wed), 227 | 3 => Some(Weekday::Thu), 228 | 4 => Some(Weekday::Fri), 229 | 5 => Some(Weekday::Sat), 230 | 6 => Some(Weekday::Sun), 231 | _ => None, 232 | } 233 | } 234 | } 235 | 236 | /// An error resulting from reading `Weekday` value with `FromStr`. 237 | #[derive(Clone, PartialEq, Eq)] 238 | pub struct ParseWeekdayError { 239 | pub(crate) _dummy: (), 240 | } 241 | 242 | #[cfg(all(not(feature = "std"), feature = "core-error"))] 243 | impl core::error::Error for ParseWeekdayError {} 244 | 245 | #[cfg(feature = "std")] 246 | impl std::error::Error for ParseWeekdayError {} 247 | 248 | impl fmt::Display for ParseWeekdayError { 249 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 250 | f.write_fmt(format_args!("{self:?}")) 251 | } 252 | } 253 | 254 | impl fmt::Debug for ParseWeekdayError { 255 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 256 | write!(f, "ParseWeekdayError {{ .. }}") 257 | } 258 | } 259 | 260 | #[cfg(feature = "defmt")] 261 | impl defmt::Format for ParseWeekdayError { 262 | fn format(&self, fmt: defmt::Formatter) { 263 | defmt::write!(fmt, "ParseWeekdayError {{ .. }}") 264 | } 265 | } 266 | 267 | // the actual `FromStr` implementation is in the `format` module to leverage the existing code 268 | 269 | #[cfg(feature = "serde")] 270 | mod weekday_serde { 271 | use super::Weekday; 272 | use core::fmt; 273 | use serde::{de, ser}; 274 | 275 | impl ser::Serialize for Weekday { 276 | fn serialize(&self, serializer: S) -> Result 277 | where 278 | S: ser::Serializer, 279 | { 280 | serializer.collect_str(&self) 281 | } 282 | } 283 | 284 | struct WeekdayVisitor; 285 | 286 | impl de::Visitor<'_> for WeekdayVisitor { 287 | type Value = Weekday; 288 | 289 | fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { 290 | f.write_str("Weekday") 291 | } 292 | 293 | fn visit_str(self, value: &str) -> Result 294 | where 295 | E: de::Error, 296 | { 297 | value.parse().map_err(|_| E::custom("short or long weekday names expected")) 298 | } 299 | } 300 | 301 | impl<'de> de::Deserialize<'de> for Weekday { 302 | fn deserialize(deserializer: D) -> Result 303 | where 304 | D: de::Deserializer<'de>, 305 | { 306 | deserializer.deserialize_str(WeekdayVisitor) 307 | } 308 | } 309 | } 310 | 311 | #[cfg(test)] 312 | mod tests { 313 | use super::Weekday; 314 | 315 | #[test] 316 | fn test_days_since() { 317 | for i in 0..7 { 318 | let base_day = Weekday::try_from(i).unwrap(); 319 | 320 | assert_eq!(base_day.num_days_from_monday(), base_day.days_since(Weekday::Mon)); 321 | assert_eq!(base_day.num_days_from_sunday(), base_day.days_since(Weekday::Sun)); 322 | 323 | assert_eq!(base_day.days_since(base_day), 0); 324 | 325 | assert_eq!(base_day.days_since(base_day.pred()), 1); 326 | assert_eq!(base_day.days_since(base_day.pred().pred()), 2); 327 | assert_eq!(base_day.days_since(base_day.pred().pred().pred()), 3); 328 | assert_eq!(base_day.days_since(base_day.pred().pred().pred().pred()), 4); 329 | assert_eq!(base_day.days_since(base_day.pred().pred().pred().pred().pred()), 5); 330 | assert_eq!(base_day.days_since(base_day.pred().pred().pred().pred().pred().pred()), 6); 331 | 332 | assert_eq!(base_day.days_since(base_day.succ()), 6); 333 | assert_eq!(base_day.days_since(base_day.succ().succ()), 5); 334 | assert_eq!(base_day.days_since(base_day.succ().succ().succ()), 4); 335 | assert_eq!(base_day.days_since(base_day.succ().succ().succ().succ()), 3); 336 | assert_eq!(base_day.days_since(base_day.succ().succ().succ().succ().succ()), 2); 337 | assert_eq!(base_day.days_since(base_day.succ().succ().succ().succ().succ().succ()), 1); 338 | } 339 | } 340 | 341 | #[test] 342 | fn test_formatting_alignment() { 343 | // No exhaustive testing here as we just delegate the 344 | // implementation to Formatter::pad. Just some basic smoke 345 | // testing to ensure that it's in fact being done. 346 | assert_eq!(format!("{:x>7}", Weekday::Mon), "xxxxMon"); 347 | assert_eq!(format!("{:^7}", Weekday::Mon), " Mon "); 348 | assert_eq!(format!("{:Z<7}", Weekday::Mon), "MonZZZZ"); 349 | } 350 | 351 | #[test] 352 | #[cfg(feature = "serde")] 353 | fn test_serde_serialize() { 354 | use Weekday::*; 355 | use serde_json::to_string; 356 | 357 | let cases: Vec<(Weekday, &str)> = vec![ 358 | (Mon, "\"Mon\""), 359 | (Tue, "\"Tue\""), 360 | (Wed, "\"Wed\""), 361 | (Thu, "\"Thu\""), 362 | (Fri, "\"Fri\""), 363 | (Sat, "\"Sat\""), 364 | (Sun, "\"Sun\""), 365 | ]; 366 | 367 | for (weekday, expected_str) in cases { 368 | let string = to_string(&weekday).unwrap(); 369 | assert_eq!(string, expected_str); 370 | } 371 | } 372 | 373 | #[test] 374 | #[cfg(feature = "serde")] 375 | fn test_serde_deserialize() { 376 | use Weekday::*; 377 | use serde_json::from_str; 378 | 379 | let cases: Vec<(&str, Weekday)> = vec![ 380 | ("\"mon\"", Mon), 381 | ("\"MONDAY\"", Mon), 382 | ("\"MonDay\"", Mon), 383 | ("\"mOn\"", Mon), 384 | ("\"tue\"", Tue), 385 | ("\"tuesday\"", Tue), 386 | ("\"wed\"", Wed), 387 | ("\"wednesday\"", Wed), 388 | ("\"thu\"", Thu), 389 | ("\"thursday\"", Thu), 390 | ("\"fri\"", Fri), 391 | ("\"friday\"", Fri), 392 | ("\"sat\"", Sat), 393 | ("\"saturday\"", Sat), 394 | ("\"sun\"", Sun), 395 | ("\"sunday\"", Sun), 396 | ]; 397 | 398 | for (str, expected_weekday) in cases { 399 | let weekday = from_str::(str).unwrap(); 400 | assert_eq!(weekday, expected_weekday); 401 | } 402 | 403 | let errors: Vec<&str> = 404 | vec!["\"not a weekday\"", "\"monDAYs\"", "\"mond\"", "mon", "\"thur\"", "\"thurs\""]; 405 | 406 | for str in errors { 407 | from_str::(str).unwrap_err(); 408 | } 409 | } 410 | 411 | #[test] 412 | #[cfg(feature = "rkyv-validation")] 413 | fn test_rkyv_validation() { 414 | let mon = Weekday::Mon; 415 | let bytes = rkyv::to_bytes::<_, 1>(&mon).unwrap(); 416 | 417 | assert_eq!(rkyv::from_bytes::(&bytes).unwrap(), mon); 418 | } 419 | } 420 | -------------------------------------------------------------------------------- /src/format/scan.rs: -------------------------------------------------------------------------------- 1 | // This is a part of Chrono. 2 | // See README.md and LICENSE.txt for details. 3 | 4 | /*! 5 | * Various scanning routines for the parser. 6 | */ 7 | 8 | use super::{INVALID, OUT_OF_RANGE, ParseResult, TOO_SHORT}; 9 | use crate::Weekday; 10 | 11 | /// Tries to parse the non-negative number from `min` to `max` digits. 12 | /// 13 | /// The absence of digits at all is an unconditional error. 14 | /// More than `max` digits are consumed up to the first `max` digits. 15 | /// Any number that does not fit in `i64` is an error. 16 | #[inline] 17 | pub(super) fn number(s: &str, min: usize, max: usize) -> ParseResult<(&str, i64)> { 18 | assert!(min <= max); 19 | 20 | // We are only interested in ascii numbers, so we can work with the `str` as bytes. We stop on 21 | // the first non-numeric byte, which may be another ascii character or beginning of multi-byte 22 | // UTF-8 character. 23 | let bytes = s.as_bytes(); 24 | if bytes.len() < min { 25 | return Err(TOO_SHORT); 26 | } 27 | 28 | let mut n = 0i64; 29 | for (i, c) in bytes.iter().take(max).cloned().enumerate() { 30 | // cloned() = copied() 31 | if !c.is_ascii_digit() { 32 | if i < min { 33 | return Err(INVALID); 34 | } else { 35 | return Ok((&s[i..], n)); 36 | } 37 | } 38 | 39 | n = match n.checked_mul(10).and_then(|n| n.checked_add((c - b'0') as i64)) { 40 | Some(n) => n, 41 | None => return Err(OUT_OF_RANGE), 42 | }; 43 | } 44 | 45 | Ok((&s[core::cmp::min(max, bytes.len())..], n)) 46 | } 47 | 48 | /// Tries to consume at least one digits as a fractional second. 49 | /// Returns the number of whole nanoseconds (0--999,999,999). 50 | pub(super) fn nanosecond(s: &str) -> ParseResult<(&str, u32)> { 51 | // record the number of digits consumed for later scaling. 52 | let origlen = s.len(); 53 | let (s, v) = number(s, 1, 9)?; 54 | let v = u32::try_from(v).expect("999,999,999 should fit u32"); 55 | let consumed = origlen - s.len(); 56 | 57 | // scale the number accordingly. 58 | const SCALE: [u32; 10] = 59 | [0, 100_000_000, 10_000_000, 1_000_000, 100_000, 10_000, 1_000, 100, 10, 1]; 60 | let v = v.checked_mul(SCALE[consumed]).ok_or(OUT_OF_RANGE)?; 61 | 62 | // if there are more than 9 digits, skip next digits. 63 | let s = s.trim_start_matches(|c: char| c.is_ascii_digit()); 64 | 65 | Ok((s, v)) 66 | } 67 | 68 | /// Tries to consume a fixed number of digits as a fractional second. 69 | /// Returns the number of whole nanoseconds (0--999,999,999). 70 | pub(super) fn nanosecond_fixed(s: &str, digits: usize) -> ParseResult<(&str, i64)> { 71 | // record the number of digits consumed for later scaling. 72 | let (s, v) = number(s, digits, digits)?; 73 | 74 | // scale the number accordingly. 75 | static SCALE: [i64; 10] = 76 | [0, 100_000_000, 10_000_000, 1_000_000, 100_000, 10_000, 1_000, 100, 10, 1]; 77 | let v = v.checked_mul(SCALE[digits]).ok_or(OUT_OF_RANGE)?; 78 | 79 | Ok((s, v)) 80 | } 81 | 82 | /// Tries to parse the month index (0 through 11) with the first three ASCII letters. 83 | pub(super) fn short_month0(s: &str) -> ParseResult<(&str, u8)> { 84 | if s.len() < 3 { 85 | return Err(TOO_SHORT); 86 | } 87 | let buf = s.as_bytes(); 88 | let month0 = match (buf[0] | 32, buf[1] | 32, buf[2] | 32) { 89 | (b'j', b'a', b'n') => 0, 90 | (b'f', b'e', b'b') => 1, 91 | (b'm', b'a', b'r') => 2, 92 | (b'a', b'p', b'r') => 3, 93 | (b'm', b'a', b'y') => 4, 94 | (b'j', b'u', b'n') => 5, 95 | (b'j', b'u', b'l') => 6, 96 | (b'a', b'u', b'g') => 7, 97 | (b's', b'e', b'p') => 8, 98 | (b'o', b'c', b't') => 9, 99 | (b'n', b'o', b'v') => 10, 100 | (b'd', b'e', b'c') => 11, 101 | _ => return Err(INVALID), 102 | }; 103 | Ok((&s[3..], month0)) 104 | } 105 | 106 | /// Tries to parse the weekday with the first three ASCII letters. 107 | pub(super) fn short_weekday(s: &str) -> ParseResult<(&str, Weekday)> { 108 | if s.len() < 3 { 109 | return Err(TOO_SHORT); 110 | } 111 | let buf = s.as_bytes(); 112 | let weekday = match (buf[0] | 32, buf[1] | 32, buf[2] | 32) { 113 | (b'm', b'o', b'n') => Weekday::Mon, 114 | (b't', b'u', b'e') => Weekday::Tue, 115 | (b'w', b'e', b'd') => Weekday::Wed, 116 | (b't', b'h', b'u') => Weekday::Thu, 117 | (b'f', b'r', b'i') => Weekday::Fri, 118 | (b's', b'a', b't') => Weekday::Sat, 119 | (b's', b'u', b'n') => Weekday::Sun, 120 | _ => return Err(INVALID), 121 | }; 122 | Ok((&s[3..], weekday)) 123 | } 124 | 125 | /// Tries to parse the month index (0 through 11) with short or long month names. 126 | /// It prefers long month names to short month names when both are possible. 127 | pub(super) fn short_or_long_month0(s: &str) -> ParseResult<(&str, u8)> { 128 | // lowercased month names, minus first three chars 129 | static LONG_MONTH_SUFFIXES: [&[u8]; 12] = [ 130 | b"uary", b"ruary", b"ch", b"il", b"", b"e", b"y", b"ust", b"tember", b"ober", b"ember", 131 | b"ember", 132 | ]; 133 | 134 | let (mut s, month0) = short_month0(s)?; 135 | 136 | // tries to consume the suffix if possible 137 | let suffix = LONG_MONTH_SUFFIXES[month0 as usize]; 138 | if s.len() >= suffix.len() && s.as_bytes()[..suffix.len()].eq_ignore_ascii_case(suffix) { 139 | s = &s[suffix.len()..]; 140 | } 141 | 142 | Ok((s, month0)) 143 | } 144 | 145 | /// Tries to parse the weekday with short or long weekday names. 146 | /// It prefers long weekday names to short weekday names when both are possible. 147 | pub(super) fn short_or_long_weekday(s: &str) -> ParseResult<(&str, Weekday)> { 148 | // lowercased weekday names, minus first three chars 149 | static LONG_WEEKDAY_SUFFIXES: [&[u8]; 7] = 150 | [b"day", b"sday", b"nesday", b"rsday", b"day", b"urday", b"day"]; 151 | 152 | let (mut s, weekday) = short_weekday(s)?; 153 | 154 | // tries to consume the suffix if possible 155 | let suffix = LONG_WEEKDAY_SUFFIXES[weekday.num_days_from_monday() as usize]; 156 | if s.len() >= suffix.len() && s.as_bytes()[..suffix.len()].eq_ignore_ascii_case(suffix) { 157 | s = &s[suffix.len()..]; 158 | } 159 | 160 | Ok((s, weekday)) 161 | } 162 | 163 | /// Tries to consume exactly one given character. 164 | pub(super) fn char(s: &str, c1: u8) -> ParseResult<&str> { 165 | match s.as_bytes().first() { 166 | Some(&c) if c == c1 => Ok(&s[1..]), 167 | Some(_) => Err(INVALID), 168 | None => Err(TOO_SHORT), 169 | } 170 | } 171 | 172 | /// Tries to consume one or more whitespace. 173 | pub(super) fn space(s: &str) -> ParseResult<&str> { 174 | let s_ = s.trim_start(); 175 | if s_.len() < s.len() { 176 | Ok(s_) 177 | } else if s.is_empty() { 178 | Err(TOO_SHORT) 179 | } else { 180 | Err(INVALID) 181 | } 182 | } 183 | 184 | /// Consumes any number (including zero) of colon or spaces. 185 | pub(crate) fn colon_or_space(s: &str) -> ParseResult<&str> { 186 | Ok(s.trim_start_matches(|c: char| c == ':' || c.is_whitespace())) 187 | } 188 | 189 | /// Parse a timezone from `s` and return the offset in seconds. 190 | /// 191 | /// The `consume_colon` function is used to parse a mandatory or optional `:` 192 | /// separator between hours offset and minutes offset. 193 | /// 194 | /// The `allow_missing_minutes` flag allows the timezone minutes offset to be 195 | /// missing from `s`. 196 | /// 197 | /// The `allow_tz_minus_sign` flag allows the timezone offset negative character 198 | /// to also be `−` MINUS SIGN (U+2212) in addition to the typical 199 | /// ASCII-compatible `-` HYPHEN-MINUS (U+2D). 200 | /// This is part of [RFC 3339 & ISO 8601]. 201 | /// 202 | /// [RFC 3339 & ISO 8601]: https://en.wikipedia.org/w/index.php?title=ISO_8601&oldid=1114309368#Time_offsets_from_UTC 203 | pub(crate) fn timezone_offset( 204 | mut s: &str, 205 | mut consume_colon: F, 206 | allow_zulu: bool, 207 | allow_missing_minutes: bool, 208 | allow_tz_minus_sign: bool, 209 | ) -> ParseResult<(&str, i32)> 210 | where 211 | F: FnMut(&str) -> ParseResult<&str>, 212 | { 213 | if allow_zulu { 214 | if let Some(&b'Z' | &b'z') = s.as_bytes().first() { 215 | return Ok((&s[1..], 0)); 216 | } 217 | } 218 | 219 | const fn digits(s: &str) -> ParseResult<(u8, u8)> { 220 | let b = s.as_bytes(); 221 | if b.len() < 2 { Err(TOO_SHORT) } else { Ok((b[0], b[1])) } 222 | } 223 | let negative = match s.chars().next() { 224 | Some('+') => { 225 | // PLUS SIGN (U+2B) 226 | s = &s['+'.len_utf8()..]; 227 | 228 | false 229 | } 230 | Some('-') => { 231 | // HYPHEN-MINUS (U+2D) 232 | s = &s['-'.len_utf8()..]; 233 | 234 | true 235 | } 236 | Some('−') => { 237 | // MINUS SIGN (U+2212) 238 | if !allow_tz_minus_sign { 239 | return Err(INVALID); 240 | } 241 | s = &s['−'.len_utf8()..]; 242 | 243 | true 244 | } 245 | Some(_) => return Err(INVALID), 246 | None => return Err(TOO_SHORT), 247 | }; 248 | 249 | // hours (00--99) 250 | let hours = match digits(s)? { 251 | (h1 @ b'0'..=b'9', h2 @ b'0'..=b'9') => i32::from((h1 - b'0') * 10 + (h2 - b'0')), 252 | _ => return Err(INVALID), 253 | }; 254 | s = &s[2..]; 255 | 256 | // colons (and possibly other separators) 257 | s = consume_colon(s)?; 258 | 259 | // minutes (00--59) 260 | // if the next two items are digits then we have to add minutes 261 | let minutes = if let Ok(ds) = digits(s) { 262 | match ds { 263 | (m1 @ b'0'..=b'5', m2 @ b'0'..=b'9') => i32::from((m1 - b'0') * 10 + (m2 - b'0')), 264 | (b'6'..=b'9', b'0'..=b'9') => return Err(OUT_OF_RANGE), 265 | _ => return Err(INVALID), 266 | } 267 | } else if allow_missing_minutes { 268 | 0 269 | } else { 270 | return Err(TOO_SHORT); 271 | }; 272 | s = match s.len() { 273 | len if len >= 2 => &s[2..], 274 | 0 => s, 275 | _ => return Err(TOO_SHORT), 276 | }; 277 | 278 | let seconds = hours * 3600 + minutes * 60; 279 | Ok((s, if negative { -seconds } else { seconds })) 280 | } 281 | 282 | /// Same as `timezone_offset` but also allows for RFC 2822 legacy timezones. 283 | /// May return `None` which indicates an insufficient offset data (i.e. `-0000`). 284 | /// See [RFC 2822 Section 4.3]. 285 | /// 286 | /// [RFC 2822 Section 4.3]: https://tools.ietf.org/html/rfc2822#section-4.3 287 | pub(super) fn timezone_offset_2822(s: &str) -> ParseResult<(&str, i32)> { 288 | // tries to parse legacy time zone names 289 | let upto = s.as_bytes().iter().position(|&c| !c.is_ascii_alphabetic()).unwrap_or(s.len()); 290 | if upto > 0 { 291 | let name = &s.as_bytes()[..upto]; 292 | let s = &s[upto..]; 293 | let offset_hours = |o| Ok((s, o * 3600)); 294 | // RFC 2822 requires support for some named North America timezones, a small subset of all 295 | // named timezones. 296 | if name.eq_ignore_ascii_case(b"gmt") 297 | || name.eq_ignore_ascii_case(b"ut") 298 | || name.eq_ignore_ascii_case(b"z") 299 | { 300 | return offset_hours(0); 301 | } else if name.eq_ignore_ascii_case(b"edt") { 302 | return offset_hours(-4); 303 | } else if name.eq_ignore_ascii_case(b"est") || name.eq_ignore_ascii_case(b"cdt") { 304 | return offset_hours(-5); 305 | } else if name.eq_ignore_ascii_case(b"cst") || name.eq_ignore_ascii_case(b"mdt") { 306 | return offset_hours(-6); 307 | } else if name.eq_ignore_ascii_case(b"mst") || name.eq_ignore_ascii_case(b"pdt") { 308 | return offset_hours(-7); 309 | } else if name.eq_ignore_ascii_case(b"pst") { 310 | return offset_hours(-8); 311 | } else if name.len() == 1 { 312 | if let b'a'..=b'i' | b'k'..=b'y' | b'A'..=b'I' | b'K'..=b'Y' = name[0] { 313 | // recommended by RFC 2822: consume but treat it as -0000 314 | return Ok((s, 0)); 315 | } 316 | } 317 | Err(INVALID) 318 | } else { 319 | timezone_offset(s, |s| Ok(s), false, false, false) 320 | } 321 | } 322 | 323 | /// Tries to consume an RFC2822 comment including preceding ` `. 324 | /// 325 | /// Returns the remaining string after the closing parenthesis. 326 | pub(super) fn comment_2822(s: &str) -> ParseResult<(&str, ())> { 327 | use CommentState::*; 328 | 329 | let s = s.trim_start(); 330 | 331 | let mut state = Start; 332 | for (i, c) in s.bytes().enumerate() { 333 | state = match (state, c) { 334 | (Start, b'(') => Next(1), 335 | (Next(1), b')') => return Ok((&s[i + 1..], ())), 336 | (Next(depth), b'\\') => Escape(depth), 337 | (Next(depth), b'(') => Next(depth + 1), 338 | (Next(depth), b')') => Next(depth - 1), 339 | (Next(depth), _) | (Escape(depth), _) => Next(depth), 340 | _ => return Err(INVALID), 341 | }; 342 | } 343 | 344 | Err(TOO_SHORT) 345 | } 346 | 347 | enum CommentState { 348 | Start, 349 | Next(usize), 350 | Escape(usize), 351 | } 352 | 353 | #[cfg(test)] 354 | mod tests { 355 | use super::{ 356 | comment_2822, nanosecond, nanosecond_fixed, short_or_long_month0, short_or_long_weekday, 357 | timezone_offset_2822, 358 | }; 359 | use crate::Weekday; 360 | use crate::format::{INVALID, TOO_SHORT}; 361 | 362 | #[test] 363 | fn test_rfc2822_comments() { 364 | let testdata = [ 365 | ("", Err(TOO_SHORT)), 366 | (" ", Err(TOO_SHORT)), 367 | ("x", Err(INVALID)), 368 | ("(", Err(TOO_SHORT)), 369 | ("()", Ok("")), 370 | (" \r\n\t()", Ok("")), 371 | ("() ", Ok(" ")), 372 | ("()z", Ok("z")), 373 | ("(x)", Ok("")), 374 | ("(())", Ok("")), 375 | ("((()))", Ok("")), 376 | ("(x(x(x)x)x)", Ok("")), 377 | ("( x ( x ( x ) x ) x )", Ok("")), 378 | (r"(\)", Err(TOO_SHORT)), 379 | (r"(\()", Ok("")), 380 | (r"(\))", Ok("")), 381 | (r"(\\)", Ok("")), 382 | ("(()())", Ok("")), 383 | ("( x ( x ) x ( x ) x )", Ok("")), 384 | ]; 385 | 386 | for (test_in, expected) in testdata.iter() { 387 | let actual = comment_2822(test_in).map(|(s, _)| s); 388 | assert_eq!( 389 | *expected, actual, 390 | "{test_in:?} expected to produce {expected:?}, but produced {actual:?}." 391 | ); 392 | } 393 | } 394 | 395 | #[test] 396 | fn test_timezone_offset_2822() { 397 | assert_eq!(timezone_offset_2822("cSt").unwrap(), ("", -21600)); 398 | assert_eq!(timezone_offset_2822("pSt").unwrap(), ("", -28800)); 399 | assert_eq!(timezone_offset_2822("mSt").unwrap(), ("", -25200)); 400 | assert_eq!(timezone_offset_2822("-1551").unwrap(), ("", -57060)); 401 | assert_eq!(timezone_offset_2822("Gp"), Err(INVALID)); 402 | } 403 | 404 | #[test] 405 | fn test_short_or_long_month0() { 406 | assert_eq!(short_or_long_month0("JUn").unwrap(), ("", 5)); 407 | assert_eq!(short_or_long_month0("mAy").unwrap(), ("", 4)); 408 | assert_eq!(short_or_long_month0("AuG").unwrap(), ("", 7)); 409 | assert_eq!(short_or_long_month0("Aprâ").unwrap(), ("â", 3)); 410 | assert_eq!(short_or_long_month0("JUl").unwrap(), ("", 6)); 411 | assert_eq!(short_or_long_month0("mAr").unwrap(), ("", 2)); 412 | assert_eq!(short_or_long_month0("Jan").unwrap(), ("", 0)); 413 | } 414 | 415 | #[test] 416 | fn test_short_or_long_weekday() { 417 | assert_eq!(short_or_long_weekday("sAtu").unwrap(), ("u", Weekday::Sat)); 418 | assert_eq!(short_or_long_weekday("thu").unwrap(), ("", Weekday::Thu)); 419 | } 420 | 421 | #[test] 422 | fn test_nanosecond_fixed() { 423 | assert_eq!(nanosecond_fixed("", 0usize).unwrap(), ("", 0)); 424 | assert!(nanosecond_fixed("", 1usize).is_err()); 425 | } 426 | 427 | #[test] 428 | fn test_nanosecond() { 429 | assert_eq!(nanosecond("2Ù").unwrap(), ("Ù", 200000000)); 430 | assert_eq!(nanosecond("8").unwrap(), ("", 800000000)); 431 | } 432 | } 433 | -------------------------------------------------------------------------------- /src/naive/time/tests.rs: -------------------------------------------------------------------------------- 1 | use super::NaiveTime; 2 | use crate::{FixedOffset, TimeDelta, Timelike}; 3 | 4 | #[test] 5 | fn test_time_from_hms_milli() { 6 | assert_eq!( 7 | NaiveTime::from_hms_milli_opt(3, 5, 7, 0), 8 | Some(NaiveTime::from_hms_nano_opt(3, 5, 7, 0).unwrap()) 9 | ); 10 | assert_eq!( 11 | NaiveTime::from_hms_milli_opt(3, 5, 7, 777), 12 | Some(NaiveTime::from_hms_nano_opt(3, 5, 7, 777_000_000).unwrap()) 13 | ); 14 | assert_eq!( 15 | NaiveTime::from_hms_milli_opt(3, 5, 59, 1_999), 16 | Some(NaiveTime::from_hms_nano_opt(3, 5, 59, 1_999_000_000).unwrap()) 17 | ); 18 | assert_eq!(NaiveTime::from_hms_milli_opt(3, 5, 59, 2_000), None); 19 | assert_eq!(NaiveTime::from_hms_milli_opt(3, 5, 59, 5_000), None); // overflow check 20 | assert_eq!(NaiveTime::from_hms_milli_opt(3, 5, 59, u32::MAX), None); 21 | } 22 | 23 | #[test] 24 | fn test_time_from_hms_micro() { 25 | assert_eq!( 26 | NaiveTime::from_hms_micro_opt(3, 5, 7, 0), 27 | Some(NaiveTime::from_hms_nano_opt(3, 5, 7, 0).unwrap()) 28 | ); 29 | assert_eq!( 30 | NaiveTime::from_hms_micro_opt(3, 5, 7, 333), 31 | Some(NaiveTime::from_hms_nano_opt(3, 5, 7, 333_000).unwrap()) 32 | ); 33 | assert_eq!( 34 | NaiveTime::from_hms_micro_opt(3, 5, 7, 777_777), 35 | Some(NaiveTime::from_hms_nano_opt(3, 5, 7, 777_777_000).unwrap()) 36 | ); 37 | assert_eq!( 38 | NaiveTime::from_hms_micro_opt(3, 5, 59, 1_999_999), 39 | Some(NaiveTime::from_hms_nano_opt(3, 5, 59, 1_999_999_000).unwrap()) 40 | ); 41 | assert_eq!(NaiveTime::from_hms_micro_opt(3, 5, 59, 2_000_000), None); 42 | assert_eq!(NaiveTime::from_hms_micro_opt(3, 5, 59, 5_000_000), None); // overflow check 43 | assert_eq!(NaiveTime::from_hms_micro_opt(3, 5, 59, u32::MAX), None); 44 | } 45 | 46 | #[test] 47 | fn test_time_hms() { 48 | assert_eq!(NaiveTime::from_hms_opt(3, 5, 7).unwrap().hour(), 3); 49 | assert_eq!( 50 | NaiveTime::from_hms_opt(3, 5, 7).unwrap().with_hour(0), 51 | Some(NaiveTime::from_hms_opt(0, 5, 7).unwrap()) 52 | ); 53 | assert_eq!( 54 | NaiveTime::from_hms_opt(3, 5, 7).unwrap().with_hour(23), 55 | Some(NaiveTime::from_hms_opt(23, 5, 7).unwrap()) 56 | ); 57 | assert_eq!(NaiveTime::from_hms_opt(3, 5, 7).unwrap().with_hour(24), None); 58 | assert_eq!(NaiveTime::from_hms_opt(3, 5, 7).unwrap().with_hour(u32::MAX), None); 59 | 60 | assert_eq!(NaiveTime::from_hms_opt(3, 5, 7).unwrap().minute(), 5); 61 | assert_eq!( 62 | NaiveTime::from_hms_opt(3, 5, 7).unwrap().with_minute(0), 63 | Some(NaiveTime::from_hms_opt(3, 0, 7).unwrap()) 64 | ); 65 | assert_eq!( 66 | NaiveTime::from_hms_opt(3, 5, 7).unwrap().with_minute(59), 67 | Some(NaiveTime::from_hms_opt(3, 59, 7).unwrap()) 68 | ); 69 | assert_eq!(NaiveTime::from_hms_opt(3, 5, 7).unwrap().with_minute(60), None); 70 | assert_eq!(NaiveTime::from_hms_opt(3, 5, 7).unwrap().with_minute(u32::MAX), None); 71 | 72 | assert_eq!(NaiveTime::from_hms_opt(3, 5, 7).unwrap().second(), 7); 73 | assert_eq!( 74 | NaiveTime::from_hms_opt(3, 5, 7).unwrap().with_second(0), 75 | Some(NaiveTime::from_hms_opt(3, 5, 0).unwrap()) 76 | ); 77 | assert_eq!( 78 | NaiveTime::from_hms_opt(3, 5, 7).unwrap().with_second(59), 79 | Some(NaiveTime::from_hms_opt(3, 5, 59).unwrap()) 80 | ); 81 | assert_eq!(NaiveTime::from_hms_opt(3, 5, 7).unwrap().with_second(60), None); 82 | assert_eq!(NaiveTime::from_hms_opt(3, 5, 7).unwrap().with_second(u32::MAX), None); 83 | } 84 | 85 | #[test] 86 | fn test_time_add() { 87 | macro_rules! check { 88 | ($lhs:expr, $rhs:expr, $sum:expr) => {{ 89 | assert_eq!($lhs + $rhs, $sum); 90 | //assert_eq!($rhs + $lhs, $sum); 91 | }}; 92 | } 93 | 94 | let hmsm = |h, m, s, ms| NaiveTime::from_hms_milli_opt(h, m, s, ms).unwrap(); 95 | 96 | check!(hmsm(3, 5, 59, 900), TimeDelta::zero(), hmsm(3, 5, 59, 900)); 97 | check!(hmsm(3, 5, 59, 900), TimeDelta::try_milliseconds(100).unwrap(), hmsm(3, 6, 0, 0)); 98 | check!(hmsm(3, 5, 59, 1_300), TimeDelta::try_milliseconds(-1800).unwrap(), hmsm(3, 5, 58, 500)); 99 | check!(hmsm(3, 5, 59, 1_300), TimeDelta::try_milliseconds(-800).unwrap(), hmsm(3, 5, 59, 500)); 100 | check!( 101 | hmsm(3, 5, 59, 1_300), 102 | TimeDelta::try_milliseconds(-100).unwrap(), 103 | hmsm(3, 5, 59, 1_200) 104 | ); 105 | check!(hmsm(3, 5, 59, 1_300), TimeDelta::try_milliseconds(100).unwrap(), hmsm(3, 5, 59, 1_400)); 106 | check!(hmsm(3, 5, 59, 1_300), TimeDelta::try_milliseconds(800).unwrap(), hmsm(3, 6, 0, 100)); 107 | check!(hmsm(3, 5, 59, 1_300), TimeDelta::try_milliseconds(1800).unwrap(), hmsm(3, 6, 1, 100)); 108 | check!(hmsm(3, 5, 59, 900), TimeDelta::try_seconds(86399).unwrap(), hmsm(3, 5, 58, 900)); // overwrap 109 | check!(hmsm(3, 5, 59, 900), TimeDelta::try_seconds(-86399).unwrap(), hmsm(3, 6, 0, 900)); 110 | check!(hmsm(3, 5, 59, 900), TimeDelta::try_days(12345).unwrap(), hmsm(3, 5, 59, 900)); 111 | check!(hmsm(3, 5, 59, 1_300), TimeDelta::try_days(1).unwrap(), hmsm(3, 5, 59, 300)); 112 | check!(hmsm(3, 5, 59, 1_300), TimeDelta::try_days(-1).unwrap(), hmsm(3, 6, 0, 300)); 113 | 114 | // regression tests for #37 115 | check!(hmsm(0, 0, 0, 0), TimeDelta::try_milliseconds(-990).unwrap(), hmsm(23, 59, 59, 10)); 116 | check!(hmsm(0, 0, 0, 0), TimeDelta::try_milliseconds(-9990).unwrap(), hmsm(23, 59, 50, 10)); 117 | } 118 | 119 | #[test] 120 | fn test_time_overflowing_add() { 121 | let hmsm = |h, m, s, ms| NaiveTime::from_hms_milli_opt(h, m, s, ms).unwrap(); 122 | 123 | assert_eq!( 124 | hmsm(3, 4, 5, 678).overflowing_add_signed(TimeDelta::try_hours(11).unwrap()), 125 | (hmsm(14, 4, 5, 678), 0) 126 | ); 127 | assert_eq!( 128 | hmsm(3, 4, 5, 678).overflowing_add_signed(TimeDelta::try_hours(23).unwrap()), 129 | (hmsm(2, 4, 5, 678), 86_400) 130 | ); 131 | assert_eq!( 132 | hmsm(3, 4, 5, 678).overflowing_add_signed(TimeDelta::try_hours(-7).unwrap()), 133 | (hmsm(20, 4, 5, 678), -86_400) 134 | ); 135 | 136 | // overflowing_add_signed with leap seconds may be counter-intuitive 137 | assert_eq!( 138 | hmsm(3, 4, 59, 1_678).overflowing_add_signed(TimeDelta::try_days(1).unwrap()), 139 | (hmsm(3, 4, 59, 678), 86_400) 140 | ); 141 | assert_eq!( 142 | hmsm(3, 4, 59, 1_678).overflowing_add_signed(TimeDelta::try_days(-1).unwrap()), 143 | (hmsm(3, 5, 0, 678), -86_400) 144 | ); 145 | } 146 | 147 | #[test] 148 | fn test_time_addassignment() { 149 | let hms = |h, m, s| NaiveTime::from_hms_opt(h, m, s).unwrap(); 150 | let mut time = hms(12, 12, 12); 151 | time += TimeDelta::try_hours(10).unwrap(); 152 | assert_eq!(time, hms(22, 12, 12)); 153 | time += TimeDelta::try_hours(10).unwrap(); 154 | assert_eq!(time, hms(8, 12, 12)); 155 | } 156 | 157 | #[test] 158 | fn test_time_subassignment() { 159 | let hms = |h, m, s| NaiveTime::from_hms_opt(h, m, s).unwrap(); 160 | let mut time = hms(12, 12, 12); 161 | time -= TimeDelta::try_hours(10).unwrap(); 162 | assert_eq!(time, hms(2, 12, 12)); 163 | time -= TimeDelta::try_hours(10).unwrap(); 164 | assert_eq!(time, hms(16, 12, 12)); 165 | } 166 | 167 | #[test] 168 | fn test_time_sub() { 169 | macro_rules! check { 170 | ($lhs:expr, $rhs:expr, $diff:expr) => {{ 171 | // `time1 - time2 = duration` is equivalent to `time2 - time1 = -duration` 172 | assert_eq!($lhs.signed_duration_since($rhs), $diff); 173 | assert_eq!($rhs.signed_duration_since($lhs), -$diff); 174 | }}; 175 | } 176 | 177 | let hmsm = |h, m, s, ms| NaiveTime::from_hms_milli_opt(h, m, s, ms).unwrap(); 178 | 179 | check!(hmsm(3, 5, 7, 900), hmsm(3, 5, 7, 900), TimeDelta::zero()); 180 | check!(hmsm(3, 5, 7, 900), hmsm(3, 5, 7, 600), TimeDelta::try_milliseconds(300).unwrap()); 181 | check!(hmsm(3, 5, 7, 200), hmsm(2, 4, 6, 200), TimeDelta::try_seconds(3600 + 60 + 1).unwrap()); 182 | check!( 183 | hmsm(3, 5, 7, 200), 184 | hmsm(2, 4, 6, 300), 185 | TimeDelta::try_seconds(3600 + 60).unwrap() + TimeDelta::try_milliseconds(900).unwrap() 186 | ); 187 | 188 | // treats the leap second as if it coincides with the prior non-leap second, 189 | // as required by `time1 - time2 = duration` and `time2 - time1 = -duration` equivalence. 190 | check!(hmsm(3, 6, 0, 200), hmsm(3, 5, 59, 1_800), TimeDelta::try_milliseconds(400).unwrap()); 191 | //check!(hmsm(3, 5, 7, 1_200), hmsm(3, 5, 6, 1_800), TimeDelta::try_milliseconds(1400).unwrap()); 192 | //check!(hmsm(3, 5, 7, 1_200), hmsm(3, 5, 6, 800), TimeDelta::try_milliseconds(1400).unwrap()); 193 | 194 | // additional equality: `time1 + duration = time2` is equivalent to 195 | // `time2 - time1 = duration` IF AND ONLY IF `time2` represents a non-leap second. 196 | assert_eq!(hmsm(3, 5, 6, 800) + TimeDelta::try_milliseconds(400).unwrap(), hmsm(3, 5, 7, 200)); 197 | //assert_eq!(hmsm(3, 5, 6, 1_800) + TimeDelta::try_milliseconds(400).unwrap(), hmsm(3, 5, 7, 200)); 198 | } 199 | 200 | #[test] 201 | fn test_core_duration_ops() { 202 | use core::time::Duration; 203 | 204 | let mut t = NaiveTime::from_hms_opt(11, 34, 23).unwrap(); 205 | let same = t + Duration::ZERO; 206 | assert_eq!(t, same); 207 | 208 | t += Duration::new(3600, 0); 209 | assert_eq!(t, NaiveTime::from_hms_opt(12, 34, 23).unwrap()); 210 | 211 | t -= Duration::new(7200, 0); 212 | assert_eq!(t, NaiveTime::from_hms_opt(10, 34, 23).unwrap()); 213 | } 214 | 215 | #[test] 216 | fn test_time_fmt() { 217 | assert_eq!( 218 | format!("{}", NaiveTime::from_hms_milli_opt(23, 59, 59, 999).unwrap()), 219 | "23:59:59.999" 220 | ); 221 | assert_eq!( 222 | format!("{}", NaiveTime::from_hms_milli_opt(23, 59, 59, 1_000).unwrap()), 223 | "23:59:60" 224 | ); 225 | assert_eq!( 226 | format!("{}", NaiveTime::from_hms_milli_opt(23, 59, 59, 1_001).unwrap()), 227 | "23:59:60.001" 228 | ); 229 | assert_eq!( 230 | format!("{}", NaiveTime::from_hms_micro_opt(0, 0, 0, 43210).unwrap()), 231 | "00:00:00.043210" 232 | ); 233 | assert_eq!( 234 | format!("{}", NaiveTime::from_hms_nano_opt(0, 0, 0, 6543210).unwrap()), 235 | "00:00:00.006543210" 236 | ); 237 | 238 | // the format specifier should have no effect on `NaiveTime` 239 | assert_eq!( 240 | format!("{:30}", NaiveTime::from_hms_milli_opt(3, 5, 7, 9).unwrap()), 241 | "03:05:07.009" 242 | ); 243 | } 244 | 245 | #[test] 246 | fn test_time_from_str() { 247 | // valid cases 248 | let valid = [ 249 | "0:0:0", 250 | "0:0:0.0000000", 251 | "0:0:0.0000003", 252 | " 4 : 3 : 2.1 ", 253 | " 09:08:07 ", 254 | " 09:08 ", 255 | " 9:8:07 ", 256 | "01:02:03", 257 | "4:3:2.1", 258 | "9:8:7", 259 | "09:8:7", 260 | "9:08:7", 261 | "9:8:07", 262 | "09:08:7", 263 | "09:8:07", 264 | "09:08:7", 265 | "9:08:07", 266 | "09:08:07", 267 | "9:8:07.123", 268 | "9:08:7.123", 269 | "09:8:7.123", 270 | "09:08:7.123", 271 | "9:08:07.123", 272 | "09:8:07.123", 273 | "09:08:07.123", 274 | "09:08:07.123", 275 | "09:08:07.1234", 276 | "09:08:07.12345", 277 | "09:08:07.123456", 278 | "09:08:07.1234567", 279 | "09:08:07.12345678", 280 | "09:08:07.123456789", 281 | "09:08:07.1234567891", 282 | "09:08:07.12345678912", 283 | "23:59:60.373929310237", 284 | ]; 285 | for &s in &valid { 286 | eprintln!("test_time_parse_from_str valid {s:?}"); 287 | let d = match s.parse::() { 288 | Ok(d) => d, 289 | Err(e) => panic!("parsing `{s}` has failed: {e}"), 290 | }; 291 | let s_ = format!("{d:?}"); 292 | // `s` and `s_` may differ, but `s.parse()` and `s_.parse()` must be same 293 | let d_ = match s_.parse::() { 294 | Ok(d) => d, 295 | Err(e) => { 296 | panic!("`{s}` is parsed into `{d:?}`, but reparsing that has failed: {e}") 297 | } 298 | }; 299 | assert!( 300 | d == d_, 301 | "`{s}` is parsed into `{d:?}`, but reparsed result \ 302 | `{d_:?}` does not match" 303 | ); 304 | } 305 | 306 | // some invalid cases 307 | // since `ParseErrorKind` is private, all we can do is to check if there was an error 308 | let invalid = [ 309 | "", // empty 310 | "x", // invalid 311 | "15", // missing data 312 | "15:8:", // trailing colon 313 | "15:8:x", // invalid data 314 | "15:8:9x", // invalid data 315 | "23:59:61", // invalid second (out of bounds) 316 | "23:54:35 GMT", // invalid (timezone non-sensical for NaiveTime) 317 | "23:54:35 +0000", // invalid (timezone non-sensical for NaiveTime) 318 | "1441497364.649", // valid datetime, not a NaiveTime 319 | "+1441497364.649", // valid datetime, not a NaiveTime 320 | "+1441497364", // valid datetime, not a NaiveTime 321 | "001:02:03", // invalid hour 322 | "01:002:03", // invalid minute 323 | "01:02:003", // invalid second 324 | "12:34:56.x", // invalid fraction 325 | "12:34:56. 0", // invalid fraction format 326 | "09:08:00000000007", // invalid second / invalid fraction format 327 | ]; 328 | for &s in &invalid { 329 | eprintln!("test_time_parse_from_str invalid {s:?}"); 330 | assert!(s.parse::().is_err()); 331 | } 332 | } 333 | 334 | #[test] 335 | fn test_time_parse_from_str() { 336 | let hms = |h, m, s| NaiveTime::from_hms_opt(h, m, s).unwrap(); 337 | assert_eq!( 338 | NaiveTime::parse_from_str("2014-5-7T12:34:56+09:30", "%Y-%m-%dT%H:%M:%S%z"), 339 | Ok(hms(12, 34, 56)) 340 | ); // ignore date and offset 341 | assert_eq!(NaiveTime::parse_from_str("PM 12:59", "%P %H:%M"), Ok(hms(12, 59, 0))); 342 | assert_eq!(NaiveTime::parse_from_str("12:59 \n\t PM", "%H:%M \n\t %P"), Ok(hms(12, 59, 0))); 343 | assert_eq!(NaiveTime::parse_from_str("\t\t12:59\tPM\t", "\t\t%H:%M\t%P\t"), Ok(hms(12, 59, 0))); 344 | assert_eq!( 345 | NaiveTime::parse_from_str("\t\t1259\t\tPM\t", "\t\t%H%M\t\t%P\t"), 346 | Ok(hms(12, 59, 0)) 347 | ); 348 | assert!(NaiveTime::parse_from_str("12:59 PM", "%H:%M\t%P").is_ok()); 349 | assert!(NaiveTime::parse_from_str("\t\t12:59 PM\t", "\t\t%H:%M\t%P\t").is_ok()); 350 | assert!(NaiveTime::parse_from_str("12:59 PM", "%H:%M %P").is_ok()); 351 | assert!(NaiveTime::parse_from_str("12:3456", "%H:%M:%S").is_err()); 352 | } 353 | 354 | #[test] 355 | fn test_overflowing_offset() { 356 | let hmsm = |h, m, s, n| NaiveTime::from_hms_milli_opt(h, m, s, n).unwrap(); 357 | 358 | let positive_offset = FixedOffset::east_opt(4 * 60 * 60).unwrap(); 359 | // regular time 360 | let t = hmsm(5, 6, 7, 890); 361 | assert_eq!(t.overflowing_add_offset(positive_offset), (hmsm(9, 6, 7, 890), 0)); 362 | assert_eq!(t.overflowing_sub_offset(positive_offset), (hmsm(1, 6, 7, 890), 0)); 363 | // leap second is preserved, and wrap to next day 364 | let t = hmsm(23, 59, 59, 1_000); 365 | assert_eq!(t.overflowing_add_offset(positive_offset), (hmsm(3, 59, 59, 1_000), 1)); 366 | assert_eq!(t.overflowing_sub_offset(positive_offset), (hmsm(19, 59, 59, 1_000), 0)); 367 | // wrap to previous day 368 | let t = hmsm(1, 2, 3, 456); 369 | assert_eq!(t.overflowing_sub_offset(positive_offset), (hmsm(21, 2, 3, 456), -1)); 370 | // an odd offset 371 | let negative_offset = FixedOffset::west_opt(((2 * 60) + 3) * 60 + 4).unwrap(); 372 | let t = hmsm(5, 6, 7, 890); 373 | assert_eq!(t.overflowing_add_offset(negative_offset), (hmsm(3, 3, 3, 890), 0)); 374 | assert_eq!(t.overflowing_sub_offset(negative_offset), (hmsm(7, 9, 11, 890), 0)); 375 | 376 | assert_eq!(t.overflowing_add_offset(positive_offset).0, t + positive_offset); 377 | assert_eq!(t.overflowing_sub_offset(positive_offset).0, t - positive_offset); 378 | } 379 | 380 | #[test] 381 | #[cfg(feature = "rkyv-validation")] 382 | fn test_rkyv_validation() { 383 | let t_min = NaiveTime::MIN; 384 | let bytes = rkyv::to_bytes::<_, 8>(&t_min).unwrap(); 385 | assert_eq!(rkyv::from_bytes::(&bytes).unwrap(), t_min); 386 | 387 | let t_max = NaiveTime::MAX; 388 | let bytes = rkyv::to_bytes::<_, 8>(&t_max).unwrap(); 389 | assert_eq!(rkyv::from_bytes::(&bytes).unwrap(), t_max); 390 | } 391 | -------------------------------------------------------------------------------- /src/month.rs: -------------------------------------------------------------------------------- 1 | use core::fmt; 2 | 3 | #[cfg(any(feature = "rkyv", feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"))] 4 | use rkyv::{Archive, Deserialize, Serialize}; 5 | 6 | use crate::OutOfRange; 7 | use crate::naive::NaiveDate; 8 | 9 | /// The month of the year. 10 | /// 11 | /// This enum is just a convenience implementation. 12 | /// The month in dates created by DateLike objects does not return this enum. 13 | /// 14 | /// It is possible to convert from a date to a month independently 15 | /// ``` 16 | /// use chrono::prelude::*; 17 | /// let date = Utc.with_ymd_and_hms(2019, 10, 28, 9, 10, 11).unwrap(); 18 | /// // `2019-10-28T09:10:11Z` 19 | /// let month = Month::try_from(u8::try_from(date.month()).unwrap()).ok(); 20 | /// assert_eq!(month, Some(Month::October)) 21 | /// ``` 22 | /// Or from a Month to an integer usable by dates 23 | /// ``` 24 | /// # use chrono::prelude::*; 25 | /// let month = Month::January; 26 | /// let dt = Utc.with_ymd_and_hms(2019, month.number_from_month(), 28, 9, 10, 11).unwrap(); 27 | /// assert_eq!((dt.year(), dt.month(), dt.day()), (2019, 1, 28)); 28 | /// ``` 29 | /// Allows mapping from and to month, from 1-January to 12-December. 30 | /// Can be Serialized/Deserialized with serde 31 | // Actual implementation is zero-indexed, API intended as 1-indexed for more intuitive behavior. 32 | #[derive(PartialEq, Eq, Copy, Clone, Debug, Hash, PartialOrd, Ord)] 33 | #[cfg_attr( 34 | any(feature = "rkyv", feature = "rkyv-16", feature = "rkyv-32", feature = "rkyv-64"), 35 | derive(Archive, Deserialize, Serialize), 36 | archive(compare(PartialEq, PartialOrd)), 37 | archive_attr(derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)) 38 | )] 39 | #[cfg_attr(feature = "rkyv-validation", archive(check_bytes))] 40 | #[cfg_attr(all(feature = "arbitrary", feature = "std"), derive(arbitrary::Arbitrary))] 41 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 42 | pub enum Month { 43 | /// January 44 | January = 0, 45 | /// February 46 | February = 1, 47 | /// March 48 | March = 2, 49 | /// April 50 | April = 3, 51 | /// May 52 | May = 4, 53 | /// June 54 | June = 5, 55 | /// July 56 | July = 6, 57 | /// August 58 | August = 7, 59 | /// September 60 | September = 8, 61 | /// October 62 | October = 9, 63 | /// November 64 | November = 10, 65 | /// December 66 | December = 11, 67 | } 68 | 69 | impl Month { 70 | /// The next month. 71 | /// 72 | /// `m`: | `January` | `February` | `...` | `December` 73 | /// ----------- | --------- | ---------- | --- | --------- 74 | /// `m.succ()`: | `February` | `March` | `...` | `January` 75 | #[inline] 76 | #[must_use] 77 | pub const fn succ(&self) -> Month { 78 | match *self { 79 | Month::January => Month::February, 80 | Month::February => Month::March, 81 | Month::March => Month::April, 82 | Month::April => Month::May, 83 | Month::May => Month::June, 84 | Month::June => Month::July, 85 | Month::July => Month::August, 86 | Month::August => Month::September, 87 | Month::September => Month::October, 88 | Month::October => Month::November, 89 | Month::November => Month::December, 90 | Month::December => Month::January, 91 | } 92 | } 93 | 94 | /// The previous month. 95 | /// 96 | /// `m`: | `January` | `February` | `...` | `December` 97 | /// ----------- | --------- | ---------- | --- | --------- 98 | /// `m.pred()`: | `December` | `January` | `...` | `November` 99 | #[inline] 100 | #[must_use] 101 | pub const fn pred(&self) -> Month { 102 | match *self { 103 | Month::January => Month::December, 104 | Month::February => Month::January, 105 | Month::March => Month::February, 106 | Month::April => Month::March, 107 | Month::May => Month::April, 108 | Month::June => Month::May, 109 | Month::July => Month::June, 110 | Month::August => Month::July, 111 | Month::September => Month::August, 112 | Month::October => Month::September, 113 | Month::November => Month::October, 114 | Month::December => Month::November, 115 | } 116 | } 117 | 118 | /// Returns a month-of-year number starting from January = 1. 119 | /// 120 | /// `m`: | `January` | `February` | `...` | `December` 121 | /// -------------------------| --------- | ---------- | --- | ----- 122 | /// `m.number_from_month()`: | 1 | 2 | `...` | 12 123 | #[inline] 124 | #[must_use] 125 | pub const fn number_from_month(&self) -> u32 { 126 | match *self { 127 | Month::January => 1, 128 | Month::February => 2, 129 | Month::March => 3, 130 | Month::April => 4, 131 | Month::May => 5, 132 | Month::June => 6, 133 | Month::July => 7, 134 | Month::August => 8, 135 | Month::September => 9, 136 | Month::October => 10, 137 | Month::November => 11, 138 | Month::December => 12, 139 | } 140 | } 141 | 142 | /// Get the name of the month 143 | /// 144 | /// ``` 145 | /// use chrono::Month; 146 | /// 147 | /// assert_eq!(Month::January.name(), "January") 148 | /// ``` 149 | #[must_use] 150 | pub const fn name(&self) -> &'static str { 151 | match *self { 152 | Month::January => "January", 153 | Month::February => "February", 154 | Month::March => "March", 155 | Month::April => "April", 156 | Month::May => "May", 157 | Month::June => "June", 158 | Month::July => "July", 159 | Month::August => "August", 160 | Month::September => "September", 161 | Month::October => "October", 162 | Month::November => "November", 163 | Month::December => "December", 164 | } 165 | } 166 | 167 | /// Get the length in days of the month 168 | /// 169 | /// Yields `None` if `year` is out of range for `NaiveDate`. 170 | pub fn num_days(&self, year: i32) -> Option { 171 | Some(match *self { 172 | Month::January => 31, 173 | Month::February => match NaiveDate::from_ymd_opt(year, 2, 1)?.leap_year() { 174 | true => 29, 175 | false => 28, 176 | }, 177 | Month::March => 31, 178 | Month::April => 30, 179 | Month::May => 31, 180 | Month::June => 30, 181 | Month::July => 31, 182 | Month::August => 31, 183 | Month::September => 30, 184 | Month::October => 31, 185 | Month::November => 30, 186 | Month::December => 31, 187 | }) 188 | } 189 | } 190 | 191 | impl TryFrom for Month { 192 | type Error = OutOfRange; 193 | 194 | fn try_from(value: u8) -> Result { 195 | match value { 196 | 1 => Ok(Month::January), 197 | 2 => Ok(Month::February), 198 | 3 => Ok(Month::March), 199 | 4 => Ok(Month::April), 200 | 5 => Ok(Month::May), 201 | 6 => Ok(Month::June), 202 | 7 => Ok(Month::July), 203 | 8 => Ok(Month::August), 204 | 9 => Ok(Month::September), 205 | 10 => Ok(Month::October), 206 | 11 => Ok(Month::November), 207 | 12 => Ok(Month::December), 208 | _ => Err(OutOfRange::new()), 209 | } 210 | } 211 | } 212 | 213 | impl num_traits::FromPrimitive for Month { 214 | /// Returns an `Option` from a i64, assuming a 1-index, January = 1. 215 | /// 216 | /// `Month::from_i64(n: i64)`: | `1` | `2` | ... | `12` 217 | /// ---------------------------| -------------------- | --------------------- | ... | ----- 218 | /// ``: | Some(Month::January) | Some(Month::February) | ... | Some(Month::December) 219 | #[inline] 220 | fn from_u64(n: u64) -> Option { 221 | Self::from_u32(n as u32) 222 | } 223 | 224 | #[inline] 225 | fn from_i64(n: i64) -> Option { 226 | Self::from_u32(n as u32) 227 | } 228 | 229 | #[inline] 230 | fn from_u32(n: u32) -> Option { 231 | match n { 232 | 1 => Some(Month::January), 233 | 2 => Some(Month::February), 234 | 3 => Some(Month::March), 235 | 4 => Some(Month::April), 236 | 5 => Some(Month::May), 237 | 6 => Some(Month::June), 238 | 7 => Some(Month::July), 239 | 8 => Some(Month::August), 240 | 9 => Some(Month::September), 241 | 10 => Some(Month::October), 242 | 11 => Some(Month::November), 243 | 12 => Some(Month::December), 244 | _ => None, 245 | } 246 | } 247 | } 248 | 249 | /// A duration in calendar months 250 | #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)] 251 | #[cfg_attr(all(feature = "arbitrary", feature = "std"), derive(arbitrary::Arbitrary))] 252 | #[cfg_attr(feature = "defmt", derive(defmt::Format))] 253 | pub struct Months(pub(crate) u32); 254 | 255 | impl Months { 256 | /// Construct a new `Months` from a number of months 257 | pub const fn new(num: u32) -> Self { 258 | Self(num) 259 | } 260 | 261 | /// Returns the total number of months in the `Months` instance. 262 | #[inline] 263 | pub const fn as_u32(&self) -> u32 { 264 | self.0 265 | } 266 | } 267 | 268 | /// An error resulting from reading `` value with `FromStr`. 269 | #[derive(Clone, PartialEq, Eq)] 270 | pub struct ParseMonthError { 271 | pub(crate) _dummy: (), 272 | } 273 | 274 | #[cfg(feature = "std")] 275 | impl std::error::Error for ParseMonthError {} 276 | 277 | #[cfg(all(not(feature = "std"), feature = "core-error"))] 278 | impl core::error::Error for ParseMonthError {} 279 | 280 | impl fmt::Display for ParseMonthError { 281 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 282 | write!(f, "ParseMonthError {{ .. }}") 283 | } 284 | } 285 | 286 | impl fmt::Debug for ParseMonthError { 287 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 288 | write!(f, "ParseMonthError {{ .. }}") 289 | } 290 | } 291 | 292 | #[cfg(feature = "defmt")] 293 | impl defmt::Format for ParseMonthError { 294 | fn format(&self, fmt: defmt::Formatter) { 295 | defmt::write!(fmt, "ParseMonthError {{ .. }}") 296 | } 297 | } 298 | 299 | #[cfg(feature = "serde")] 300 | mod month_serde { 301 | use super::Month; 302 | use serde::{de, ser}; 303 | 304 | use core::fmt; 305 | 306 | impl ser::Serialize for Month { 307 | fn serialize(&self, serializer: S) -> Result 308 | where 309 | S: ser::Serializer, 310 | { 311 | serializer.collect_str(self.name()) 312 | } 313 | } 314 | 315 | struct MonthVisitor; 316 | 317 | impl de::Visitor<'_> for MonthVisitor { 318 | type Value = Month; 319 | 320 | fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result { 321 | f.write_str("Month") 322 | } 323 | 324 | fn visit_str(self, value: &str) -> Result 325 | where 326 | E: de::Error, 327 | { 328 | value.parse().map_err(|_| E::custom("short (3-letter) or full month names expected")) 329 | } 330 | } 331 | 332 | impl<'de> de::Deserialize<'de> for Month { 333 | fn deserialize(deserializer: D) -> Result 334 | where 335 | D: de::Deserializer<'de>, 336 | { 337 | deserializer.deserialize_str(MonthVisitor) 338 | } 339 | } 340 | } 341 | 342 | #[cfg(test)] 343 | mod tests { 344 | use super::Month; 345 | use crate::{Datelike, Months, OutOfRange, TimeZone, Utc}; 346 | 347 | #[test] 348 | fn test_month_enum_try_from() { 349 | assert_eq!(Month::try_from(1), Ok(Month::January)); 350 | assert_eq!(Month::try_from(2), Ok(Month::February)); 351 | assert_eq!(Month::try_from(12), Ok(Month::December)); 352 | assert_eq!(Month::try_from(13), Err(OutOfRange::new())); 353 | 354 | let date = Utc.with_ymd_and_hms(2019, 10, 28, 9, 10, 11).unwrap(); 355 | assert_eq!(Month::try_from(date.month() as u8), Ok(Month::October)); 356 | 357 | let month = Month::January; 358 | let dt = Utc.with_ymd_and_hms(2019, month.number_from_month(), 28, 9, 10, 11).unwrap(); 359 | assert_eq!((dt.year(), dt.month(), dt.day()), (2019, 1, 28)); 360 | } 361 | 362 | #[test] 363 | fn test_month_enum_primitive_parse() { 364 | use num_traits::FromPrimitive; 365 | 366 | let jan_opt = Month::from_u32(1); 367 | let feb_opt = Month::from_u64(2); 368 | let dec_opt = Month::from_i64(12); 369 | let no_month = Month::from_u32(13); 370 | assert_eq!(jan_opt, Some(Month::January)); 371 | assert_eq!(feb_opt, Some(Month::February)); 372 | assert_eq!(dec_opt, Some(Month::December)); 373 | assert_eq!(no_month, None); 374 | 375 | let date = Utc.with_ymd_and_hms(2019, 10, 28, 9, 10, 11).unwrap(); 376 | assert_eq!(Month::from_u32(date.month()), Some(Month::October)); 377 | 378 | let month = Month::January; 379 | let dt = Utc.with_ymd_and_hms(2019, month.number_from_month(), 28, 9, 10, 11).unwrap(); 380 | assert_eq!((dt.year(), dt.month(), dt.day()), (2019, 1, 28)); 381 | } 382 | 383 | #[test] 384 | fn test_month_enum_succ_pred() { 385 | assert_eq!(Month::January.succ(), Month::February); 386 | assert_eq!(Month::December.succ(), Month::January); 387 | assert_eq!(Month::January.pred(), Month::December); 388 | assert_eq!(Month::February.pred(), Month::January); 389 | } 390 | 391 | #[test] 392 | fn test_month_partial_ord() { 393 | assert!(Month::January <= Month::January); 394 | assert!(Month::January < Month::February); 395 | assert!(Month::January < Month::December); 396 | assert!(Month::July >= Month::May); 397 | assert!(Month::September > Month::March); 398 | } 399 | 400 | #[test] 401 | fn test_months_as_u32() { 402 | assert_eq!(Months::new(0).as_u32(), 0); 403 | assert_eq!(Months::new(1).as_u32(), 1); 404 | assert_eq!(Months::new(u32::MAX).as_u32(), u32::MAX); 405 | } 406 | 407 | #[test] 408 | #[cfg(feature = "serde")] 409 | fn test_serde_serialize() { 410 | use Month::*; 411 | use serde_json::to_string; 412 | 413 | let cases: Vec<(Month, &str)> = vec![ 414 | (January, "\"January\""), 415 | (February, "\"February\""), 416 | (March, "\"March\""), 417 | (April, "\"April\""), 418 | (May, "\"May\""), 419 | (June, "\"June\""), 420 | (July, "\"July\""), 421 | (August, "\"August\""), 422 | (September, "\"September\""), 423 | (October, "\"October\""), 424 | (November, "\"November\""), 425 | (December, "\"December\""), 426 | ]; 427 | 428 | for (month, expected_str) in cases { 429 | let string = to_string(&month).unwrap(); 430 | assert_eq!(string, expected_str); 431 | } 432 | } 433 | 434 | #[test] 435 | #[cfg(feature = "serde")] 436 | fn test_serde_deserialize() { 437 | use Month::*; 438 | use serde_json::from_str; 439 | 440 | let cases: Vec<(&str, Month)> = vec![ 441 | ("\"january\"", January), 442 | ("\"jan\"", January), 443 | ("\"FeB\"", February), 444 | ("\"MAR\"", March), 445 | ("\"mar\"", March), 446 | ("\"april\"", April), 447 | ("\"may\"", May), 448 | ("\"june\"", June), 449 | ("\"JULY\"", July), 450 | ("\"august\"", August), 451 | ("\"september\"", September), 452 | ("\"October\"", October), 453 | ("\"November\"", November), 454 | ("\"DECEmbEr\"", December), 455 | ]; 456 | 457 | for (string, expected_month) in cases { 458 | let month = from_str::(string).unwrap(); 459 | assert_eq!(month, expected_month); 460 | } 461 | 462 | let errors: Vec<&str> = 463 | vec!["\"not a month\"", "\"ja\"", "\"Dece\"", "Dec", "\"Augustin\""]; 464 | 465 | for string in errors { 466 | from_str::(string).unwrap_err(); 467 | } 468 | } 469 | 470 | #[test] 471 | #[cfg(feature = "rkyv-validation")] 472 | fn test_rkyv_validation() { 473 | let month = Month::January; 474 | let bytes = rkyv::to_bytes::<_, 1>(&month).unwrap(); 475 | assert_eq!(rkyv::from_bytes::(&bytes).unwrap(), month); 476 | } 477 | 478 | #[test] 479 | fn num_days() { 480 | assert_eq!(Month::January.num_days(2020), Some(31)); 481 | assert_eq!(Month::February.num_days(2020), Some(29)); 482 | assert_eq!(Month::February.num_days(2019), Some(28)); 483 | } 484 | } 485 | --------------------------------------------------------------------------------