mirror of
https://github.com/chronotope/chrono.git
synced 2025-10-02 07:21:41 +00:00
Merge branch '0.4.x' into merge_0.4.x
This commit is contained in:
commit
8164f9f635
12
.github/workflows/lint.yml
vendored
12
.github/workflows/lint.yml
vendored
@ -18,16 +18,20 @@ jobs:
|
|||||||
- run: cargo fmt --check -- --color=always
|
- run: cargo fmt --check -- --color=always
|
||||||
- run: cargo fmt --check --manifest-path fuzz/Cargo.toml
|
- run: cargo fmt --check --manifest-path fuzz/Cargo.toml
|
||||||
- run: cargo clippy --color=always -- -D warnings
|
- run: cargo clippy --color=always -- -D warnings
|
||||||
- run: cargo clippy --color=always --target x86_64-pc-windows-msvc -- -D warnings
|
- run: |
|
||||||
- run: cargo clippy --manifest-path fuzz/Cargo.toml --color=always -- -D warnings
|
cargo clippy --color=always --target x86_64-pc-windows-msvc \
|
||||||
|
-- -D warnings
|
||||||
|
- run: |
|
||||||
|
cargo clippy --manifest-path fuzz/Cargo.toml --color=always \
|
||||||
|
-- -D warnings
|
||||||
env:
|
env:
|
||||||
RUSTFLAGS: "-Dwarnings"
|
RUSTFLAGS: "-Dwarnings"
|
||||||
|
|
||||||
cargo-deny:
|
cargo-deny:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: EmbarkStudios/cargo-deny-action@v1
|
- uses: EmbarkStudios/cargo-deny-action@v1
|
||||||
|
|
||||||
check-doc:
|
check-doc:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
36
.github/workflows/test.yml
vendored
36
.github/workflows/test.yml
vendored
@ -45,9 +45,18 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
toolchain: 1.56.1
|
toolchain: 1.56.1
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
# run --lib and --doc to avoid the long running integration tests which are run elsewhere
|
# run --lib and --doc to avoid the long running integration tests
|
||||||
- run: cargo test --lib --features unstable-locales,wasmbind,clock,serde,windows-sys --color=always -- --color=always
|
# which are run elsewhere
|
||||||
- run: cargo test --doc --features unstable-locales,wasmbind,clock,serde,windows-sys --color=always -- --color=always
|
- run: |
|
||||||
|
cargo test --lib \
|
||||||
|
--features \
|
||||||
|
unstable-locales,wasmbind,clock,serde,windows-sys \
|
||||||
|
--color=always -- --color=always
|
||||||
|
- run: |
|
||||||
|
cargo test --doc \
|
||||||
|
--features \
|
||||||
|
unstable-locales,wasmbind,clock,serde,windows-sys \
|
||||||
|
--color=always -- --color=always
|
||||||
|
|
||||||
rust_versions:
|
rust_versions:
|
||||||
strategy:
|
strategy:
|
||||||
@ -63,7 +72,8 @@ jobs:
|
|||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
- run: cargo check --benches
|
- run: cargo check --benches
|
||||||
- run: cargo check --manifest-path fuzz/Cargo.toml --all-targets
|
- 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
|
# run --lib and --doc to avoid the long running integration tests
|
||||||
|
# which are run elsewhere
|
||||||
- run: cargo test --lib --all-features --color=always -- --color=always
|
- run: cargo test --lib --all-features --color=always -- --color=always
|
||||||
- run: cargo test --doc --all-features --color=always -- --color=always
|
- run: cargo test --doc --all-features --color=always -- --color=always
|
||||||
|
|
||||||
@ -77,7 +87,13 @@ jobs:
|
|||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
- uses: taiki-e/install-action@cargo-hack
|
- uses: taiki-e/install-action@cargo-hack
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
- run: cargo hack check --feature-powerset --optional-deps serde,rkyv --skip default --skip __internal_bench --skip __doctest --skip iana-time-zone --skip pure-rust-locales
|
- run: |
|
||||||
|
cargo hack check --feature-powerset --optional-deps serde,rkyv \
|
||||||
|
--skip default --skip __internal_bench --skip __doctest \
|
||||||
|
--skip iana-time-zone --skip pure-rust-locales
|
||||||
|
# run using `bash` on all platforms for consistent
|
||||||
|
# line-continuation marks
|
||||||
|
shell: bash
|
||||||
- run: cargo test --lib --no-default-features
|
- run: cargo test --lib --no-default-features
|
||||||
- run: cargo test --doc --no-default-features
|
- run: cargo test --doc --no-default-features
|
||||||
|
|
||||||
@ -119,9 +135,12 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
node-version: "12"
|
node-version: "12"
|
||||||
- run: |
|
- run: |
|
||||||
|
set -euxo pipefail
|
||||||
export RUST_BACKTRACE=1
|
export RUST_BACKTRACE=1
|
||||||
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
|
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf \
|
||||||
|
| bash --noprofile --norc
|
||||||
wasm-pack --version
|
wasm-pack --version
|
||||||
|
shell: bash
|
||||||
- run: cargo build --target ${{ matrix.target }} --color=always
|
- run: cargo build --target ${{ matrix.target }} --color=always
|
||||||
|
|
||||||
features_check_wasm:
|
features_check_wasm:
|
||||||
@ -136,7 +155,10 @@ jobs:
|
|||||||
targets: wasm32-unknown-unknown
|
targets: wasm32-unknown-unknown
|
||||||
- uses: taiki-e/install-action@cargo-hack
|
- uses: taiki-e/install-action@cargo-hack
|
||||||
- uses: Swatinem/rust-cache@v2
|
- uses: Swatinem/rust-cache@v2
|
||||||
- run: cargo hack check --feature-powerset --optional-deps serde,rkyv --skip default --skip __internal_bench --skip __doctest --skip iana-time-zone --skip pure-rust-locales
|
- run: |
|
||||||
|
cargo hack check --feature-powerset --optional-deps serde,rkyv \
|
||||||
|
--skip default --skip __internal_bench --skip __doctest \
|
||||||
|
--skip iana-time-zone --skip pure-rust-locales
|
||||||
|
|
||||||
cross-targets:
|
cross-targets:
|
||||||
strategy:
|
strategy:
|
||||||
|
@ -3,8 +3,8 @@ cff-version: 1.2.0
|
|||||||
message: Please cite this crate using these information.
|
message: Please cite this crate using these information.
|
||||||
|
|
||||||
# Version information.
|
# Version information.
|
||||||
date-released: 2023-05-29
|
date-released: 2023-05-31
|
||||||
version: 0.4.25
|
version: 0.4.26
|
||||||
|
|
||||||
# Project information.
|
# Project information.
|
||||||
abstract: Date and time library for Rust
|
abstract: Date and time library for Rust
|
||||||
|
@ -10,7 +10,7 @@ categories = ["date-and-time"]
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
exclude = ["/ci/*"]
|
exclude = ["/ci/*"]
|
||||||
edition = "2018"
|
edition = "2021"
|
||||||
rust-version = "1.56.0"
|
rust-version = "1.56.0"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
@ -51,7 +51,6 @@ android-tzdata = "0.1.1"
|
|||||||
serde_json = { version = "1" }
|
serde_json = { version = "1" }
|
||||||
serde_derive = { version = "1", default-features = false }
|
serde_derive = { version = "1", default-features = false }
|
||||||
bincode = { version = "1.3.0" }
|
bincode = { version = "1.3.0" }
|
||||||
num-iter = { version = "0.1.35", default-features = false }
|
|
||||||
doc-comment = { version = "0.3" }
|
doc-comment = { version = "0.3" }
|
||||||
|
|
||||||
[target.'cfg(all(target_arch = "wasm32", not(any(target_os = "emscripten", target_os = "wasi"))))'.dev-dependencies]
|
[target.'cfg(all(target_arch = "wasm32", not(any(target_os = "emscripten", target_os = "wasi"))))'.dev-dependencies]
|
||||||
|
84
README.md
84
README.md
@ -1,4 +1,4 @@
|
|||||||
[Chrono][docsrs]: Date and Time for Rust
|
[Chrono][docsrs]: Timezone-aware date and time handling
|
||||||
========================================
|
========================================
|
||||||
|
|
||||||
[![Chrono GitHub Actions][gh-image]][gh-checks]
|
[![Chrono GitHub Actions][gh-image]][gh-checks]
|
||||||
@ -15,45 +15,65 @@
|
|||||||
[gitter-image]: https://badges.gitter.im/chrono-rs/chrono.svg
|
[gitter-image]: https://badges.gitter.im/chrono-rs/chrono.svg
|
||||||
[gitter]: https://gitter.im/chrono-rs/chrono
|
[gitter]: https://gitter.im/chrono-rs/chrono
|
||||||
|
|
||||||
It aims to be a feature-complete superset of
|
Chrono aims to provide all functionality needed to do correct operations on dates and times in the
|
||||||
the [time](https://github.com/rust-lang-deprecated/time) library.
|
[proleptic Gregorian calendar](https://en.wikipedia.org/wiki/Proleptic_Gregorian_calendar):
|
||||||
In particular,
|
|
||||||
|
|
||||||
* Chrono strictly adheres to ISO 8601.
|
* The [`DateTime`](https://docs.rs/chrono/latest/chrono/struct.DateTime.html) type is timezone-aware
|
||||||
* Chrono is timezone-aware by default, with separate timezone-naive types.
|
by default, with separate timezone-naive types.
|
||||||
* Chrono is space-optimal and (while not being the primary goal) reasonably efficient.
|
* 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 an `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,
|
Timezone data is not shipped with chrono by default to limit binary sizes. Use the companion crate
|
||||||
which Chrono builds upon and should acknowledge:
|
[Chrono-TZ](https://crates.io/crates/chrono-tz) or [`tzfile`](https://crates.io/crates/tzfile) for
|
||||||
|
full timezone support.
|
||||||
|
|
||||||
* [Initial research on
|
## Documentation
|
||||||
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)
|
See [docs.rs](https://docs.rs/chrono/latest/chrono/) for the API reference.
|
||||||
* Luis de Bethencourt's [rust-datetime](https://github.com/luisbg/rust-datetime)
|
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
|
|
||||||
Only proleptic Gregorian calendar (i.e. extended to support older dates) is supported.
|
* Only the 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.
|
* 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.
|
## Crate features
|
||||||
Time types are limited in the nanosecond accuracy.
|
|
||||||
|
|
||||||
[Leap seconds are supported in the representation but
|
Default features:
|
||||||
Chrono doesn't try to make use of them](https://docs.rs/chrono/0.5/chrono/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.
|
|
||||||
|
|
||||||
Chrono inherently does not support an inaccurate or partial date and time representation.
|
* `alloc`: Enable features that depend on allocation (primarily string formatting)
|
||||||
Any operation that can be ambiguous will return `None` in such cases.
|
* `std`: Enables functionality that depends on the standard library. This is a superset of `alloc`
|
||||||
For example, "a month later" of 2014-01-30 is not well-defined
|
and adds interoperation with standard library types and traits.
|
||||||
and consequently `Utc.ymd_opt(2014, 1, 30).unwrap().with_month(2)` returns `None`.
|
* `clock`: Enables reading the system time (`now`) and local timezone (`Local`).
|
||||||
|
* `wasmbind`: Interface with the JS Date API for the `wasm32` target.
|
||||||
|
|
||||||
Non ISO week handling is not yet supported.
|
Optional features:
|
||||||
For now you can use the [chrono_ext](https://crates.io/crates/chrono_ext)
|
|
||||||
crate ([sources](https://github.com/bcourtine/chrono-ext/)).
|
|
||||||
|
|
||||||
Advanced time zone handling is not yet supported.
|
* `serde`: Enable serialization/deserialization via serde.
|
||||||
For now you can try the [Chrono-tz](https://github.com/chronotope/chrono-tz/) crate instead.
|
* `rkyv`: Enable serialization/deserialization via rkyv.
|
||||||
|
* `rustc-serialize`: Enable serialization/deserialization via rustc-serialize (deprecated).
|
||||||
|
* `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.
|
||||||
|
|
||||||
|
## Rust version requirements
|
||||||
|
|
||||||
|
The Minimum Supported Rust Version (MSRV) is currently **Rust 1.56.0**.
|
||||||
|
|
||||||
|
The MSRV is explicitly tested in CI. It may be bumped in minor releases, but this is not done
|
||||||
|
lightly.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This project is licensed under either of
|
||||||
|
|
||||||
|
* [Apache License, Version 2.0](https://www.apache.org/licenses/LICENSE-2.0)
|
||||||
|
* [MIT License](https://opensource.org/licenses/MIT)
|
||||||
|
|
||||||
|
at your option.
|
||||||
|
@ -866,6 +866,7 @@ fn test_parse() {
|
|||||||
|
|
||||||
// fixed: dot plus nanoseconds
|
// fixed: dot plus nanoseconds
|
||||||
check!("", [fix!(Nanosecond)]; ); // no field set, but not an error
|
check!("", [fix!(Nanosecond)]; ); // no field set, but not an error
|
||||||
|
check!(".", [fix!(Nanosecond)]; TOO_SHORT);
|
||||||
check!("4", [fix!(Nanosecond)]; TOO_LONG); // never consumes `4`
|
check!("4", [fix!(Nanosecond)]; TOO_LONG); // never consumes `4`
|
||||||
check!("4", [fix!(Nanosecond), num!(Second)]; second: 4);
|
check!("4", [fix!(Nanosecond), num!(Second)]; second: 4);
|
||||||
check!(".0", [fix!(Nanosecond)]; nanosecond: 0);
|
check!(".0", [fix!(Nanosecond)]; nanosecond: 0);
|
||||||
@ -888,13 +889,13 @@ fn test_parse() {
|
|||||||
check!(".000000000547", [fix!(Nanosecond)]; nanosecond: 0);
|
check!(".000000000547", [fix!(Nanosecond)]; nanosecond: 0);
|
||||||
check!(".0000000009999999999999999999999999", [fix!(Nanosecond)]; nanosecond: 0);
|
check!(".0000000009999999999999999999999999", [fix!(Nanosecond)]; nanosecond: 0);
|
||||||
check!(".4🤠", [fix!(Nanosecond), lit!("🤠")]; nanosecond: 400_000_000);
|
check!(".4🤠", [fix!(Nanosecond), lit!("🤠")]; nanosecond: 400_000_000);
|
||||||
check!(".", [fix!(Nanosecond)]; TOO_SHORT);
|
|
||||||
check!(".4x", [fix!(Nanosecond)]; TOO_LONG);
|
check!(".4x", [fix!(Nanosecond)]; TOO_LONG);
|
||||||
check!(". 4", [fix!(Nanosecond)]; INVALID);
|
check!(". 4", [fix!(Nanosecond)]; INVALID);
|
||||||
check!(" .4", [fix!(Nanosecond)]; TOO_LONG); // no automatic trimming
|
check!(" .4", [fix!(Nanosecond)]; TOO_LONG); // no automatic trimming
|
||||||
|
|
||||||
// fixed: nanoseconds without the dot
|
// fixed: nanoseconds without the dot
|
||||||
check!("", [internal_fix!(Nanosecond3NoDot)]; TOO_SHORT);
|
check!("", [internal_fix!(Nanosecond3NoDot)]; TOO_SHORT);
|
||||||
|
check!(".", [internal_fix!(Nanosecond3NoDot)]; TOO_SHORT);
|
||||||
check!("0", [internal_fix!(Nanosecond3NoDot)]; TOO_SHORT);
|
check!("0", [internal_fix!(Nanosecond3NoDot)]; TOO_SHORT);
|
||||||
check!("4", [internal_fix!(Nanosecond3NoDot)]; TOO_SHORT);
|
check!("4", [internal_fix!(Nanosecond3NoDot)]; TOO_SHORT);
|
||||||
check!("42", [internal_fix!(Nanosecond3NoDot)]; TOO_SHORT);
|
check!("42", [internal_fix!(Nanosecond3NoDot)]; TOO_SHORT);
|
||||||
@ -910,6 +911,7 @@ fn test_parse() {
|
|||||||
check!(".421", [internal_fix!(Nanosecond3NoDot)]; INVALID);
|
check!(".421", [internal_fix!(Nanosecond3NoDot)]; INVALID);
|
||||||
|
|
||||||
check!("", [internal_fix!(Nanosecond6NoDot)]; TOO_SHORT);
|
check!("", [internal_fix!(Nanosecond6NoDot)]; TOO_SHORT);
|
||||||
|
check!(".", [internal_fix!(Nanosecond6NoDot)]; TOO_SHORT);
|
||||||
check!("0", [internal_fix!(Nanosecond6NoDot)]; TOO_SHORT);
|
check!("0", [internal_fix!(Nanosecond6NoDot)]; TOO_SHORT);
|
||||||
check!("1234", [internal_fix!(Nanosecond6NoDot)]; TOO_SHORT);
|
check!("1234", [internal_fix!(Nanosecond6NoDot)]; TOO_SHORT);
|
||||||
check!("12345", [internal_fix!(Nanosecond6NoDot)]; TOO_SHORT);
|
check!("12345", [internal_fix!(Nanosecond6NoDot)]; TOO_SHORT);
|
||||||
@ -923,6 +925,7 @@ fn test_parse() {
|
|||||||
check!(".42100", [internal_fix!(Nanosecond6NoDot)]; INVALID);
|
check!(".42100", [internal_fix!(Nanosecond6NoDot)]; INVALID);
|
||||||
|
|
||||||
check!("", [internal_fix!(Nanosecond9NoDot)]; TOO_SHORT);
|
check!("", [internal_fix!(Nanosecond9NoDot)]; TOO_SHORT);
|
||||||
|
check!(".", [internal_fix!(Nanosecond9NoDot)]; TOO_SHORT);
|
||||||
check!("42195", [internal_fix!(Nanosecond9NoDot)]; TOO_SHORT);
|
check!("42195", [internal_fix!(Nanosecond9NoDot)]; TOO_SHORT);
|
||||||
check!("12345678", [internal_fix!(Nanosecond9NoDot)]; TOO_SHORT);
|
check!("12345678", [internal_fix!(Nanosecond9NoDot)]; TOO_SHORT);
|
||||||
check!("421950803", [internal_fix!(Nanosecond9NoDot)]; nanosecond: 421_950_803);
|
check!("421950803", [internal_fix!(Nanosecond9NoDot)]; nanosecond: 421_950_803);
|
||||||
|
@ -4,8 +4,6 @@
|
|||||||
//! A collection of parsed date and time items.
|
//! A collection of parsed date and time items.
|
||||||
//! They can be constructed incrementally while being checked for consistency.
|
//! They can be constructed incrementally while being checked for consistency.
|
||||||
|
|
||||||
use core::convert::TryFrom;
|
|
||||||
|
|
||||||
use super::{ParseResult, IMPOSSIBLE, NOT_ENOUGH, OUT_OF_RANGE};
|
use super::{ParseResult, IMPOSSIBLE, NOT_ENOUGH, OUT_OF_RANGE};
|
||||||
use crate::naive::{NaiveDate, NaiveDateTime, NaiveTime};
|
use crate::naive::{NaiveDate, NaiveDateTime, NaiveTime};
|
||||||
use crate::offset::{FixedOffset, LocalResult, Offset, TimeZone};
|
use crate::offset::{FixedOffset, LocalResult, Offset, TimeZone};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use core::{convert::TryFrom, fmt};
|
use core::fmt;
|
||||||
|
|
||||||
#[cfg(feature = "rkyv")]
|
#[cfg(feature = "rkyv")]
|
||||||
use rkyv::{Archive, Deserialize, Serialize};
|
use rkyv::{Archive, Deserialize, Serialize};
|
||||||
@ -12,7 +12,6 @@ use crate::OutOfRange;
|
|||||||
///
|
///
|
||||||
/// It is possible to convert from a date to a month independently
|
/// It is possible to convert from a date to a month independently
|
||||||
/// ```
|
/// ```
|
||||||
/// # use std::convert::TryFrom;
|
|
||||||
/// use chrono::prelude::*;
|
/// use chrono::prelude::*;
|
||||||
/// let date = Utc.with_ymd_and_hms(2019, 10, 28, 9, 10, 11).unwrap();
|
/// let date = Utc.with_ymd_and_hms(2019, 10, 28, 9, 10, 11).unwrap();
|
||||||
/// // `2019-10-28T09:10:11Z`
|
/// // `2019-10-28T09:10:11Z`
|
||||||
@ -311,8 +310,6 @@ mod month_serde {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use core::convert::TryFrom;
|
|
||||||
|
|
||||||
use super::Month;
|
use super::Month;
|
||||||
use crate::{Datelike, OutOfRange, TimeZone, Utc};
|
use crate::{Datelike, OutOfRange, TimeZone, Utc};
|
||||||
|
|
||||||
|
@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
#[cfg(any(feature = "alloc", feature = "std", test))]
|
#[cfg(any(feature = "alloc", feature = "std", test))]
|
||||||
use core::borrow::Borrow;
|
use core::borrow::Borrow;
|
||||||
use core::convert::TryFrom;
|
|
||||||
use core::ops::{Add, AddAssign, RangeInclusive, Sub, SubAssign};
|
use core::ops::{Add, AddAssign, RangeInclusive, Sub, SubAssign};
|
||||||
use core::{fmt, str};
|
use core::{fmt, str};
|
||||||
|
|
||||||
@ -2250,10 +2249,7 @@ mod tests {
|
|||||||
};
|
};
|
||||||
use crate::time_delta::TimeDelta;
|
use crate::time_delta::TimeDelta;
|
||||||
use crate::{Datelike, Weekday};
|
use crate::{Datelike, Weekday};
|
||||||
use std::{
|
use std::{i32, u32};
|
||||||
convert::{TryFrom, TryInto},
|
|
||||||
i32, u32,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn diff_months() {
|
fn diff_months() {
|
||||||
@ -2342,9 +2338,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_readme_doomsday() {
|
fn test_readme_doomsday() {
|
||||||
use num_iter::range_inclusive;
|
for y in NaiveDate::MIN.year()..=NaiveDate::MAX.year() {
|
||||||
|
|
||||||
for y in range_inclusive(NaiveDate::MIN.year(), NaiveDate::MAX.year()) {
|
|
||||||
// even months
|
// even months
|
||||||
let d4 = NaiveDate::from_ymd_opt(y, 4, 4).unwrap();
|
let d4 = NaiveDate::from_ymd_opt(y, 4, 4).unwrap();
|
||||||
let d6 = NaiveDate::from_ymd_opt(y, 6, 6).unwrap();
|
let d6 = NaiveDate::from_ymd_opt(y, 6, 6).unwrap();
|
||||||
|
@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
#[cfg(any(feature = "alloc", feature = "std", test))]
|
#[cfg(any(feature = "alloc", feature = "std", test))]
|
||||||
use core::borrow::Borrow;
|
use core::borrow::Borrow;
|
||||||
use core::convert::TryFrom;
|
|
||||||
use core::fmt::Write;
|
use core::fmt::Write;
|
||||||
use core::ops::{Add, AddAssign, Sub, SubAssign};
|
use core::ops::{Add, AddAssign, Sub, SubAssign};
|
||||||
use core::{fmt, str};
|
use core::{fmt, str};
|
||||||
|
@ -504,8 +504,6 @@ const fn weekday_from_u32_mod7(n: u32) -> Weekday {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use num_iter::range_inclusive;
|
|
||||||
use std::convert::TryFrom;
|
|
||||||
use std::u32;
|
use std::u32;
|
||||||
|
|
||||||
use super::weekday_from_u32_mod7;
|
use super::weekday_from_u32_mod7;
|
||||||
@ -555,7 +553,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_of() {
|
fn test_of() {
|
||||||
fn check(expected: bool, flags: YearFlags, ordinal1: u32, ordinal2: u32) {
|
fn check(expected: bool, flags: YearFlags, ordinal1: u32, ordinal2: u32) {
|
||||||
for ordinal in range_inclusive(ordinal1, ordinal2) {
|
for ordinal in ordinal1..=ordinal2 {
|
||||||
let of = match Of::new(ordinal, flags) {
|
let of = match Of::new(ordinal, flags) {
|
||||||
Some(of) => of,
|
Some(of) => of,
|
||||||
None if !expected => continue,
|
None if !expected => continue,
|
||||||
@ -591,8 +589,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_mdf_valid() {
|
fn test_mdf_valid() {
|
||||||
fn check(expected: bool, flags: YearFlags, month1: u32, day1: u32, month2: u32, day2: u32) {
|
fn check(expected: bool, flags: YearFlags, month1: u32, day1: u32, month2: u32, day2: u32) {
|
||||||
for month in range_inclusive(month1, month2) {
|
for month in month1..=month2 {
|
||||||
for day in range_inclusive(day1, day2) {
|
for day in day1..=day2 {
|
||||||
let mdf = match Mdf::new(month, day, flags) {
|
let mdf = match Mdf::new(month, day, flags) {
|
||||||
Some(mdf) => mdf,
|
Some(mdf) => mdf,
|
||||||
None if !expected => continue,
|
None if !expected => continue,
|
||||||
@ -682,7 +680,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_of_fields() {
|
fn test_of_fields() {
|
||||||
for &flags in FLAGS.iter() {
|
for &flags in FLAGS.iter() {
|
||||||
for ordinal in range_inclusive(1u32, 366) {
|
for ordinal in 1u32..=366 {
|
||||||
if let Some(of) = Of::new(ordinal, flags) {
|
if let Some(of) = Of::new(ordinal, flags) {
|
||||||
assert_eq!(of.ordinal(), ordinal);
|
assert_eq!(of.ordinal(), ordinal);
|
||||||
}
|
}
|
||||||
@ -695,7 +693,7 @@ mod tests {
|
|||||||
fn check(flags: YearFlags, ordinal: u32) {
|
fn check(flags: YearFlags, ordinal: u32) {
|
||||||
let of = Of::new(ordinal, flags).unwrap();
|
let of = Of::new(ordinal, flags).unwrap();
|
||||||
|
|
||||||
for ordinal in range_inclusive(0u32, 1024) {
|
for ordinal in 0u32..=1024 {
|
||||||
let of = of.with_ordinal(ordinal);
|
let of = of.with_ordinal(ordinal);
|
||||||
assert_eq!(of, Of::new(ordinal, flags));
|
assert_eq!(of, Of::new(ordinal, flags));
|
||||||
if let Some(of) = of {
|
if let Some(of) = of {
|
||||||
@ -733,7 +731,7 @@ mod tests {
|
|||||||
|
|
||||||
for &flags in FLAGS.iter() {
|
for &flags in FLAGS.iter() {
|
||||||
let mut prev = Of::new(1, flags).unwrap().weekday();
|
let mut prev = Of::new(1, flags).unwrap().weekday();
|
||||||
for ordinal in range_inclusive(2u32, flags.ndays()) {
|
for ordinal in 2u32..=flags.ndays() {
|
||||||
let of = Of::new(ordinal, flags).unwrap();
|
let of = Of::new(ordinal, flags).unwrap();
|
||||||
let expected = prev.succ();
|
let expected = prev.succ();
|
||||||
assert_eq!(of.weekday(), expected);
|
assert_eq!(of.weekday(), expected);
|
||||||
@ -745,8 +743,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_mdf_fields() {
|
fn test_mdf_fields() {
|
||||||
for &flags in FLAGS.iter() {
|
for &flags in FLAGS.iter() {
|
||||||
for month in range_inclusive(1u32, 12) {
|
for month in 1u32..=12 {
|
||||||
for day in range_inclusive(1u32, 31) {
|
for day in 1u32..31 {
|
||||||
let mdf = match Mdf::new(month, day, flags) {
|
let mdf = match Mdf::new(month, day, flags) {
|
||||||
Some(mdf) => mdf,
|
Some(mdf) => mdf,
|
||||||
None => continue,
|
None => continue,
|
||||||
@ -766,7 +764,7 @@ mod tests {
|
|||||||
fn check(flags: YearFlags, month: u32, day: u32) {
|
fn check(flags: YearFlags, month: u32, day: u32) {
|
||||||
let mdf = Mdf::new(month, day, flags).unwrap();
|
let mdf = Mdf::new(month, day, flags).unwrap();
|
||||||
|
|
||||||
for month in range_inclusive(0u32, 16) {
|
for month in 0u32..=16 {
|
||||||
let mdf = match mdf.with_month(month) {
|
let mdf = match mdf.with_month(month) {
|
||||||
Some(mdf) => mdf,
|
Some(mdf) => mdf,
|
||||||
None if month > 12 => continue,
|
None if month > 12 => continue,
|
||||||
@ -779,7 +777,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
for day in range_inclusive(0u32, 1024) {
|
for day in 0u32..=1024 {
|
||||||
let mdf = match mdf.with_day(day) {
|
let mdf = match mdf.with_day(day) {
|
||||||
Some(mdf) => mdf,
|
Some(mdf) => mdf,
|
||||||
None if day > 31 => continue,
|
None if day > 31 => continue,
|
||||||
@ -822,7 +820,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_of_to_mdf() {
|
fn test_of_to_mdf() {
|
||||||
for i in range_inclusive(0u32, 8192) {
|
for i in 0u32..=8192 {
|
||||||
if let Some(of) = Of(i).validate() {
|
if let Some(of) = Of(i).validate() {
|
||||||
assert!(of.to_mdf().valid());
|
assert!(of.to_mdf().valid());
|
||||||
}
|
}
|
||||||
@ -831,7 +829,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_mdf_to_of() {
|
fn test_mdf_to_of() {
|
||||||
for i in range_inclusive(0u32, 8192) {
|
for i in 0u32..=8192 {
|
||||||
let mdf = Mdf(i);
|
let mdf = Mdf(i);
|
||||||
assert_eq!(mdf.valid(), mdf.to_of().is_some());
|
assert_eq!(mdf.valid(), mdf.to_of().is_some());
|
||||||
}
|
}
|
||||||
@ -839,7 +837,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_of_to_mdf_to_of() {
|
fn test_of_to_mdf_to_of() {
|
||||||
for i in range_inclusive(0u32, 8192) {
|
for i in 0u32..=8192 {
|
||||||
if let Some(of) = Of(i).validate() {
|
if let Some(of) = Of(i).validate() {
|
||||||
assert_eq!(of, of.to_mdf().to_of().unwrap());
|
assert_eq!(of, of.to_mdf().to_of().unwrap());
|
||||||
}
|
}
|
||||||
@ -848,7 +846,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_mdf_to_of_to_mdf() {
|
fn test_mdf_to_of_to_mdf() {
|
||||||
for i in range_inclusive(0u32, 8192) {
|
for i in 0u32..=8192 {
|
||||||
let mdf = Mdf(i);
|
let mdf = Mdf(i);
|
||||||
if mdf.valid() {
|
if mdf.valid() {
|
||||||
assert_eq!(mdf, mdf.to_of().unwrap().to_mdf());
|
assert_eq!(mdf, mdf.to_of().unwrap().to_mdf());
|
||||||
|
25
src/round.rs
25
src/round.rs
@ -178,6 +178,9 @@ where
|
|||||||
T: Timelike + Add<TimeDelta, Output = T> + Sub<TimeDelta, Output = T>,
|
T: Timelike + Add<TimeDelta, Output = T> + Sub<TimeDelta, Output = T>,
|
||||||
{
|
{
|
||||||
if let Some(span) = duration.num_nanoseconds() {
|
if let Some(span) = duration.num_nanoseconds() {
|
||||||
|
if span < 0 {
|
||||||
|
return Err(RoundingError::DurationExceedsLimit);
|
||||||
|
}
|
||||||
if naive.timestamp().abs() > MAX_SECONDS_TIMESTAMP_FOR_NANOS {
|
if naive.timestamp().abs() > MAX_SECONDS_TIMESTAMP_FOR_NANOS {
|
||||||
return Err(RoundingError::TimestampExceedsLimit);
|
return Err(RoundingError::TimestampExceedsLimit);
|
||||||
}
|
}
|
||||||
@ -217,6 +220,9 @@ where
|
|||||||
T: Timelike + Add<TimeDelta, Output = T> + Sub<TimeDelta, Output = T>,
|
T: Timelike + Add<TimeDelta, Output = T> + Sub<TimeDelta, Output = T>,
|
||||||
{
|
{
|
||||||
if let Some(span) = duration.num_nanoseconds() {
|
if let Some(span) = duration.num_nanoseconds() {
|
||||||
|
if span < 0 {
|
||||||
|
return Err(RoundingError::DurationExceedsLimit);
|
||||||
|
}
|
||||||
if naive.timestamp().abs() > MAX_SECONDS_TIMESTAMP_FOR_NANOS {
|
if naive.timestamp().abs() > MAX_SECONDS_TIMESTAMP_FOR_NANOS {
|
||||||
return Err(RoundingError::TimestampExceedsLimit);
|
return Err(RoundingError::TimestampExceedsLimit);
|
||||||
}
|
}
|
||||||
@ -304,10 +310,10 @@ impl std::error::Error for RoundingError {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{DurationRound, SubsecRound, TimeDelta};
|
use super::{DurationRound, RoundingError, SubsecRound, TimeDelta};
|
||||||
use crate::offset::{FixedOffset, TimeZone, Utc};
|
use crate::offset::{FixedOffset, TimeZone, Utc};
|
||||||
use crate::NaiveDate;
|
|
||||||
use crate::Timelike;
|
use crate::Timelike;
|
||||||
|
use crate::{NaiveDate, NaiveDateTime};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_round_subsecs() {
|
fn test_round_subsecs() {
|
||||||
@ -760,4 +766,19 @@ mod tests {
|
|||||||
"1969-12-12 12:10:00 UTC"
|
"1969-12-12 12:10:00 UTC"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn issue1010() {
|
||||||
|
let dt = NaiveDateTime::from_timestamp_opt(-4227854320, 1678774288).unwrap();
|
||||||
|
let span = TimeDelta::microseconds(-7019067213869040);
|
||||||
|
assert_eq!(dt.duration_trunc(span), Err(RoundingError::DurationExceedsLimit));
|
||||||
|
|
||||||
|
let dt = NaiveDateTime::from_timestamp_opt(320041586, 1920103021).unwrap();
|
||||||
|
let span = TimeDelta::nanoseconds(-8923838508697114584);
|
||||||
|
assert_eq!(dt.duration_round(span), Err(RoundingError::DurationExceedsLimit));
|
||||||
|
|
||||||
|
let dt = NaiveDateTime::from_timestamp_opt(-2621440, 0).unwrap();
|
||||||
|
let span = TimeDelta::nanoseconds(-9223372036854771421);
|
||||||
|
assert_eq!(dt.duration_round(span), Err(RoundingError::DurationExceedsLimit));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -219,9 +219,7 @@ mod tests {
|
|||||||
date.ordinal() as i32 + 365 * diff(1) + diff(4) - diff(100) + diff(400)
|
date.ordinal() as i32 + 365 * diff(1) + diff(4) - diff(100) + diff(400)
|
||||||
}
|
}
|
||||||
|
|
||||||
use num_iter::range_inclusive;
|
for year in NaiveDate::MIN.year()..=NaiveDate::MAX.year() {
|
||||||
|
|
||||||
for year in range_inclusive(NaiveDate::MIN.year(), NaiveDate::MAX.year()) {
|
|
||||||
let jan1_year = NaiveDate::from_ymd_opt(year, 1, 1).unwrap();
|
let jan1_year = NaiveDate::from_ymd_opt(year, 1, 1).unwrap();
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
jan1_year.num_days_from_ce(),
|
jan1_year.num_days_from_ce(),
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use core::{convert::TryFrom, fmt};
|
use core::fmt;
|
||||||
|
|
||||||
#[cfg(feature = "rkyv")]
|
#[cfg(feature = "rkyv")]
|
||||||
use rkyv::{Archive, Deserialize, Serialize};
|
use rkyv::{Archive, Deserialize, Serialize};
|
||||||
@ -14,7 +14,6 @@ use crate::OutOfRange;
|
|||||||
/// # Example
|
/// # Example
|
||||||
/// ```
|
/// ```
|
||||||
/// use chrono::Weekday;
|
/// use chrono::Weekday;
|
||||||
/// use std::convert::TryFrom;
|
|
||||||
///
|
///
|
||||||
/// let monday = "Monday".parse::<Weekday>().unwrap();
|
/// let monday = "Monday".parse::<Weekday>().unwrap();
|
||||||
/// assert_eq!(monday, Weekday::Mon);
|
/// assert_eq!(monday, Weekday::Mon);
|
||||||
@ -199,7 +198,6 @@ impl fmt::Debug for ParseWeekdayError {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::Weekday;
|
use super::Weekday;
|
||||||
use std::convert::TryFrom;
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_num_days_from() {
|
fn test_num_days_from() {
|
||||||
|
@ -52,34 +52,68 @@ fn verify_against_date_command_local(path: &'static str, dt: NaiveDateTime) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// path to Unix `date` command. Should work on most Linux and Unixes. Not the
|
||||||
|
/// path for MacOS (/bin/date) which uses a different version of `date` with
|
||||||
|
/// different arguments (so it won't run which is okay).
|
||||||
|
/// for testing only
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[cfg(not(target_os = "aix"))]
|
||||||
|
const DATE_PATH: &'static str = "/usr/bin/date";
|
||||||
|
#[allow(dead_code)]
|
||||||
|
#[cfg(target_os = "aix")]
|
||||||
|
const DATE_PATH: &'static str = "/opt/freeware/bin/date";
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
/// test helper to sanity check the date command behaves as expected
|
||||||
|
/// asserts the command succeeded
|
||||||
|
fn assert_run_date_version() {
|
||||||
|
// note environment variable `LANG`
|
||||||
|
match std::env::var_os("LANG") {
|
||||||
|
Some(lang) => eprintln!("LANG: {:?}", lang),
|
||||||
|
None => eprintln!("LANG not set"),
|
||||||
|
}
|
||||||
|
let out = process::Command::new(DATE_PATH).arg("--version").output().unwrap();
|
||||||
|
let stdout = String::from_utf8(out.stdout).unwrap();
|
||||||
|
let stderr = String::from_utf8(out.stderr).unwrap();
|
||||||
|
// note the `date` binary version
|
||||||
|
eprintln!("command: {:?} --version\nstdout: {:?}\nstderr: {:?}", DATE_PATH, stdout, stderr);
|
||||||
|
assert!(out.status.success(), "command failed: {:?} --version", DATE_PATH);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
fn try_verify_against_date_command() {
|
fn try_verify_against_date_command() {
|
||||||
#[cfg(not(target_os = "aix"))]
|
if !path::Path::new(DATE_PATH).exists() {
|
||||||
let date_path = "/usr/bin/date";
|
eprintln!("date command {:?} not found, skipping", DATE_PATH);
|
||||||
#[cfg(target_os = "aix")]
|
|
||||||
let date_path = "/opt/freeware/bin/date";
|
|
||||||
|
|
||||||
if !path::Path::new(date_path).exists() {
|
|
||||||
// date command not found, skipping
|
|
||||||
// avoid running this on macOS, which has path /bin/date
|
|
||||||
// as the required CLI arguments are not present in the
|
|
||||||
// macOS build.
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
assert_run_date_version();
|
||||||
|
|
||||||
let mut date = NaiveDate::from_ymd_opt(1975, 1, 1).unwrap().and_hms_opt(0, 0, 0).unwrap();
|
let mut date = NaiveDate::from_ymd_opt(1975, 1, 1).unwrap().and_hms_opt(0, 0, 0).unwrap();
|
||||||
|
|
||||||
|
eprintln!(
|
||||||
|
"Run command {:?} for every hour from {} to 2077, skipping some years...",
|
||||||
|
DATE_PATH,
|
||||||
|
date.year()
|
||||||
|
);
|
||||||
|
let mut count: u64 = 0;
|
||||||
|
let mut year_at = date.year();
|
||||||
while date.year() < 2078 {
|
while date.year() < 2078 {
|
||||||
if (1975..=1977).contains(&date.year())
|
if (1975..=1977).contains(&date.year())
|
||||||
|| (2020..=2022).contains(&date.year())
|
|| (2020..=2022).contains(&date.year())
|
||||||
|| (2073..=2077).contains(&date.year())
|
|| (2073..=2077).contains(&date.year())
|
||||||
{
|
{
|
||||||
verify_against_date_command_local(date_path, date);
|
if date.year() != year_at {
|
||||||
|
eprintln!("at year {}...", date.year());
|
||||||
|
year_at = date.year();
|
||||||
|
}
|
||||||
|
verify_against_date_command_local(DATE_PATH, date);
|
||||||
|
count += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
date += chrono::TimeDelta::hours(1);
|
date += chrono::TimeDelta::hours(1);
|
||||||
}
|
}
|
||||||
|
eprintln!("Command {:?} was run {} times", DATE_PATH, count);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
@ -98,6 +132,7 @@ fn verify_against_date_command_format_local(path: &'static str, dt: NaiveDateTim
|
|||||||
// Z%Z - too many ways to represent it, will most likely fail
|
// Z%Z - too many ways to represent it, will most likely fail
|
||||||
|
|
||||||
let output = process::Command::new(path)
|
let output = process::Command::new(path)
|
||||||
|
.env("LANG", "c")
|
||||||
.arg("-d")
|
.arg("-d")
|
||||||
.arg(format!(
|
.arg(format!(
|
||||||
"{}-{:02}-{:02} {:02}:{:02}:{:02}",
|
"{}-{:02}-{:02} {:02}:{:02}:{:02}",
|
||||||
@ -124,15 +159,15 @@ fn verify_against_date_command_format_local(path: &'static str, dt: NaiveDateTim
|
|||||||
#[test]
|
#[test]
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
fn try_verify_against_date_command_format() {
|
fn try_verify_against_date_command_format() {
|
||||||
let date_path = "/usr/bin/date";
|
if !path::Path::new(DATE_PATH).exists() {
|
||||||
|
eprintln!("date command {:?} not found, skipping", DATE_PATH);
|
||||||
if !path::Path::new(date_path).exists() {
|
|
||||||
// date command not found, skipping
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
assert_run_date_version();
|
||||||
|
|
||||||
let mut date = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap().and_hms_opt(12, 11, 13).unwrap();
|
let mut date = NaiveDate::from_ymd_opt(1970, 1, 1).unwrap().and_hms_opt(12, 11, 13).unwrap();
|
||||||
while date.year() < 2008 {
|
while date.year() < 2008 {
|
||||||
verify_against_date_command_format_local(date_path, date);
|
verify_against_date_command_format_local(DATE_PATH, date);
|
||||||
date += chrono::TimeDelta::days(55);
|
date += chrono::TimeDelta::days(55);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user