diff --git a/.github/workflows/codecov.yml b/.github/workflows/codecov.yml index 74cd0f9b..88634264 100644 --- a/.github/workflows/codecov.yml +++ b/.github/workflows/codecov.yml @@ -12,12 +12,13 @@ jobs: CARGO_TERM_COLOR: always steps: - uses: actions/checkout@v4 - - name: Install Rust - run: rustup update stable + # nightly is required for --doctests, see cargo-llvm-cov#2 + - name: Install Rust (nightly) + run: rustup update nightly - name: Install cargo-llvm-cov uses: taiki-e/install-action@cargo-llvm-cov - name: Generate code coverage - run: cargo llvm-cov --all-features --workspace --lcov --output-path lcov.info + run: cargo +nightly llvm-cov --all-features --workspace --lcov --doctests --output-path lcov.info - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 env: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index e8bc1d31..920a702b 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -17,12 +17,16 @@ jobs: - uses: Swatinem/rust-cache@v2 - run: cargo fmt --check -- --color=always - run: cargo fmt --check --manifest-path fuzz/Cargo.toml + - run: cargo fmt --check --manifest-path bench/Cargo.toml - run: | cargo clippy --all-features --all-targets --color=always \ -- -D warnings - run: | cargo clippy --manifest-path fuzz/Cargo.toml --color=always \ -- -D warnings + - run: | + cargo clippy --manifest-path bench/Cargo.toml --color=always \ + -- -D warnings env: RUSTFLAGS: "-Dwarnings" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 33d00b7a..4479d6ff 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -6,22 +6,10 @@ on: pull_request: jobs: - timezones_linux: + timezones: strategy: matrix: - os: [ubuntu-latest] - tz: ["ACST-9:30", "EST4", "UTC0", "Asia/Katmandu"] - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - uses: Swatinem/rust-cache@v2 - - run: cargo test --all-features --color=always -- --color=always - - timezones_other: - strategy: - matrix: - os: [macos-latest, windows-latest] + os: [ubuntu-latest, macos-latest, windows-latest] tz: ["ACST-9:30", "EST4", "UTC0", "Asia/Katmandu"] runs-on: ${{ matrix.os }} steps: @@ -67,7 +55,7 @@ jobs: with: toolchain: ${{ matrix.rust_version }} - uses: Swatinem/rust-cache@v2 - - run: cargo check --benches + - run: cargo check --manifest-path bench/Cargo.toml --benches - run: cargo check --manifest-path fuzz/Cargo.toml --all-targets # run --lib and --doc to avoid the long running integration tests # which are run elsewhere @@ -119,7 +107,6 @@ jobs: os: [ubuntu-latest] target: [ - wasm32-wasi, wasm32-unknown-emscripten, aarch64-apple-ios, aarch64-linux-android, @@ -145,7 +132,6 @@ jobs: - uses: dtolnay/rust-toolchain@stable with: targets: wasm32-unknown-unknown - - uses: taiki-e/install-action@cargo-hack - uses: Swatinem/rust-cache@v2 - uses: actions/setup-node@v3 - uses: jetli/wasm-pack-action@v0.4.0 @@ -153,6 +139,27 @@ jobs: # with the host system. - run: TZ="$(date +%z)" NOW="$(date +%s)" wasm-pack test --node -- --features wasmbind + test_wasi: + strategy: + matrix: + os: [ubuntu-latest] + target: + - wasm32-wasi + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-wasi + - uses: Swatinem/rust-cache@v2 + - run: cargo install cargo-wasi + - uses: mwilliamson/setup-wasmtime-action@v2 + with: + wasmtime-version: "12.0.1" + # We can't use `--all-features` because `rustc-serialize` doesn't support + # `wasm32-wasi`. + - run: cargo wasi test --features=serde,unstable-locales --color=always -- --color=always + cross-targets: strategy: matrix: @@ -165,6 +172,20 @@ jobs: - uses: Swatinem/rust-cache@v2 - run: cross check --target ${{ matrix.target }} + cross-tests: + strategy: + matrix: + os: [ubuntu-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - run: cargo install cross + - uses: Swatinem/rust-cache@v2 + - run: cross test --lib --all-features --target i686-unknown-linux-gnu --color=always + - run: cross test --doc --all-features --target i686-unknown-linux-gnu --color=always + - run: cross test --lib --all-features --target i686-unknown-linux-musl --color=always + - run: cross test --doc --all-features --target i686-unknown-linux-musl --color=always + check-docs: runs-on: ubuntu-latest steps: diff --git a/CITATION.cff b/CITATION.cff index e5a1720b..5dd5b1fb 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -3,8 +3,8 @@ cff-version: 1.2.0 message: Please cite this crate using these information. # Version information. -date-released: 2023-08-29 -version: 0.4.27 +date-released: 2023-09-07 +version: 0.4.30 # Project information. abstract: Date and time library for Rust diff --git a/Cargo.toml b/Cargo.toml index 4cc62c6c..0e24ee01 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,13 +25,11 @@ std = [] clock = ["std", "winapi", "iana-time-zone", "android-tzdata"] wasmbind = ["wasm-bindgen", "js-sys"] unstable-locales = ["pure-rust-locales", "alloc"] -__internal_bench = ["criterion"] -__doctest = [] +__internal_bench = [] [dependencies] serde = { version = "1.0.99", default-features = false, optional = true } pure-rust-locales = { version = "0.6", optional = true } -criterion = { version = "0.4.0", optional = true } rkyv = { version = "0.7", optional = true } arbitrary = { version = "1.0.0", features = ["derive"], optional = true } @@ -55,7 +53,6 @@ android-tzdata = { version = "0.1.1", optional = true } serde_json = { version = "1" } serde_derive = { version = "1", default-features = false } bincode = { version = "1.3.0" } -doc-comment = { version = "0.3" } [target.'cfg(all(target_arch = "wasm32", not(any(target_os = "emscripten", target_os = "wasi"))))'.dev-dependencies] wasm-bindgen-test = "0.3" @@ -66,13 +63,3 @@ rustdoc-args = ["--cfg", "docsrs"] [package.metadata.playground] features = ["serde"] - -[[bench]] -name = "chrono" -required-features = ["__internal_bench"] -harness = false - -[[bench]] -name = "serde" -required-features = ["__internal_bench", "serde"] -harness = false diff --git a/bench/Cargo.toml b/bench/Cargo.toml new file mode 100644 index 00000000..4c140688 --- /dev/null +++ b/bench/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "benches" +version = "0.1.0" +edition = "2021" + +# Even as a `dev-dependency` Criterion and its dependencies can affect the MSRV of chrono. +# But not when it lives in a separate crate :-). +# See https://github.com/chronotope/chrono/pull/1104. + +[lib] +name = "benches" + +[dependencies] +chrono = { path = "..", features = ["__internal_bench", "serde"] } + +[[bench]] +name = "chrono" +harness = false + +[[bench]] +name = "serde" +harness = false + +[dev-dependencies] +criterion = "0.5.0" +serde_json = "1" diff --git a/benches/chrono.rs b/bench/benches/chrono.rs similarity index 99% rename from benches/chrono.rs rename to bench/benches/chrono.rs index 77a446b2..05ec703c 100644 --- a/benches/chrono.rs +++ b/bench/benches/chrono.rs @@ -1,5 +1,4 @@ //! Benchmarks for chrono that just depend on std -#![cfg(feature = "__internal_bench")] use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; diff --git a/benches/serde.rs b/bench/benches/serde.rs similarity index 95% rename from benches/serde.rs rename to bench/benches/serde.rs index e9de4408..ee4deb5a 100644 --- a/benches/serde.rs +++ b/bench/benches/serde.rs @@ -1,5 +1,3 @@ -#![cfg(feature = "__internal_bench")] - use criterion::{black_box, criterion_group, criterion_main, Criterion}; use chrono::NaiveDateTime; diff --git a/bench/src/lib.rs b/bench/src/lib.rs new file mode 100644 index 00000000..d87371ca --- /dev/null +++ b/bench/src/lib.rs @@ -0,0 +1 @@ +// This file only exists to make `benches` a valid crate. diff --git a/src/datetime/mod.rs b/src/datetime/mod.rs index cfa045dc..2c6606af 100644 --- a/src/datetime/mod.rs +++ b/src/datetime/mod.rs @@ -660,10 +660,24 @@ impl DateTime { /// and returns a new [`DateTime`] instance with the parsed timezone as the [`FixedOffset`]. /// /// RFC 2822 is the internet message standard that specifies the representation of times in HTTP - /// and email headers. + /// and email headers. It is the 2001 revision of RFC 822, and is itself revised as RFC 5322 in + /// 2008. /// - /// The RFC 2822 standard allows arbitrary intermixed whitespace. - /// See [RFC 2822 Appendix A.5] + /// # Support for the obsolete date format + /// + /// - A 2-digit year is interpreted to be a year in 1950-2049. + /// - The standard allows comments and whitespace between many of the tokens. See [4.3] and + /// [Appendix A.5] + /// - Single letter 'military' time zone names are parsed as a `-0000` offset. + /// They were defined with the wrong sign in RFC 822 and corrected in RFC 2822. But because + /// the meaning is now ambiguous, the standard says they should be be considered as `-0000` + /// unless there is out-of-band information confirming their meaning. + /// The exception is `Z`, which remains identical to `+0000`. + /// + /// [4.3]: https://www.rfc-editor.org/rfc/rfc2822#section-4.3 + /// [Appendix A.5]: https://www.rfc-editor.org/rfc/rfc2822#appendix-A.5 + /// + /// # Example /// /// ``` /// # use chrono::{DateTime, FixedOffset, TimeZone}; @@ -672,8 +686,6 @@ impl DateTime { /// FixedOffset::east_opt(0).unwrap().with_ymd_and_hms(2015, 2, 18, 23, 16, 9).unwrap() /// ); /// ``` - /// - /// [RFC 2822 Appendix A.5]: https://www.rfc-editor.org/rfc/rfc2822#appendix-A.5 pub fn parse_from_rfc2822(s: &str) -> ParseResult> { const ITEMS: &[Item<'static>] = &[Item::Fixed(Fixed::RFC2822)]; let mut parsed = Parsed::new(); diff --git a/src/datetime/tests.rs b/src/datetime/tests.rs index 88e08952..607f1eeb 100644 --- a/src/datetime/tests.rs +++ b/src/datetime/tests.rs @@ -311,6 +311,7 @@ fn ymdhms_nano( } // local helper function to easily create a DateTime +#[cfg(any(feature = "alloc", feature = "std"))] fn ymdhms_utc(year: i32, month: u32, day: u32, hour: u32, min: u32, sec: u32) -> DateTime { Utc.with_ymd_and_hms(year, month, day, hour, min, sec).unwrap() } @@ -818,6 +819,7 @@ fn test_datetime_from_str() { .unwrap()) ); assert!("2015-2-18T23:16:9.15".parse::>().is_err()); + assert!("2015-02-18T23:16:9.15øøø".parse::>().is_err()); // no test for `DateTime`, we cannot verify that much. } @@ -915,7 +917,7 @@ fn test_parse_datetime_utc() { } #[test] -fn test_utc_datetime_from_str() { +fn test_parse_from_str() { let edt = FixedOffset::east_opt(570 * 60).unwrap(); let edt0 = FixedOffset::east_opt(0).unwrap(); let wdt = FixedOffset::west_opt(10 * 3600).unwrap(); @@ -930,11 +932,7 @@ fn test_utc_datetime_from_str() { ) .is_err()); assert_eq!( - Utc.datetime_from_str("Fri, 09 Aug 2013 23:54:35 GMT", "%a, %d %b %Y %H:%M:%S GMT"), - Ok(Utc.with_ymd_and_hms(2013, 8, 9, 23, 54, 35).unwrap()) - ); - assert_eq!( - DateTime::::parse_from_str("0", "%s").unwrap(), + DateTime::::parse_from_str("0", "%s").unwrap(), NaiveDateTime::from_timestamp_opt(0, 0).unwrap().and_utc().fixed_offset() ); @@ -978,53 +976,6 @@ fn test_utc_datetime_from_str() { // no test for `DateTime`, we cannot verify that much. } -#[test] -fn test_utc_datetime_from_str_with_spaces() { - let dt = ymdhms_utc(2013, 8, 9, 23, 54, 35); - // with varying spaces - should succeed - assert_eq!(Utc.datetime_from_str(" Aug 09 2013 23:54:35", " %b %d %Y %H:%M:%S"), Ok(dt),); - assert_eq!(Utc.datetime_from_str("Aug 09 2013 23:54:35 ", "%b %d %Y %H:%M:%S "), Ok(dt),); - assert_eq!(Utc.datetime_from_str(" Aug 09 2013 23:54:35 ", " %b %d %Y %H:%M:%S "), Ok(dt),); - assert_eq!(Utc.datetime_from_str(" Aug 09 2013 23:54:35", " %b %d %Y %H:%M:%S"), Ok(dt),); - assert_eq!(Utc.datetime_from_str(" Aug 09 2013 23:54:35", " %b %d %Y %H:%M:%S"), Ok(dt),); - assert_eq!( - Utc.datetime_from_str("\n\tAug 09 2013 23:54:35 ", "\n\t%b %d %Y %H:%M:%S "), - Ok(dt), - ); - assert_eq!(Utc.datetime_from_str("\tAug 09 2013 23:54:35\t", "\t%b %d %Y %H:%M:%S\t"), Ok(dt),); - assert_eq!(Utc.datetime_from_str("Aug 09 2013 23:54:35", "%b %d %Y %H:%M:%S"), Ok(dt),); - assert_eq!(Utc.datetime_from_str("Aug 09 2013 23:54:35", "%b %d %Y %H:%M:%S"), Ok(dt),); - assert_eq!(Utc.datetime_from_str("Aug 09 2013\t23:54:35", "%b %d %Y\t%H:%M:%S"), Ok(dt),); - assert_eq!(Utc.datetime_from_str("Aug 09 2013\t\t23:54:35", "%b %d %Y\t\t%H:%M:%S"), Ok(dt),); - // with varying spaces - should fail - // leading whitespace in format - assert!(Utc.datetime_from_str("Aug 09 2013 23:54:35", " %b %d %Y %H:%M:%S").is_err()); - // trailing whitespace in format - assert!(Utc.datetime_from_str("Aug 09 2013 23:54:35", "%b %d %Y %H:%M:%S ").is_err()); - // extra mid-string whitespace in format - assert!(Utc.datetime_from_str("Aug 09 2013 23:54:35", "%b %d %Y %H:%M:%S").is_err()); - // mismatched leading whitespace - assert!(Utc.datetime_from_str("\tAug 09 2013 23:54:35", "\n%b %d %Y %H:%M:%S").is_err()); - // mismatched trailing whitespace - assert!(Utc.datetime_from_str("Aug 09 2013 23:54:35 ", "%b %d %Y %H:%M:%S\n").is_err()); - // mismatched mid-string whitespace - assert!(Utc.datetime_from_str("Aug 09 2013 23:54:35", "%b %d %Y\t%H:%M:%S").is_err()); - // trailing whitespace in format - assert!(Utc.datetime_from_str("Aug 09 2013 23:54:35", "%b %d %Y %H:%M:%S ").is_err()); - // trailing whitespace (newline) in format - assert!(Utc.datetime_from_str("Aug 09 2013 23:54:35", "%b %d %Y %H:%M:%S\n").is_err()); - // leading space in data - assert!(Utc.datetime_from_str(" Aug 09 2013 23:54:35", "%b %d %Y %H:%M:%S").is_err()); - // trailing space in data - assert!(Utc.datetime_from_str("Aug 09 2013 23:54:35 ", "%b %d %Y %H:%M:%S").is_err()); - // trailing tab in data - assert!(Utc.datetime_from_str("Aug 09 2013 23:54:35\t", "%b %d %Y %H:%M:%S").is_err()); - // mismatched newlines - assert!(Utc.datetime_from_str("\nAug 09 2013 23:54:35", "%b %d %Y %H:%M:%S\n").is_err()); - // trailing literal in data - assert!(Utc.datetime_from_str("Aug 09 2013 23:54:35 !!!", "%b %d %Y %H:%M:%S ").is_err()); -} - #[test] fn test_datetime_parse_from_str() { let dt = ymdhms(&FixedOffset::east_opt(-9 * 60 * 60).unwrap(), 2013, 8, 9, 23, 54, 35); @@ -1218,8 +1169,11 @@ fn test_subsecond_part() { assert_eq!(1234567, datetime.timestamp_subsec_nanos()); } +// Some targets, such as `wasm32-wasi`, have a problematic definition of `SystemTime`, such as an +// `i32` (year 2035 problem), or an `u64` (no values before `UNIX-EPOCH`). +// See https://github.com/rust-lang/rust/issues/44394. #[test] -#[cfg(feature = "std")] +#[cfg(all(feature = "std", not(all(target_arch = "wasm32", target_os = "wasi"))))] fn test_from_system_time() { use std::time::{Duration, SystemTime, UNIX_EPOCH}; diff --git a/src/format/mod.rs b/src/format/mod.rs index d4eac190..8940b074 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -18,14 +18,14 @@ //! # Example #![cfg_attr(not(feature = "std"), doc = "```ignore")] #![cfg_attr(feature = "std", doc = "```rust")] -//! use chrono::{TimeZone, Utc}; +//! use chrono::{NaiveDateTime, TimeZone, Utc}; //! //! let date_time = Utc.with_ymd_and_hms(2020, 11, 10, 0, 1, 32).unwrap(); //! //! let formatted = format!("{}", date_time.format("%Y-%m-%d %H:%M:%S")); //! assert_eq!(formatted, "2020-11-10 00:01:32"); //! -//! let parsed = Utc.datetime_from_str(&formatted, "%Y-%m-%d %H:%M:%S")?; +//! let parsed = NaiveDateTime::parse_from_str(&formatted, "%Y-%m-%d %H:%M:%S")?.and_utc(); //! assert_eq!(parsed, date_time); //! # Ok::<(), chrono::ParseError>(()) //! ``` @@ -451,7 +451,7 @@ const BAD_FORMAT: ParseError = ParseError(ParseErrorKind::BadFormat); // this implementation is here only because we need some private code from `scan` -/// Parsing a `str` into a `Weekday` uses the format [`%W`](./format/strftime/index.html). +/// Parsing a `str` into a `Weekday` uses the format [`%A`](./format/strftime/index.html). /// /// # Example /// @@ -487,7 +487,7 @@ impl FromStr for Weekday { } } -/// Parsing a `str` into a `Month` uses the format [`%W`](./format/strftime/index.html). +/// Parsing a `str` into a `Month` uses the format [`%B`](./format/strftime/index.html). /// /// # Example /// diff --git a/src/format/parse.rs b/src/format/parse.rs index c325e314..6d49cc09 100644 --- a/src/format/parse.rs +++ b/src/format/parse.rs @@ -614,7 +614,7 @@ fn parse_rfc3339_relaxed<'a>(parsed: &mut Parsed, mut s: &'a str) -> ParseResult Ok(_) => return Err(NOT_ENOUGH), }; s = s.trim_start(); - let (s, offset) = if s.len() >= 3 && "UTC".eq_ignore_ascii_case(&s[..3]) { + let (s, offset) = if s.len() >= 3 && "UTC".as_bytes().eq_ignore_ascii_case(&s.as_bytes()[..3]) { (&s[3..], 0) } else { scan::timezone_offset(s, scan::consume_colon_maybe, true, false, true)? @@ -626,7 +626,7 @@ fn parse_rfc3339_relaxed<'a>(parsed: &mut Parsed, mut s: &'a str) -> ParseResult #[cfg(test)] mod tests { use crate::format::*; - use crate::{DateTime, FixedOffset, TimeZone, Timelike, Utc}; + use crate::{DateTime, FixedOffset, NaiveDateTime, TimeZone, Timelike, Utc}; macro_rules! parsed { ($($k:ident: $v:expr),*) => (#[allow(unused_mut)] { @@ -1733,7 +1733,10 @@ mod tests { assert_eq!(dt.format(RFC850_FMT).to_string(), "Sunday, 06-Nov-94 08:49:37 GMT"); // Check that it parses correctly - assert_eq!(Ok(dt), Utc.datetime_from_str("Sunday, 06-Nov-94 08:49:37 GMT", RFC850_FMT)); + assert_eq!( + NaiveDateTime::parse_from_str("Sunday, 06-Nov-94 08:49:37 GMT", RFC850_FMT), + Ok(dt.naive_utc()) + ); // Check that the rest of the weekdays parse correctly (this test originally failed because // Sunday parsed incorrectly). @@ -1765,7 +1768,7 @@ mod tests { ]; for val in &testdates { - assert_eq!(Ok(val.0), Utc.datetime_from_str(val.1, RFC850_FMT)); + assert_eq!(NaiveDateTime::parse_from_str(val.1, RFC850_FMT), Ok(val.0.naive_utc())); } let test_dates_fail = [ @@ -1784,7 +1787,7 @@ mod tests { ]; for val in &test_dates_fail { - assert!(Utc.datetime_from_str(val, RFC850_FMT).is_err()); + assert!(NaiveDateTime::parse_from_str(val, RFC850_FMT).is_err()); } } diff --git a/src/format/strftime.rs b/src/format/strftime.rs index 63c0427c..41e53b3f 100644 --- a/src/format/strftime.rs +++ b/src/format/strftime.rs @@ -909,11 +909,20 @@ mod tests { } #[test] - #[cfg(feature = "unstable-locales")] + #[cfg(all(feature = "unstable-locales", target_pointer_width = "64"))] fn test_type_sizes() { use core::mem::size_of; assert_eq!(size_of::(), 24); assert_eq!(size_of::(), 56); assert_eq!(size_of::(), 2); } + + #[test] + #[cfg(all(feature = "unstable-locales", target_pointer_width = "32"))] + fn test_type_sizes() { + use core::mem::size_of; + assert_eq!(size_of::(), 12); + assert_eq!(size_of::(), 28); + assert_eq!(size_of::(), 2); + } } diff --git a/src/lib.rs b/src/lib.rs index ff6641eb..89d8fca5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,20 +1,21 @@ //! # Chrono: Date and Time for Rust //! -//! It aims to be a feature-complete superset of -//! the [time](https://github.com/rust-lang-deprecated/time) library. -//! In particular, + +//! Chrono aims to provide all functionality needed to do correct operations on dates and times in the +//! [proleptic Gregorian calendar](https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar): //! -//! * Chrono strictly adheres to ISO 8601. -//! * Chrono is timezone-aware by default, with separate timezone-naive types. -//! * Chrono is space-optimal and (while not being the primary goal) reasonably efficient. +//! * The [`DateTime`](https://docs.rs/chrono/latest/chrono/struct.DateTime.html) type is timezone-aware +//! by default, with separate timezone-naive types. +//! * Operations that may produce an invalid or ambiguous date and time return `Option` or +//! [`LocalResult`](https://docs.rs/chrono/latest/chrono/offset/enum.LocalResult.html). +//! * Configurable parsing and formatting with a `strftime` inspired date and time formatting syntax. +//! * The [`Local`](https://docs.rs/chrono/latest/chrono/offset/struct.Local.html) timezone works with +//! the current timezone of the OS. +//! * Types and operations are implemented to be reasonably efficient. //! -//! There were several previous attempts to bring a good date and time library to Rust, -//! which Chrono builds upon and should acknowledge: -//! -//! * [Initial research on -//! the wiki](https://github.com/rust-lang/rust-wiki-backup/blob/master/Lib-datetime.md) -//! * Dietrich Epp's [datetime-rs](https://github.com/depp/datetime-rs) -//! * Luis de Bethencourt's [rust-datetime](https://github.com/luisbg/rust-datetime) +//! Timezone data is not shipped with chrono by default to limit binary sizes. Use the companion crate +//! [Chrono-TZ](https://crates.io/crates/chrono-tz) or [`tzfile`](https://crates.io/crates/tzfile) for +//! full timezone support. //! //! ### Features //! @@ -29,10 +30,13 @@ //! and traits. //! - `clock`: Enables reading the system time (`now`) that depends on the standard library for //! UNIX-like operating systems and the Windows API (`winapi`) for Windows. +//! - `wasmbind`: Interface with the JS Date API for the `wasm32` target. //! //! Optional features: //! //! - [`serde`][]: Enable serialization/deserialization via serde. +//! - `rkyv`: Enable serialization/deserialization via rkyv. +//! - `arbitrary`: construct arbitrary instances of a type with the Arbitrary crate. //! - `unstable-locales`: Enable localization. This adds various methods with a //! `_localized` suffix. The implementation and API may change or even be //! removed in a patch release. Feedback welcome. @@ -284,16 +288,12 @@ //! Ok(fixed_dt.clone())); //! assert_eq!(DateTime::::parse_from_rfc3339("2014-11-28T21:00:09+09:00"), Ok(fixed_dt.clone())); //! -//! // method 3 -//! assert_eq!(Utc.datetime_from_str("2014-11-28 12:00:09", "%Y-%m-%d %H:%M:%S"), Ok(dt.clone())); -//! assert_eq!(Utc.datetime_from_str("Fri Nov 28 12:00:09 2014", "%a %b %e %T %Y"), Ok(dt.clone())); -//! //! // oops, the year is missing! -//! assert!(Utc.datetime_from_str("Fri Nov 28 12:00:09", "%a %b %e %T %Y").is_err()); +//! assert!(DateTime::::parse_from_str("Fri Nov 28 12:00:09", "%a %b %e %T %Y").is_err()); //! // oops, the format string does not include the year at all! -//! assert!(Utc.datetime_from_str("Fri Nov 28 12:00:09", "%a %b %e %T").is_err()); +//! assert!(DateTime::::parse_from_str("Fri Nov 28 12:00:09", "%a %b %e %T").is_err()); //! // oops, the weekday is incorrect! -//! assert!(Utc.datetime_from_str("Sat Nov 28 12:00:09 2014", "%a %b %e %T %Y").is_err()); +//! assert!(DateTime::::parse_from_str("Sat Nov 28 12:00:09 2014", "%a %b %e %T %Y").is_err()); //! ``` //! //! Again : See [`format::strftime`](./format/strftime/index.html#specifiers) @@ -343,18 +343,18 @@ //! //! ## Limitations //! -//! Only proleptic Gregorian calendar (i.e. extended to support older dates) is supported. -//! Be very careful if you really have to deal with pre-20C dates, they can be in Julian or others. +//! Only the proleptic Gregorian calendar (i.e. extended to support older dates) is supported. +//! Date types are limited to about +/- 262,000 years from the common epoch. +//! Time types are limited to nanosecond accuracy. +//! Leap seconds can be represented, but Chrono does not fully support them. +//! See [Leap Second Handling](https://docs.rs/chrono/latest/chrono/naive/struct.NaiveTime.html#leap-second-handling). //! -//! Date types are limited in about +/- 262,000 years from the common epoch. -//! Time types are limited in the nanosecond accuracy. +//! ## Rust version requirements //! -//! [Leap seconds are supported in the representation but -//! Chrono doesn't try to make use of them](./naive/struct.NaiveTime.html#leap-second-handling). -//! (The main reason is that leap seconds are not really predictable.) -//! Almost *every* operation over the possible leap seconds will ignore them. -//! Consider using `NaiveDateTime` with the implicit TAI (International Atomic Time) scale -//! if you want. +//! The Minimum Supported Rust Version (MSRV) is currently **Rust 1.57.0**. +//! +//! The MSRV is explicitly tested in CI. It may be bumped in minor releases, but this is not done +//! lightly. //! //! Chrono inherently does not support an inaccurate or partial date and time representation. //! Any operation that can be ambiguous will return `None` in such cases. @@ -367,6 +367,89 @@ //! //! Advanced time zone handling is not yet supported. //! For now you can try the [Chrono-tz](https://github.com/chronotope/chrono-tz/) crate instead. +//! +//! ## Relation between chrono and time 0.1 +//! +//! Rust first had a `time` module added to `std` in its 0.7 release. It later moved to +//! `libextra`, and then to a `libtime` library shipped alongside the standard library. In 2014 +//! work on chrono started in order to provide a full-featured date and time library in Rust. +//! Some improvements from chrono made it into the standard library; notably, `chrono::Duration` +//! was included as `std::time::Duration` ([rust#15934]) in 2014. +//! +//! In preparation of Rust 1.0 at the end of 2014 `libtime` was moved out of the Rust distro and +//! into the `time` crate to eventually be redesigned ([rust#18832], [rust#18858]), like the +//! `num` and `rand` crates. Of course chrono kept its dependency on this `time` crate. `time` +//! started re-exporting `std::time::Duration` during this period. Later, the standard library was +//! changed to have a more limited unsigned `Duration` type ([rust#24920], [RFC 1040]), while the +//! `time` crate kept the full functionality with `time::Duration`. `time::Duration` had been a +//! part of chrono's public API. +//! +//! By 2016 `time` 0.1 lived under the `rust-lang-deprecated` organisation and was not actively +//! maintained ([time#136]). chrono absorbed the platform functionality and `Duration` type of the +//! `time` crate in [chrono#478] (the work started in [chrono#286]). In order to preserve +//! compatibility with downstream crates depending on `time` and `chrono` sharing a `Duration` +//! type, chrono kept depending on time 0.1. chrono offered the option to opt out of the `time` +//! dependency by disabling the `oldtime` feature (swapping it out for an effectively similar +//! chrono type). In 2019, @jhpratt took over maintenance on the `time` crate and released what +//! amounts to a new crate as `time` 0.2. +//! +//! [rust#15934]: https://github.com/rust-lang/rust/pull/15934 +//! [rust#18832]: https://github.com/rust-lang/rust/pull/18832#issuecomment-62448221 +//! [rust#18858]: https://github.com/rust-lang/rust/pull/18858 +//! [rust#24920]: https://github.com/rust-lang/rust/pull/24920 +//! [RFC 1040]: https://rust-lang.github.io/rfcs/1040-duration-reform.html +//! [time#136]: https://github.com/time-rs/time/issues/136 +//! [chrono#286]: https://github.com/chronotope/chrono/pull/286 +//! [chrono#478]: https://github.com/chronotope/chrono/pull/478 +//! +//! ## Security advisories +//! +//! In November of 2020 [CVE-2020-26235] and [RUSTSEC-2020-0071] were opened against the `time` crate. +//! @quininer had found that calls to `localtime_r` may be unsound ([chrono#499]). Eventually, almost +//! a year later, this was also made into a security advisory against chrono as [RUSTSEC-2020-0159], +//! which had platform code similar to `time`. +//! +//! On Unix-like systems a process is given a timezone id or description via the `TZ` environment +//! variable. We need this timezone data to calculate the current local time from a value that is +//! in UTC, such as the time from the system clock. `time` 0.1 and chrono used the POSIX function +//! `localtime_r` to do the conversion to local time, which reads the `TZ` variable. +//! +//! Rust assumes the environment to be writable and uses locks to access it from multiple threads. +//! Some other programming languages and libraries use similar locking strategies, but these are +//! typically not shared across languages. More importantly, POSIX declares modifying the +//! environment in a multi-threaded process as unsafe, and `getenv` in libc can't be changed to +//! take a lock because it returns a pointer to the data (see [rust#27970] for more discussion). +//! +//! Since version 4.20 chrono no longer uses `localtime_r`, instead using Rust code to query the +//! timezone (from the `TZ` variable or via `iana-time-zone` as a fallback) and work with data +//! from the system timezone database directly. The code for this was forked from the [tz-rs crate] +//! by @x-hgg-x. As such, chrono now respects the Rust lock when reading the `TZ` environment +//! variable. In general, code should avoid modifying the environment. +//! +//! [CVE-2020-26235]: https://nvd.nist.gov/vuln/detail/CVE-2020-26235 +//! [RUSTSEC-2020-0071]: https://rustsec.org/advisories/RUSTSEC-2020-0071 +//! [chrono#499]: https://github.com/chronotope/chrono/pull/499 +//! [RUSTSEC-2020-0159]: https://rustsec.org/advisories/RUSTSEC-2020-0159.html +//! [rust#27970]: https://github.com/rust-lang/rust/issues/27970 +//! [chrono#677]: https://github.com/chronotope/chrono/pull/677 +//! [tz-rs crate]: https://crates.io/crates/tz-rs +//! +//! ## Removing time 0.1 +//! +//! Because time 0.1 has been unmaintained for years, however, the security advisory mentioned +//! above has not been addressed. While chrono maintainers were careful not to break backwards +//! compatibility with the `time::Duration` type, there has been a long stream of issues from +//! users inquiring about the time 0.1 dependency with the vulnerability. We investigated the +//! potential breakage of removing the time 0.1 dependency in [chrono#1095] using a crater-like +//! experiment and determined that the potential for breaking (public) dependencies is very low. +//! We reached out to those few crates that did still depend on compatibility with time 0.1. +//! +//! As such, for chrono 0.4.30 we have decided to swap out the time 0.1 `Duration` implementation +//! for a local one that will offer a strict superset of the existing API going forward. This +//! will prevent most downstream users from being affected by the security vulnerability in time +//! 0.1 while minimizing the ecosystem impact of semver-incompatible version churn. +//! +//! [chrono#1095]: https://github.com/chronotope/chrono/pull/1095 #![doc(html_root_url = "https://docs.rs/chrono/latest/", test(attr(deny(warnings))))] #![cfg_attr(feature = "bench", feature(test))] // lib stability features as per RFC #507 @@ -385,14 +468,6 @@ use core::fmt; mod time_delta; pub use time_delta::TimeDelta; -#[cfg(feature = "__doctest")] -#[cfg_attr(feature = "__doctest", cfg(doctest))] -use doc_comment::doctest; - -#[cfg(feature = "__doctest")] -#[cfg_attr(feature = "__doctest", cfg(doctest))] -doctest!("../README.md"); - /// A convenience module appropriate for glob imports (`use chrono::prelude::*;`). pub mod prelude { #[doc(no_inline)] diff --git a/src/naive/date.rs b/src/naive/date.rs index 2aef693f..954baa91 100644 --- a/src/naive/date.rs +++ b/src/naive/date.rs @@ -1409,6 +1409,21 @@ impl NaiveDate { NaiveWeek { date: *self, start } } + /// Returns `true` if this is a leap year. + /// + /// ``` + /// # use chrono::NaiveDate; + /// assert_eq!(NaiveDate::from_ymd_opt(2000, 1, 1).unwrap().leap_year(), true); + /// assert_eq!(NaiveDate::from_ymd_opt(2001, 1, 1).unwrap().leap_year(), false); + /// assert_eq!(NaiveDate::from_ymd_opt(2002, 1, 1).unwrap().leap_year(), false); + /// assert_eq!(NaiveDate::from_ymd_opt(2003, 1, 1).unwrap().leap_year(), false); + /// assert_eq!(NaiveDate::from_ymd_opt(2004, 1, 1).unwrap().leap_year(), true); + /// assert_eq!(NaiveDate::from_ymd_opt(2100, 1, 1).unwrap().leap_year(), false); + /// ``` + pub const fn leap_year(&self) -> bool { + self.ymdf & (0b1000) == 0 + } + // This duplicates `Datelike::year()`, because trait methods can't be const yet. #[inline] const fn year(&self) -> i32 { @@ -3172,6 +3187,16 @@ mod tests { assert!(dt.with_ordinal0(4294967295).is_none()); } + #[test] + fn test_leap_year() { + for year in 0..=MAX_YEAR { + let date = NaiveDate::from_ymd_opt(year, 1, 1).unwrap(); + let is_leap = year % 4 == 0 && (year % 100 != 0 || year % 400 == 0); + assert_eq!(date.leap_year(), is_leap); + assert_eq!(date.leap_year(), date.with_ordinal(366).is_some()); + } + } + // MAX_YEAR-12-31 minus 0000-01-01 // = ((MAX_YEAR+1)-01-01 minus 0001-01-01) + (0001-01-01 minus 0000-01-01) - 1 day // = ((MAX_YEAR+1)-01-01 minus 0001-01-01) + 365 days diff --git a/src/naive/datetime/tests.rs b/src/naive/datetime/tests.rs index c1ee385d..91370464 100644 --- a/src/naive/datetime/tests.rs +++ b/src/naive/datetime/tests.rs @@ -348,6 +348,51 @@ fn test_datetime_parse_from_str() { ); } +#[test] +fn test_datetime_parse_from_str_with_spaces() { + let parse_from_str = NaiveDateTime::parse_from_str; + let dt = NaiveDate::from_ymd_opt(2013, 8, 9).unwrap().and_hms_opt(23, 54, 35).unwrap(); + // with varying spaces - should succeed + assert_eq!(parse_from_str(" Aug 09 2013 23:54:35", " %b %d %Y %H:%M:%S"), Ok(dt)); + assert_eq!(parse_from_str("Aug 09 2013 23:54:35 ", "%b %d %Y %H:%M:%S "), Ok(dt)); + assert_eq!(parse_from_str(" Aug 09 2013 23:54:35 ", " %b %d %Y %H:%M:%S "), Ok(dt)); + assert_eq!(parse_from_str(" Aug 09 2013 23:54:35", " %b %d %Y %H:%M:%S"), Ok(dt)); + assert_eq!(parse_from_str(" Aug 09 2013 23:54:35", " %b %d %Y %H:%M:%S"), Ok(dt)); + assert_eq!(parse_from_str("\n\tAug 09 2013 23:54:35 ", "\n\t%b %d %Y %H:%M:%S "), Ok(dt),); + assert_eq!(parse_from_str("\tAug 09 2013 23:54:35\t", "\t%b %d %Y %H:%M:%S\t"), Ok(dt)); + assert_eq!(parse_from_str("Aug 09 2013 23:54:35", "%b %d %Y %H:%M:%S"), Ok(dt)); + assert_eq!(parse_from_str("Aug 09 2013 23:54:35", "%b %d %Y %H:%M:%S"), Ok(dt)); + assert_eq!(parse_from_str("Aug 09 2013\t23:54:35", "%b %d %Y\t%H:%M:%S"), Ok(dt)); + assert_eq!(parse_from_str("Aug 09 2013\t\t23:54:35", "%b %d %Y\t\t%H:%M:%S"), Ok(dt)); + // with varying spaces - should fail + // leading whitespace in format + assert!(parse_from_str("Aug 09 2013 23:54:35", " %b %d %Y %H:%M:%S").is_err()); + // trailing whitespace in format + assert!(parse_from_str("Aug 09 2013 23:54:35", "%b %d %Y %H:%M:%S ").is_err()); + // extra mid-string whitespace in format + assert!(parse_from_str("Aug 09 2013 23:54:35", "%b %d %Y %H:%M:%S").is_err()); + // mismatched leading whitespace + assert!(parse_from_str("\tAug 09 2013 23:54:35", "\n%b %d %Y %H:%M:%S").is_err()); + // mismatched trailing whitespace + assert!(parse_from_str("Aug 09 2013 23:54:35 ", "%b %d %Y %H:%M:%S\n").is_err()); + // mismatched mid-string whitespace + assert!(parse_from_str("Aug 09 2013 23:54:35", "%b %d %Y\t%H:%M:%S").is_err()); + // trailing whitespace in format + assert!(parse_from_str("Aug 09 2013 23:54:35", "%b %d %Y %H:%M:%S ").is_err()); + // trailing whitespace (newline) in format + assert!(parse_from_str("Aug 09 2013 23:54:35", "%b %d %Y %H:%M:%S\n").is_err()); + // leading space in data + assert!(parse_from_str(" Aug 09 2013 23:54:35", "%b %d %Y %H:%M:%S").is_err()); + // trailing space in data + assert!(parse_from_str("Aug 09 2013 23:54:35 ", "%b %d %Y %H:%M:%S").is_err()); + // trailing tab in data + assert!(parse_from_str("Aug 09 2013 23:54:35\t", "%b %d %Y %H:%M:%S").is_err()); + // mismatched newlines + assert!(parse_from_str("\nAug 09 2013 23:54:35", "%b %d %Y %H:%M:%S\n").is_err()); + // trailing literal in data + assert!(parse_from_str("Aug 09 2013 23:54:35 !!!", "%b %d %Y %H:%M:%S ").is_err()); +} + #[test] fn test_datetime_add_sub_invariant() { // issue #37 diff --git a/src/offset/local/mod.rs b/src/offset/local/mod.rs index d7e7792c..601b92d7 100644 --- a/src/offset/local/mod.rs +++ b/src/offset/local/mod.rs @@ -257,12 +257,4 @@ mod tests { ); } } - - /// Test Issue #866 - #[test] - fn test_issue_866() { - #[allow(deprecated)] - let local_20221106 = Local.ymd(2022, 11, 6); - let _dt_20221106 = local_20221106.and_hms_milli_opt(1, 2, 59, 1000).unwrap(); - } } diff --git a/src/offset/local/tz_info/timezone.rs b/src/offset/local/tz_info/timezone.rs index 4cf6afe3..02b6f341 100644 --- a/src/offset/local/tz_info/timezone.rs +++ b/src/offset/local/tz_info/timezone.rs @@ -859,8 +859,12 @@ mod tests { assert_eq!(time_zone_local, time_zone_local_1); } - let time_zone_utc = TimeZone::from_posix_tz("UTC")?; - assert_eq!(time_zone_utc.find_local_time_type(0)?.offset(), 0); + // `TimeZone::from_posix_tz("UTC")` will return `Error` if the environment does not have + // a time zone database, like for example some docker containers. + // In that case skip the test. + if let Ok(time_zone_utc) = TimeZone::from_posix_tz("UTC") { + assert_eq!(time_zone_utc.find_local_time_type(0)?.offset(), 0); + } } assert!(TimeZone::from_posix_tz("EST5EDT,0/0,J365/25").is_err()); diff --git a/src/offset/mod.rs b/src/offset/mod.rs index ea539153..419550e2 100644 --- a/src/offset/mod.rs +++ b/src/offset/mod.rs @@ -444,6 +444,7 @@ pub trait TimeZone: Sized + Clone { /// /// See also [`DateTime::parse_from_str`] which gives a [`DateTime`] with /// parsed [`FixedOffset`]. + #[deprecated(since = "0.4.29", note = "use `DateTime::parse_from_str` instead")] fn datetime_from_str(&self, s: &str, fmt: &str) -> ParseResult> { let mut parsed = Parsed::new(); parse(&mut parsed, s, StrftimeItems::new(fmt))?; diff --git a/src/time_delta.rs b/src/time_delta.rs index 90e15f54..0b11be2d 100644 --- a/src/time_delta.rs +++ b/src/time_delta.rs @@ -34,9 +34,9 @@ const SECS_PER_MINUTE: i64 = 60; /// The number of seconds in an hour. const SECS_PER_HOUR: i64 = 3600; /// The number of (non-leap) seconds in days. -const SECS_PER_DAY: i64 = 86400; +const SECS_PER_DAY: i64 = 86_400; /// The number of (non-leap) seconds in a week. -const SECS_PER_WEEK: i64 = 604800; +const SECS_PER_WEEK: i64 = 604_800; macro_rules! try_opt { ($e:expr) => { @@ -492,19 +492,22 @@ mod tests { assert!(TimeDelta::seconds(1) != TimeDelta::zero()); assert_eq!(TimeDelta::seconds(1) + TimeDelta::seconds(2), TimeDelta::seconds(3)); assert_eq!( - TimeDelta::seconds(86399) + TimeDelta::seconds(4), + TimeDelta::seconds(86_399) + TimeDelta::seconds(4), TimeDelta::days(1) + TimeDelta::seconds(3) ); - assert_eq!(TimeDelta::days(10) - TimeDelta::seconds(1000), TimeDelta::seconds(863000)); - assert_eq!(TimeDelta::days(10) - TimeDelta::seconds(1000000), TimeDelta::seconds(-136000)); + assert_eq!(TimeDelta::days(10) - TimeDelta::seconds(1000), TimeDelta::seconds(863_000)); assert_eq!( - TimeDelta::days(2) + TimeDelta::seconds(86399) + TimeDelta::nanoseconds(1234567890), - TimeDelta::days(3) + TimeDelta::nanoseconds(234567890) + TimeDelta::days(10) - TimeDelta::seconds(1_000_000), + TimeDelta::seconds(-136_000) + ); + assert_eq!( + TimeDelta::days(2) + TimeDelta::seconds(86_399) + TimeDelta::nanoseconds(1_234_567_890), + TimeDelta::days(3) + TimeDelta::nanoseconds(234_567_890) ); assert_eq!(-TimeDelta::days(3), TimeDelta::days(-3)); assert_eq!( -(TimeDelta::days(3) + TimeDelta::seconds(70)), - TimeDelta::days(-4) + TimeDelta::seconds(86400 - 70) + TimeDelta::days(-4) + TimeDelta::seconds(86_400 - 70) ); } @@ -513,10 +516,10 @@ mod tests { assert_eq!(TimeDelta::zero().num_days(), 0); assert_eq!(TimeDelta::days(1).num_days(), 1); assert_eq!(TimeDelta::days(-1).num_days(), -1); - assert_eq!(TimeDelta::seconds(86399).num_days(), 0); - assert_eq!(TimeDelta::seconds(86401).num_days(), 1); - assert_eq!(TimeDelta::seconds(-86399).num_days(), 0); - assert_eq!(TimeDelta::seconds(-86401).num_days(), -1); + assert_eq!(TimeDelta::seconds(86_399).num_days(), 0); + assert_eq!(TimeDelta::seconds(86_401).num_days(), 1); + assert_eq!(TimeDelta::seconds(-86_399).num_days(), 0); + assert_eq!(TimeDelta::seconds(-86_401).num_days(), -1); assert_eq!(TimeDelta::days(i32::MAX as i64).num_days(), i32::MAX as i64); assert_eq!(TimeDelta::days(i32::MIN as i64).num_days(), i32::MIN as i64); } @@ -705,7 +708,7 @@ mod tests { assert_eq!(TimeDelta::microseconds(42).to_string(), "PT0.000042S"); assert_eq!(TimeDelta::nanoseconds(42).to_string(), "PT0.000000042S"); assert_eq!((TimeDelta::days(7) + TimeDelta::milliseconds(6543)).to_string(), "P7DT6.543S"); - assert_eq!(TimeDelta::seconds(-86401).to_string(), "-P1DT1S"); + assert_eq!(TimeDelta::seconds(-86_401).to_string(), "-P1DT1S"); assert_eq!(TimeDelta::nanoseconds(-1).to_string(), "-PT0.000000001S"); // the format specifier should have no effect on `TimeDelta` @@ -718,11 +721,14 @@ mod tests { #[test] fn test_to_std() { assert_eq!(TimeDelta::seconds(1).to_std(), Ok(StdDuration::new(1, 0))); - assert_eq!(TimeDelta::seconds(86401).to_std(), Ok(StdDuration::new(86401, 0))); - assert_eq!(TimeDelta::milliseconds(123).to_std(), Ok(StdDuration::new(0, 123000000))); - assert_eq!(TimeDelta::milliseconds(123765).to_std(), Ok(StdDuration::new(123, 765000000))); + assert_eq!(TimeDelta::seconds(86_401).to_std(), Ok(StdDuration::new(86_401, 0))); + assert_eq!(TimeDelta::milliseconds(123).to_std(), Ok(StdDuration::new(0, 123_000_000))); + assert_eq!( + TimeDelta::milliseconds(123_765).to_std(), + Ok(StdDuration::new(123, 765_000_000)) + ); assert_eq!(TimeDelta::nanoseconds(777).to_std(), Ok(StdDuration::new(0, 777))); - assert_eq!(MAX.to_std(), Ok(StdDuration::new(9223372036854775, 807000000))); + assert_eq!(MAX.to_std(), Ok(StdDuration::new(9_223_372_036_854_775, 807_000_000))); assert_eq!(TimeDelta::seconds(-1).to_std(), Err(OutOfRangeError(()))); assert_eq!(TimeDelta::milliseconds(-1).to_std(), Err(OutOfRangeError(()))); } @@ -730,23 +736,26 @@ mod tests { #[test] fn test_from_std() { assert_eq!(Ok(TimeDelta::seconds(1)), TimeDelta::from_std(StdDuration::new(1, 0))); - assert_eq!(Ok(TimeDelta::seconds(86401)), TimeDelta::from_std(StdDuration::new(86401, 0))); + assert_eq!(Ok(TimeDelta::seconds(86401)), TimeDelta::from_std(StdDuration::new(86_401, 0))); assert_eq!( Ok(TimeDelta::milliseconds(123)), - TimeDelta::from_std(StdDuration::new(0, 123000000)) + TimeDelta::from_std(StdDuration::new(0, 123_000_000)) ); assert_eq!( - Ok(TimeDelta::milliseconds(123765)), - TimeDelta::from_std(StdDuration::new(123, 765000000)) + Ok(TimeDelta::milliseconds(123_765)), + TimeDelta::from_std(StdDuration::new(123, 765_000_000)) ); assert_eq!(Ok(TimeDelta::nanoseconds(777)), TimeDelta::from_std(StdDuration::new(0, 777))); - assert_eq!(Ok(MAX), TimeDelta::from_std(StdDuration::new(9223372036854775, 807000000))); assert_eq!( - TimeDelta::from_std(StdDuration::new(9223372036854776, 0)), + Ok(MAX), + TimeDelta::from_std(StdDuration::new(9_223_372_036_854_775, 807_000_000)) + ); + assert_eq!( + TimeDelta::from_std(StdDuration::new(9_223_372_036_854_776, 0)), Err(OutOfRangeError(())) ); assert_eq!( - TimeDelta::from_std(StdDuration::new(9223372036854775, 807000001)), + TimeDelta::from_std(StdDuration::new(9_223_372_036_854_775, 807_000_001)), Err(OutOfRangeError(())) ); } diff --git a/src/traits.rs b/src/traits.rs index 8a63300c..0018a7d9 100644 --- a/src/traits.rs +++ b/src/traits.rs @@ -316,6 +316,11 @@ pub trait Timelike: Sized { fn with_nanosecond(&self, nano: u32) -> Option; /// Returns the number of non-leap seconds past the last midnight. + /// + /// Every value in 00:00:00-23:59:59 maps to an integer in 0-86399. + /// + /// This method is not intended to provide the real number of seconds since midnight on a given + /// day. It does not take things like DST transitions into account. #[inline] fn num_seconds_from_midnight(&self) -> u32 { self.hour() * 3600 + self.minute() * 60 + self.second()