You've already forked parse_datetime
mirror of
https://github.com/uutils/parse_datetime.git
synced 2026-06-10 16:13:15 -07:00
Compare commits
39 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b83d7a953c | |||
| 4b412e03d2 | |||
| c312311cf0 | |||
| 672a2acbc4 | |||
| 665eb95a13 | |||
| 0371374e7e | |||
| 1129eeaeaf | |||
| 400969043d | |||
| d464f3a2b1 | |||
| 025feb86f8 | |||
| 53ed79bcfc | |||
| d5bd1e8e4a | |||
| 7e9a8b502c | |||
| 7440dd9ecb | |||
| e5b92637b1 | |||
| 16ce783763 | |||
| 62f26a49b3 | |||
| 01687f304d | |||
| 3767622cc3 | |||
| 93f2eafcf7 | |||
| 42233b0c81 | |||
| e9b8f7a43e | |||
| 22da5707a6 | |||
| 2efa5c1d7b | |||
| f7aa4540c9 | |||
| 72cada4740 | |||
| 108ebcb04a | |||
| fe0fd40ea5 | |||
| 30ea3911d7 | |||
| ee4971cc73 | |||
| f6f1257961 | |||
| 82b82df5b9 | |||
| d46a321e82 | |||
| cfcf9c307d | |||
| d3367318eb | |||
| 8c59af1c99 | |||
| 7aeb87d88e | |||
| 65f9eac853 | |||
| 4a1b3f3777 |
@@ -0,0 +1,15 @@
|
||||
name: Security audit
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
jobs:
|
||||
audit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: rustsec/audit-check@v2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
+19
-18
@@ -3,6 +3,7 @@ on: [push, pull_request]
|
||||
name: Basic CI
|
||||
|
||||
env:
|
||||
CARGO_INCREMENTAL: "0"
|
||||
CARGO_TERM_COLOR: always
|
||||
RUST_MIN_SRV: "1.71.1"
|
||||
|
||||
@@ -15,7 +16,6 @@ jobs:
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- run: cargo check
|
||||
|
||||
test:
|
||||
@@ -26,7 +26,6 @@ jobs:
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- run: cargo test
|
||||
|
||||
fmt:
|
||||
@@ -34,8 +33,6 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- run: rustup component add rustfmt
|
||||
- run: cargo fmt --all -- --check
|
||||
|
||||
clippy:
|
||||
@@ -46,8 +43,6 @@ jobs:
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- run: rustup component add clippy
|
||||
- run: cargo clippy --all-targets -- -D warnings
|
||||
|
||||
min_version:
|
||||
@@ -61,7 +56,7 @@ jobs:
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: ${{ env.RUST_MIN_SRV }}
|
||||
- run: cargo test
|
||||
- run: cargo check
|
||||
|
||||
coverage:
|
||||
name: Code Coverage
|
||||
@@ -75,14 +70,14 @@ jobs:
|
||||
- { os: windows-latest , features: windows }
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- name: emulate nightly toolchain
|
||||
run: echo "RUSTC_BOOTSTRAP=1" >> "${GITHUB_ENV}"
|
||||
- name: Initialize workflow variables
|
||||
id: vars
|
||||
shell: bash
|
||||
run: |
|
||||
## VARs setup
|
||||
outputs() { step_id="vars"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo "${var}=${!var}" >> $GITHUB_OUTPUT; done; }
|
||||
# toolchain
|
||||
TOOLCHAIN="nightly" ## default to "nightly" toolchain (required for certain required unstable compiler flags) ## !maint: refactor when stable channel has needed support
|
||||
# * specify gnu-type TOOLCHAIN for windows; `grcov` requires gnu-style code coverage data files
|
||||
case ${{ matrix.job.os }} in windows-*) TOOLCHAIN="$TOOLCHAIN-x86_64-pc-windows-gnu" ;; esac;
|
||||
# * use requested TOOLCHAIN if specified
|
||||
@@ -94,13 +89,12 @@ jobs:
|
||||
outputs CODECOV_FLAGS
|
||||
|
||||
- name: rust toolchain ~ install
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
uses: dtolnay/rust-toolchain@nightly # this job is not reproducible...
|
||||
with:
|
||||
components: llvm-tools-preview
|
||||
- name: Test
|
||||
run: cargo test --no-fail-fast
|
||||
env:
|
||||
CARGO_INCREMENTAL: "0"
|
||||
RUSTC_WRAPPER: ""
|
||||
RUSTFLAGS: "-Cinstrument-coverage -Zcoverage-options=branch -Ccodegen-units=1 -Copt-level=0 -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort"
|
||||
RUSTDOCFLAGS: "-Cpanic=abort"
|
||||
@@ -109,12 +103,12 @@ jobs:
|
||||
id: build_grcov
|
||||
shell: bash
|
||||
run: |
|
||||
git clone https://github.com/mozilla/grcov.git ~/grcov/
|
||||
git clone --depth=1 https://github.com/mozilla/grcov.git ~/grcov/
|
||||
cd ~/grcov
|
||||
# Hardcode the version of crossbeam-epoch. See
|
||||
# https://github.com/uutils/coreutils/issues/3680
|
||||
sed -i -e "s|tempfile =|crossbeam-epoch = \"=0.9.8\"\ntempfile =|" Cargo.toml
|
||||
cargo install --path .
|
||||
cargo install --path . --locked
|
||||
cd -
|
||||
# Uncomment when the upstream issue
|
||||
# https://github.com/mozilla/grcov/issues/849 is fixed
|
||||
@@ -137,7 +131,7 @@ jobs:
|
||||
grcov . --binary-path="${COVERAGE_REPORT_DIR}" --output-type lcov --output-path "${COVERAGE_REPORT_FILE}" --branch --ignore build.rs --ignore "vendor/*" --ignore "/*" --ignore "[a-zA-Z]:/*" --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()" --source-dir .
|
||||
echo "report=${COVERAGE_REPORT_FILE}" >> $GITHUB_OUTPUT
|
||||
- name: Upload coverage results (to Codecov.io)
|
||||
uses: codecov/codecov-action@v5
|
||||
uses: codecov/codecov-action@v7
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
files: ${{ steps.coverage.outputs.report }}
|
||||
@@ -155,13 +149,20 @@ jobs:
|
||||
RUN_FOR: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
- name: emulate nightly toolchain
|
||||
run: echo "RUSTC_BOOTSTRAP=1" >> "${GITHUB_ENV}"
|
||||
- name: Install `cargo-fuzz`
|
||||
run: cargo install cargo-fuzz
|
||||
run: cargo install cargo-fuzz --locked
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Run from_str for XX seconds
|
||||
- name: Run fuzz_parse_datetime for XX seconds
|
||||
shell: bash
|
||||
run: |
|
||||
## Run it
|
||||
cd fuzz
|
||||
cargo +nightly fuzz run fuzz_parse_datetime -- -max_total_time=${{ env.RUN_FOR }} -detect_leaks=0
|
||||
cargo fuzz run fuzz_parse_datetime -- -max_total_time=${{ env.RUN_FOR }} -detect_leaks=0
|
||||
- name: Run fuzz_large_year for XX seconds
|
||||
shell: bash
|
||||
run: |
|
||||
## Run it
|
||||
cd fuzz
|
||||
cargo fuzz run fuzz_large_year -- -max_total_time=${{ env.RUN_FOR }} -detect_leaks=0
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
name: CodSpeed
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
pull_request:
|
||||
# `workflow_dispatch` allows CodSpeed to trigger backtest
|
||||
# performance analysis in order to generate initial data.
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
codspeed:
|
||||
name: Run benchmarks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Setup rust toolchain, cache and cargo-codspeed binary
|
||||
uses: moonrepo/setup-rust@v1
|
||||
with:
|
||||
channel: stable
|
||||
cache-target: release
|
||||
bins: cargo-codspeed
|
||||
|
||||
- name: Build the benchmark target(s)
|
||||
run: cargo codspeed build
|
||||
|
||||
- name: Run the benchmarks
|
||||
uses: CodSpeedHQ/action@v4
|
||||
with:
|
||||
mode: simulation
|
||||
run: cargo codspeed run
|
||||
Generated
+641
-15
File diff suppressed because it is too large
Load Diff
+10
-3
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "parse_datetime"
|
||||
description = "parsing human-readable time strings and converting them to a DateTime"
|
||||
version = "0.14.0"
|
||||
description = "parsing human-readable time strings and converting them to a Zoned datetime"
|
||||
version = "0.15.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/uutils/parse_datetime"
|
||||
@@ -9,9 +9,16 @@ readme = "README.md"
|
||||
rust-version = "1.71.1"
|
||||
|
||||
[dependencies]
|
||||
winnow = "0.7.10"
|
||||
winnow = "1.0.0"
|
||||
num-traits = "0.2.19"
|
||||
jiff = { version = "0.2.15", default-features = false, features = ["tz-system", "tzdb-bundle-platform", "tzdb-zoneinfo"] }
|
||||
|
||||
[dev-dependencies]
|
||||
rstest = "0.26"
|
||||
|
||||
[dev-dependencies.codspeed-criterion-compat]
|
||||
version = "4.0"
|
||||
|
||||
[[bench]]
|
||||
name = "parse_datetime"
|
||||
harness = false
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
[](https://crates.io/crates/parse_datetime)
|
||||
[](https://github.com/uutils/parse_datetime/blob/main/LICENSE)
|
||||
[](https://codecov.io/gh/uutils/parse_datetime)
|
||||
[](https://codspeed.io/uutils/parse_datetime?utm_source=badge)
|
||||
|
||||
A Rust crate for parsing human-readable relative time strings and human-readable datetime strings and converting them to a jiff's `Zoned` object.
|
||||
A Rust crate for parsing human-readable relative time strings and
|
||||
human-readable datetime strings.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -26,25 +28,44 @@ Then, import the crate and use the `parse_datetime_at_date` function:
|
||||
|
||||
```rs
|
||||
use jiff::{ToSpan, Zoned};
|
||||
use parse_datetime::parse_datetime_at_date;
|
||||
use parse_datetime::{parse_datetime_at_date, ParsedDateTime};
|
||||
|
||||
let now = Zoned::now();
|
||||
let after = parse_datetime_at_date(now.clone(), "+3 days");
|
||||
|
||||
assert_eq!(
|
||||
now.checked_add(3.days()).unwrap(),
|
||||
after.unwrap()
|
||||
);
|
||||
match after.unwrap() {
|
||||
ParsedDateTime::InRange(z) => assert_eq!(now.checked_add(3.days()).unwrap(), z),
|
||||
ParsedDateTime::Extended(_) => unreachable!("unexpected for this input"),
|
||||
}
|
||||
```
|
||||
|
||||
For DateTime parsing, import the `parse_datetime` function:
|
||||
|
||||
```rs
|
||||
use jiff::{civil::{date, time} ,Zoned};
|
||||
use parse_datetime::parse_datetime;
|
||||
use parse_datetime::{parse_datetime, ParsedDateTime};
|
||||
|
||||
let dt = parse_datetime("2021-02-14 06:37:47");
|
||||
assert_eq!(dt.unwrap(), Zoned::now().with().date(date(2021, 2, 14)).time(time(6, 37, 47, 0)).build().unwrap());
|
||||
match dt.unwrap() {
|
||||
ParsedDateTime::InRange(z) => assert_eq!(z, Zoned::now().with().date(date(2021, 2, 14)).time(time(6, 37, 47, 0)).build().unwrap()),
|
||||
ParsedDateTime::Extended(_) => unreachable!("unexpected for this input"),
|
||||
}
|
||||
```
|
||||
|
||||
For years beyond jiff's representable range (e.g., year 10000+), the result is an `ExtendedDateTime`:
|
||||
|
||||
```rs
|
||||
use parse_datetime::{parse_datetime, ParsedDateTime};
|
||||
|
||||
let dt = parse_datetime("12000-01-01").unwrap();
|
||||
match dt {
|
||||
ParsedDateTime::Extended(ext) => {
|
||||
assert_eq!(ext.year, 12000);
|
||||
assert_eq!(ext.month, 1);
|
||||
assert_eq!(ext.day, 1);
|
||||
}
|
||||
ParsedDateTime::InRange(_) => unreachable!("year 12000 is out of jiff range"),
|
||||
}
|
||||
```
|
||||
|
||||
### Supported Formats
|
||||
@@ -69,7 +90,9 @@ The `parse_datetime` and `parse_datetime_at_date` functions support absolute dat
|
||||
|
||||
The `parse_datetime` and `parse_datetime_at_date` function return:
|
||||
|
||||
- `Ok(Zoned)` - If the input string can be parsed as a `Zoned` object
|
||||
- `Ok(ParsedDateTime)` - If the input string can be parsed
|
||||
- `ParsedDateTime::InRange(Zoned)` for years supported by `jiff::Zoned`
|
||||
- `ParsedDateTime::Extended(ExtendedDateTime)` for out-of-range years (for example `>9999`)
|
||||
- `Err(ParseDateTimeError::InvalidInput)` - If the input string cannot be parsed
|
||||
|
||||
## Fuzzer
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
use codspeed_criterion_compat::{criterion_group, criterion_main, Criterion};
|
||||
use jiff::Zoned;
|
||||
use parse_datetime::{parse_datetime, parse_datetime_at_date};
|
||||
|
||||
fn bench_iso_datetime(c: &mut Criterion) {
|
||||
c.bench_function("parse_iso_datetime", |b| {
|
||||
b.iter(|| parse_datetime("2021-02-14 06:37:47 +0000"))
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_iso_datetime_t_sep(c: &mut Criterion) {
|
||||
c.bench_function("parse_iso_datetime_t_separator", |b| {
|
||||
b.iter(|| parse_datetime("2021-02-14T22:37:47-0800"))
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_date_only(c: &mut Criterion) {
|
||||
c.bench_function("parse_date_only", |b| {
|
||||
b.iter(|| parse_datetime("1997-01-01"))
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_date_slash_format(c: &mut Criterion) {
|
||||
c.bench_function("parse_date_slash_format", |b| {
|
||||
b.iter(|| parse_datetime("05/07/1987"))
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_epoch_timestamp(c: &mut Criterion) {
|
||||
c.bench_function("parse_epoch_timestamp", |b| {
|
||||
b.iter(|| parse_datetime("@1613371067"))
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_relative_time(c: &mut Criterion) {
|
||||
let now = Zoned::now();
|
||||
c.bench_function("parse_relative_time", |b| {
|
||||
b.iter(|| parse_datetime_at_date(now.clone(), "+3 days"))
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_relative_time_complex(c: &mut Criterion) {
|
||||
let now = Zoned::now();
|
||||
c.bench_function("parse_relative_time_complex", |b| {
|
||||
b.iter(|| parse_datetime_at_date(now.clone(), "1 year 3 months 2 days ago"))
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_relative_keywords(c: &mut Criterion) {
|
||||
c.bench_function("parse_yesterday", |b| {
|
||||
b.iter(|| parse_datetime("yesterday"))
|
||||
});
|
||||
c.bench_function("parse_tomorrow", |b| b.iter(|| parse_datetime("tomorrow")));
|
||||
c.bench_function("parse_now", |b| b.iter(|| parse_datetime("now")));
|
||||
}
|
||||
|
||||
fn bench_weekday(c: &mut Criterion) {
|
||||
c.bench_function("parse_weekday", |b| b.iter(|| parse_datetime("wednesday")));
|
||||
}
|
||||
|
||||
fn bench_timezone_offset(c: &mut Criterion) {
|
||||
c.bench_function("parse_timezone_offset", |b| {
|
||||
b.iter(|| parse_datetime("UTC+07:00"))
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_datetime_with_delta(c: &mut Criterion) {
|
||||
c.bench_function("parse_datetime_with_delta", |b| {
|
||||
b.iter(|| parse_datetime("1997-01-01 00:00:00 +0000 +1 year"))
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_ctime_format(c: &mut Criterion) {
|
||||
c.bench_function("parse_ctime_format", |b| {
|
||||
b.iter(|| parse_datetime("Wed Jan 1 00:00:00 1997"))
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_datetime_with_timezone_name(c: &mut Criterion) {
|
||||
c.bench_function("parse_datetime_with_tz_name", |b| {
|
||||
b.iter(|| parse_datetime("1997-01-19 08:17:48 BRT"))
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_datetime_ending_in_z(c: &mut Criterion) {
|
||||
c.bench_function("parse_datetime_ending_in_z", |b| {
|
||||
b.iter(|| parse_datetime("2023-06-03 12:00:01Z"))
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_invalid_input(c: &mut Criterion) {
|
||||
c.bench_function("parse_invalid_input", |b| {
|
||||
b.iter(|| parse_datetime("NotADate"))
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_extended_year(c: &mut Criterion) {
|
||||
c.bench_function("parse_extended_year", |b| {
|
||||
b.iter(|| parse_datetime("10000-01-01"))
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_extended_year_rollover(c: &mut Criterion) {
|
||||
c.bench_function("parse_extended_year_rollover", |b| {
|
||||
b.iter(|| parse_datetime("9999-12-31 +1 day"))
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_extended_year_relative(c: &mut Criterion) {
|
||||
let base = jiff::civil::DateTime::from(jiff::civil::date(2000, 1, 1))
|
||||
.to_zoned(jiff::tz::TimeZone::UTC)
|
||||
.unwrap();
|
||||
c.bench_function("parse_extended_year_relative", |b| {
|
||||
b.iter(|| parse_datetime_at_date(base.clone(), "10000-01-01 +1 day"))
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_extended_large_year(c: &mut Criterion) {
|
||||
c.bench_function("parse_extended_large_year", |b| {
|
||||
b.iter(|| parse_datetime("999999-06-15"))
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
bench_iso_datetime,
|
||||
bench_iso_datetime_t_sep,
|
||||
bench_date_only,
|
||||
bench_date_slash_format,
|
||||
bench_epoch_timestamp,
|
||||
bench_relative_time,
|
||||
bench_relative_time_complex,
|
||||
bench_relative_keywords,
|
||||
bench_weekday,
|
||||
bench_timezone_offset,
|
||||
bench_datetime_with_delta,
|
||||
bench_ctime_format,
|
||||
bench_datetime_with_timezone_name,
|
||||
bench_datetime_ending_in_z,
|
||||
bench_invalid_input,
|
||||
bench_extended_year,
|
||||
bench_extended_year_rollover,
|
||||
bench_extended_year_relative,
|
||||
bench_extended_large_year,
|
||||
);
|
||||
criterion_main!(benches);
|
||||
Generated
+55
-48
@@ -7,6 +7,9 @@ name = "arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||
dependencies = [
|
||||
"derive_arbitrary",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
@@ -16,9 +19,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.47"
|
||||
version = "1.2.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07"
|
||||
checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
@@ -33,15 +36,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.5"
|
||||
name = "derive_arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
|
||||
checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "fuzz_parse_datetime"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"jiff",
|
||||
"libfuzzer-sys",
|
||||
"parse_datetime",
|
||||
]
|
||||
@@ -60,9 +76,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "jiff"
|
||||
version = "0.2.16"
|
||||
version = "0.2.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35"
|
||||
checksum = "4603d3033e49e2b0e31229fcab20a5d40089c607d975cd9c80551dc69eed9102"
|
||||
dependencies = [
|
||||
"jiff-static",
|
||||
"jiff-tzdb-platform",
|
||||
@@ -70,14 +86,14 @@ dependencies = [
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
"serde_core",
|
||||
"windows-sys",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiff-static"
|
||||
version = "0.2.16"
|
||||
version = "0.2.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69"
|
||||
checksum = "782d32378dddf207193ac91cefb848ad41abb58195c95168e1291227a0832b47"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -86,9 +102,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "jiff-tzdb"
|
||||
version = "0.1.4"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524"
|
||||
checksum = "c900ef84826f1338a557697dc8fc601df9ca9af4ac137c7fb61d4c6f2dfd3076"
|
||||
|
||||
[[package]]
|
||||
name = "jiff-tzdb-platform"
|
||||
@@ -111,15 +127,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.177"
|
||||
version = "0.2.183"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
||||
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
||||
|
||||
[[package]]
|
||||
name = "libfuzzer-sys"
|
||||
version = "0.4.12"
|
||||
version = "0.4.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d"
|
||||
checksum = "a9fd2f41a1cba099f79a0b6b6c35656cf7c03351a7bae8ff0f28f25270f929d2"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"cc",
|
||||
@@ -127,15 +143,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.28"
|
||||
version = "0.4.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
@@ -148,7 +164,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "parse_datetime"
|
||||
version = "0.13.3"
|
||||
version = "0.15.0"
|
||||
dependencies = [
|
||||
"jiff",
|
||||
"num-traits",
|
||||
@@ -157,33 +173,33 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.11.1"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
|
||||
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic-util"
|
||||
version = "0.2.4"
|
||||
version = "0.2.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
|
||||
checksum = "091397be61a01d4be58e7841595bd4bfedb15f1cd54977d79b8271e94ed799a3"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.103"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.42"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@@ -222,9 +238,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.110"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -233,15 +249,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.22"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.1+wasi-0.2.4"
|
||||
version = "1.0.2+wasi-0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
|
||||
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
@@ -252,26 +268,17 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.13"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
|
||||
checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.46.0"
|
||||
version = "0.51.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
|
||||
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
|
||||
|
||||
@@ -8,6 +8,8 @@ cargo-fuzz = true
|
||||
|
||||
[dependencies]
|
||||
libfuzzer-sys = "0.4.7"
|
||||
arbitrary = { version = "1", features = ["derive"] }
|
||||
jiff = "0.2"
|
||||
|
||||
[dependencies.parse_datetime]
|
||||
path = "../"
|
||||
@@ -17,3 +19,9 @@ name = "fuzz_parse_datetime"
|
||||
path = "fuzz_targets/parse_datetime.rs"
|
||||
test = false
|
||||
doc = false
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_large_year"
|
||||
path = "fuzz_targets/large_year.rs"
|
||||
test = false
|
||||
doc = false
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
#![no_main]
|
||||
|
||||
use arbitrary::Arbitrary;
|
||||
use jiff::{civil::DateTime, tz::TimeZone};
|
||||
use libfuzzer_sys::fuzz_target;
|
||||
|
||||
#[derive(Arbitrary, Debug)]
|
||||
struct Input {
|
||||
/// Year for the base date (biased toward boundary years).
|
||||
base_year_selector: u8,
|
||||
/// Year to embed in a constructed large-year input string.
|
||||
input_year: u32,
|
||||
month: u8,
|
||||
day: u8,
|
||||
/// Suffix appended after the constructed date (e.g. relative items).
|
||||
suffix: String,
|
||||
/// Whether to also call parse_datetime (no base).
|
||||
try_no_base: bool,
|
||||
}
|
||||
|
||||
fn base_year(selector: u8) -> i16 {
|
||||
match selector % 6 {
|
||||
0 => 2024,
|
||||
1 => 9998,
|
||||
2 => 9999,
|
||||
3 => 1,
|
||||
4 => 100,
|
||||
_ => (selector as i16) * 40,
|
||||
}
|
||||
}
|
||||
|
||||
fn clamp_year(y: u32) -> u32 {
|
||||
// Focus on the interesting range: 9990..=100_000 and 0..=20_000
|
||||
match y % 4 {
|
||||
0 => 9990 + (y % 20), // near boundary
|
||||
1 => 10000 + (y % 90_000), // large years
|
||||
2 => y % 20_000, // general range
|
||||
_ => 2_147_485_540 + (y % 10), // near GNU_MAX_YEAR
|
||||
}
|
||||
}
|
||||
|
||||
fuzz_target!(|input: Input| {
|
||||
let year = clamp_year(input.input_year);
|
||||
let month = (input.month % 12) + 1;
|
||||
let day = (input.day % 28) + 1;
|
||||
let date_str = format!("{year:04}-{month:02}-{day:02} {}", input.suffix);
|
||||
|
||||
// Test parse_datetime (uses current time as base).
|
||||
if input.try_no_base {
|
||||
let _ = parse_datetime::parse_datetime(&date_str);
|
||||
}
|
||||
|
||||
// Test parse_datetime_at_date with a controlled base.
|
||||
let by = base_year(input.base_year_selector);
|
||||
if let Ok(base) = DateTime::new(by, 1, 1, 0, 0, 0, 0) {
|
||||
if let Ok(base) = base.to_zoned(TimeZone::UTC) {
|
||||
let _ = parse_datetime::parse_datetime_at_date(base, &date_str);
|
||||
}
|
||||
}
|
||||
|
||||
// Also try a bare large year as a pure number.
|
||||
let bare = format!("{year}");
|
||||
let _ = parse_datetime::parse_datetime(&bare);
|
||||
});
|
||||
+757
File diff suppressed because it is too large
Load Diff
+611
-63
File diff suppressed because it is too large
Load Diff
+8
-8
@@ -44,11 +44,11 @@ use super::{
|
||||
pub(crate) struct Date {
|
||||
pub(crate) day: u8,
|
||||
pub(crate) month: u8,
|
||||
pub(crate) year: Option<u16>,
|
||||
pub(crate) year: Option<u32>,
|
||||
}
|
||||
|
||||
impl Date {
|
||||
pub(super) fn with_year(self, year: u16) -> Self {
|
||||
pub(super) fn with_year(self, year: u32) -> Self {
|
||||
Date {
|
||||
day: self.day,
|
||||
month: self.month,
|
||||
@@ -118,12 +118,12 @@ impl TryFrom<Date> for jiff::civil::Date {
|
||||
type Error = &'static str;
|
||||
|
||||
fn try_from(date: Date) -> Result<Self, Self::Error> {
|
||||
jiff::civil::Date::new(
|
||||
date.year.unwrap_or(0) as i16,
|
||||
date.month as i8,
|
||||
date.day as i8,
|
||||
)
|
||||
.map_err(|_| "date is not valid")
|
||||
let year = date.year.unwrap_or(0);
|
||||
let year: i16 = year
|
||||
.try_into()
|
||||
.map_err(|_| "date year is outside the supported range")?;
|
||||
jiff::civil::Date::new(year, date.month as i8, date.day as i8)
|
||||
.map_err(|_| "date is not valid")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -33,7 +33,7 @@ use super::primitive::{dec_uint, plus_or_minus, s};
|
||||
/// - `nanosecond` is always in the range of `0..1_000_000_000`.
|
||||
/// - Negative timestamps are represented by a negative `second` value and a
|
||||
/// positive `nanosecond` value.
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
pub(super) struct Timestamp {
|
||||
second: i64,
|
||||
nanosecond: u32,
|
||||
|
||||
+219
-21
@@ -48,6 +48,7 @@ mod primitive;
|
||||
|
||||
pub(crate) mod error;
|
||||
|
||||
use crate::ParsedDateTime;
|
||||
use jiff::Zoned;
|
||||
use primitive::space;
|
||||
use winnow::{
|
||||
@@ -62,7 +63,6 @@ use error::Error;
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
enum Item {
|
||||
Timestamp(epoch::Timestamp),
|
||||
DateTime(combined::DateTime),
|
||||
Date(date::Date),
|
||||
Time(time::Time),
|
||||
@@ -73,18 +73,21 @@ enum Item {
|
||||
Pure(String),
|
||||
}
|
||||
|
||||
/// Parse a date and time string and build a `Zoned` object. The parsed result
|
||||
/// is resolved against the given base date and time.
|
||||
pub(crate) fn parse_at_date<S: AsRef<str> + Clone>(base: Zoned, input: S) -> Result<Zoned, Error> {
|
||||
/// Parse a date and time string and resolve it against the given base date and
|
||||
/// time, returning a [`ParsedDateTime`] result.
|
||||
pub(crate) fn parse_at_date<S: AsRef<str> + Clone>(
|
||||
base: Zoned,
|
||||
input: S,
|
||||
) -> Result<ParsedDateTime, Error> {
|
||||
match parse(&mut input.as_ref()) {
|
||||
Ok(builder) => builder.set_base(base).build(),
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a date and time string and build a `Zoned` object. The parsed result
|
||||
/// is resolved against the current local date and time.
|
||||
pub(crate) fn parse_at_local<S: AsRef<str> + Clone>(input: S) -> Result<Zoned, Error> {
|
||||
/// Parse a date and time string and resolve it against the current local date
|
||||
/// and time, returning a [`ParsedDateTime`] result.
|
||||
pub(crate) fn parse_at_local<S: AsRef<str> + Clone>(input: S) -> Result<ParsedDateTime, Error> {
|
||||
match parse(&mut input.as_ref()) {
|
||||
Ok(builder) => builder.build(), // the builder uses current local date and time if no base is given.
|
||||
Err(e) => Err(e.into()),
|
||||
@@ -209,12 +212,9 @@ fn parse_timestamp(input: &mut &str) -> ModalResult<DateTimeBuilder> {
|
||||
trace(
|
||||
"parse_timestamp",
|
||||
// Expect exactly one timestamp and then EOF (allowing trailing spaces).
|
||||
terminated(epoch::parse.map(Item::Timestamp), preceded(space, eof)),
|
||||
terminated(epoch::parse, preceded(space, eof)),
|
||||
)
|
||||
.verify_map(|item: Item| match item {
|
||||
Item::Timestamp(ts) => DateTimeBuilder::new().set_timestamp(ts).ok(),
|
||||
_ => None,
|
||||
})
|
||||
.verify_map(|ts| DateTimeBuilder::new().set_timestamp(ts).ok())
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
@@ -277,12 +277,13 @@ fn expect_error(input: &mut &str, reason: &'static str) -> ErrMode<ContextError>
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::ParsedDateTime;
|
||||
use jiff::{civil::DateTime, tz::TimeZone, ToSpan, Zoned};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn at_date(builder: DateTimeBuilder, base: Zoned) -> Zoned {
|
||||
builder.set_base(base).build().unwrap()
|
||||
builder.set_base(base).build().unwrap().expect_in_range()
|
||||
}
|
||||
|
||||
fn at_utc(builder: DateTimeBuilder) -> Zoned {
|
||||
@@ -299,6 +300,22 @@ mod tests {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn assert_extended_datetime(input: &str, base: Zoned, expected: &str) {
|
||||
let parsed = parse_at_date(base, input).unwrap();
|
||||
assert!(
|
||||
matches!(parsed, ParsedDateTime::Extended(_)),
|
||||
"expected extended datetime, got in-range for: {input}"
|
||||
);
|
||||
assert_eq!(parsed.to_string(), expected, "{input}");
|
||||
}
|
||||
|
||||
fn expect_extended_datetime(parsed: ParsedDateTime) -> crate::ExtendedDateTime {
|
||||
match parsed {
|
||||
ParsedDateTime::Extended(dt) => dt,
|
||||
ParsedDateTime::InRange(z) => panic!("expected extended datetime, got in-range: {z}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn date_and_time() {
|
||||
assert_eq!(
|
||||
@@ -408,13 +425,12 @@ mod tests {
|
||||
let result = parse(&mut "2025-05-19 @1690466034");
|
||||
assert!(result.is_err());
|
||||
|
||||
// Pure number as year (too large).
|
||||
// Pure number as year (large years are accepted).
|
||||
let result = parse(&mut "jul 18 12:30 10000");
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("year must be no greater than 9999"));
|
||||
assert!(result.is_ok());
|
||||
let built = result.unwrap().build().unwrap();
|
||||
let dt = expect_extended_datetime(built);
|
||||
assert_eq!(dt.year, 10000);
|
||||
|
||||
// Pure number as time (too long).
|
||||
let result = parse(&mut "01:02 12345");
|
||||
@@ -449,6 +465,184 @@ mod tests {
|
||||
.contains("invalid minute in pure number"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_base_year_with_yearless_date_errors() {
|
||||
let base = DateTime::new(-1, 1, 1, 0, 0, 0, 0)
|
||||
.unwrap()
|
||||
.to_zoned(TimeZone::UTC)
|
||||
.unwrap();
|
||||
let result = parse_at_date(base, "11/14");
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("base year must be non-negative"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn boundary_rollover_from_9999_falls_back_to_extended() {
|
||||
let base = "2000-01-01 00:00:00"
|
||||
.parse::<DateTime>()
|
||||
.unwrap()
|
||||
.to_zoned(TimeZone::UTC)
|
||||
.unwrap();
|
||||
assert_extended_datetime("9999-12-31 +1 day", base, "10000-01-01 00:00:00+00:00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn boundary_year_9999_absolute_date_parses() {
|
||||
let base = "2000-01-01 00:00:00"
|
||||
.parse::<DateTime>()
|
||||
.unwrap()
|
||||
.to_zoned(TimeZone::UTC)
|
||||
.unwrap();
|
||||
assert_extended_datetime("9999-12-31", base, "9999-12-31 00:00:00+00:00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn boundary_year_9999_with_time_parses() {
|
||||
let base = "2000-01-01 00:00:00"
|
||||
.parse::<DateTime>()
|
||||
.unwrap()
|
||||
.to_zoned(TimeZone::UTC)
|
||||
.unwrap();
|
||||
assert_extended_datetime("9999-12-31 12:00", base, "9999-12-31 12:00:00+00:00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn boundary_year_9999_with_utc_timezone_parses() {
|
||||
let base = "2000-01-01 00:00:00"
|
||||
.parse::<DateTime>()
|
||||
.unwrap()
|
||||
.to_zoned(TimeZone::UTC)
|
||||
.unwrap();
|
||||
assert_extended_datetime("9999-12-31 12:00 UTC", base, "9999-12-31 12:00:00+00:00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn boundary_year_9999_with_explicit_offset_parses() {
|
||||
let base = "2000-01-01 00:00:00"
|
||||
.parse::<DateTime>()
|
||||
.unwrap()
|
||||
.to_zoned(TimeZone::UTC)
|
||||
.unwrap();
|
||||
assert_extended_datetime("9999-12-31 12:00 +01:00", base, "9999-12-31 12:00:00+01:00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_year_relative_parity_months_and_years() {
|
||||
let base = "2000-01-01 00:00:00"
|
||||
.parse::<DateTime>()
|
||||
.unwrap()
|
||||
.to_zoned(TimeZone::UTC)
|
||||
.unwrap();
|
||||
|
||||
assert_extended_datetime(
|
||||
"10000-01-31 +2 months",
|
||||
base.clone(),
|
||||
"10000-03-31 00:00:00+00:00",
|
||||
);
|
||||
assert_extended_datetime(
|
||||
"10000-01-31 +3 months",
|
||||
base.clone(),
|
||||
"10000-05-01 00:00:00+00:00",
|
||||
);
|
||||
assert_extended_datetime(
|
||||
"10000-08-31 +6 months",
|
||||
base.clone(),
|
||||
"10001-03-03 00:00:00+00:00",
|
||||
);
|
||||
assert_extended_datetime(
|
||||
"10000-05-31 3 months ago",
|
||||
base.clone(),
|
||||
"10000-03-02 00:00:00+00:00",
|
||||
);
|
||||
assert_extended_datetime("10000-02-29 +1 year", base, "10001-03-01 00:00:00+00:00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_year_above_gnu_max() {
|
||||
assert!(parse_at_local("2147485548-01-01").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_year_can_return_in_range_after_relative() {
|
||||
let base = "2000-01-01 00:00:00"
|
||||
.parse::<DateTime>()
|
||||
.unwrap()
|
||||
.to_zoned(TimeZone::UTC)
|
||||
.unwrap();
|
||||
let result = parse_at_date(base, "10000-01-01 -1000 years").unwrap();
|
||||
let z = result.expect_in_range();
|
||||
assert_eq!(
|
||||
z.strftime("%Y-%m-%d %H:%M:%S%:z").to_string(),
|
||||
"9000-01-01 00:00:00+00:00"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_year_can_return_in_range_after_relative_with_named_timezone_rule() {
|
||||
let base = "2000-01-01 00:00:00"
|
||||
.parse::<DateTime>()
|
||||
.unwrap()
|
||||
.to_zoned(TimeZone::UTC)
|
||||
.unwrap();
|
||||
let result = parse_at_date(base, "TZ=\"Europe/Paris\" 10000-01-01 -1000 years").unwrap();
|
||||
let z = result.expect_in_range();
|
||||
assert_eq!(
|
||||
z.strftime("%Y-%m-%d %H:%M:%S%:z").to_string(),
|
||||
"9000-01-01 00:00:00+01:00"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_year_time_with_explicit_offset_is_extended() {
|
||||
let base = "2000-01-01 00:00:00"
|
||||
.parse::<DateTime>()
|
||||
.unwrap()
|
||||
.to_zoned(TimeZone::UTC)
|
||||
.unwrap();
|
||||
let dt =
|
||||
expect_extended_datetime(parse_at_date(base, "10000-01-01 12:34:56+02:00").unwrap());
|
||||
assert_eq!((dt.year, dt.month, dt.day), (10000, 1, 1));
|
||||
assert_eq!((dt.hour, dt.minute, dt.second), (12, 34, 56));
|
||||
assert_eq!(dt.offset_seconds, 2 * 3600);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_at_date_returns_error_for_invalid_input() {
|
||||
let base = "2000-01-01 00:00:00"
|
||||
.parse::<DateTime>()
|
||||
.unwrap()
|
||||
.to_zoned(TimeZone::UTC)
|
||||
.unwrap();
|
||||
let result = parse_at_date(base, "not-a-date");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "expected extended datetime")]
|
||||
fn assert_extended_datetime_panics_for_in_range_input() {
|
||||
let base = "2000-01-01 00:00:00"
|
||||
.parse::<DateTime>()
|
||||
.unwrap()
|
||||
.to_zoned(TimeZone::UTC)
|
||||
.unwrap();
|
||||
assert_extended_datetime("2001-01-01", base, "2001-01-01 00:00:00+00:00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "ParsedDateTime is not representable as jiff::Zoned")]
|
||||
fn expect_in_range_panics_for_extended_input() {
|
||||
let base = "2000-01-01 00:00:00"
|
||||
.parse::<DateTime>()
|
||||
.unwrap()
|
||||
.to_zoned(TimeZone::UTC)
|
||||
.unwrap();
|
||||
let parsed = parse_at_date(base, "10000-01-01").unwrap();
|
||||
let _ = parsed.expect_in_range();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_weekday() {
|
||||
// Jan 1 2025 is a Wed
|
||||
@@ -563,11 +757,15 @@ mod tests {
|
||||
for (input, expected) in [
|
||||
(
|
||||
r#"TZ="Europe/Paris" 2025-01-02"#,
|
||||
"2025-01-02 00:00:00[Europe/Paris]".parse().unwrap(),
|
||||
"2025-01-02 00:00:00[Europe/Paris]"
|
||||
.parse::<Zoned>()
|
||||
.unwrap(),
|
||||
),
|
||||
(
|
||||
r#"TZ="Europe/Paris" 2025-01-02 03:04:05"#,
|
||||
"2025-01-02 03:04:05[Europe/Paris]".parse().unwrap(),
|
||||
"2025-01-02 03:04:05[Europe/Paris]"
|
||||
.parse::<Zoned>()
|
||||
.unwrap(),
|
||||
),
|
||||
] {
|
||||
assert_eq!(parse_build(input), expected, "{input}");
|
||||
|
||||
@@ -108,6 +108,15 @@ impl Offset {
|
||||
hour_adjustment,
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn total_seconds(&self) -> i32 {
|
||||
let secs = (self.hours as i32) * 3600 + (self.minutes as i32) * 60;
|
||||
if self.negative {
|
||||
-secs
|
||||
} else {
|
||||
secs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<(bool, u8, u8)> for Offset {
|
||||
@@ -465,4 +474,12 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn total_seconds() {
|
||||
assert_eq!(off(false, 0, 0).total_seconds(), 0);
|
||||
assert_eq!(off(false, 5, 30).total_seconds(), 19_800);
|
||||
assert_eq!(off(true, 5, 30).total_seconds(), -19_800);
|
||||
assert_eq!(off(false, 24, 0).total_seconds(), 86_400);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ pub(crate) enum Day {
|
||||
Sunday,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
#[derive(PartialEq, Eq, Debug, Clone)]
|
||||
pub(crate) struct Weekday {
|
||||
pub(crate) offset: i32,
|
||||
pub(crate) day: Day,
|
||||
|
||||
+29
-20
@@ -14,11 +14,12 @@ use winnow::{stream::AsChar, token::take_while, ModalResult, Parser};
|
||||
|
||||
use super::primitive::s;
|
||||
|
||||
const GNU_MAX_YEAR: u32 = 2_147_485_547;
|
||||
// TODO: Leverage `TryFrom` trait.
|
||||
pub(super) fn year_from_str(year_str: &str) -> Result<u16, &'static str> {
|
||||
pub(super) fn year_from_str(year_str: &str) -> Result<u32, &'static str> {
|
||||
let mut year = year_str
|
||||
.parse::<u16>()
|
||||
.map_err(|_| "year must be a valid u16 number")?;
|
||||
.parse::<u32>()
|
||||
.map_err(|_| "year must be a non-negative integer")?;
|
||||
|
||||
// If year is 68 or smaller, then 2000 is added to it; otherwise, if year
|
||||
// is less than 100, then 1900 is added to it.
|
||||
@@ -34,13 +35,8 @@ pub(super) fn year_from_str(year_str: &str) -> Result<u16, &'static str> {
|
||||
}
|
||||
}
|
||||
|
||||
// 2147485547 is the maximum value accepted by GNU, but chrono only
|
||||
// behaves like GNU for years in the range: [0, 9999], so we keep in the
|
||||
// range [0, 9999].
|
||||
//
|
||||
// See discussion in https://github.com/uutils/parse_datetime/issues/160.
|
||||
if year > 9999 {
|
||||
return Err("year must be no greater than 9999");
|
||||
if year > GNU_MAX_YEAR {
|
||||
return Err("year exceeds GNU maximum");
|
||||
}
|
||||
|
||||
Ok(year)
|
||||
@@ -57,18 +53,31 @@ mod tests {
|
||||
#[test]
|
||||
fn test_year() {
|
||||
// 2-characters are converted to 19XX/20XX
|
||||
assert_eq!(year_from_str("10").unwrap(), 2010u16);
|
||||
assert_eq!(year_from_str("68").unwrap(), 2068u16);
|
||||
assert_eq!(year_from_str("69").unwrap(), 1969u16);
|
||||
assert_eq!(year_from_str("99").unwrap(), 1999u16);
|
||||
assert_eq!(year_from_str("10").unwrap(), 2010u32);
|
||||
assert_eq!(year_from_str("68").unwrap(), 2068u32);
|
||||
assert_eq!(year_from_str("69").unwrap(), 1969u32);
|
||||
assert_eq!(year_from_str("99").unwrap(), 1999u32);
|
||||
|
||||
// 3,4-characters are converted verbatim
|
||||
assert_eq!(year_from_str("468").unwrap(), 468u16);
|
||||
assert_eq!(year_from_str("469").unwrap(), 469u16);
|
||||
assert_eq!(year_from_str("1568").unwrap(), 1568u16);
|
||||
assert_eq!(year_from_str("1569").unwrap(), 1569u16);
|
||||
assert_eq!(year_from_str("468").unwrap(), 468u32);
|
||||
assert_eq!(year_from_str("469").unwrap(), 469u32);
|
||||
assert_eq!(year_from_str("1568").unwrap(), 1568u32);
|
||||
assert_eq!(year_from_str("1569").unwrap(), 1569u32);
|
||||
|
||||
// years greater than 9999 are not accepted
|
||||
assert!(year_from_str("10000").is_err());
|
||||
// very large years are accepted up to GNU's upper bound
|
||||
assert_eq!(year_from_str("10000").unwrap(), 10000u32);
|
||||
assert_eq!(year_from_str("2147485547").unwrap(), 2_147_485_547u32);
|
||||
assert_eq!(
|
||||
year_from_str("2147485548").unwrap_err(),
|
||||
"year exceeds GNU maximum"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_year_errors() {
|
||||
assert_eq!(
|
||||
year_from_str("not-a-year").unwrap_err(),
|
||||
"year must be a non-negative integer"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+236
-50
@@ -1,6 +1,7 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
//! A Rust crate for parsing human-readable relative time strings and human-readable datetime strings and converting them to a `DateTime`.
|
||||
//! A Rust crate for parsing human-readable relative time strings and
|
||||
//! human-readable datetime strings.
|
||||
//! The function supports the following formats for time:
|
||||
//!
|
||||
//! * ISO formats
|
||||
@@ -13,8 +14,78 @@ use std::fmt::{self, Display};
|
||||
|
||||
use jiff::Zoned;
|
||||
|
||||
mod extended;
|
||||
mod items;
|
||||
|
||||
pub use extended::{DateParts, ExtendedDateTime, TimeParts};
|
||||
|
||||
/// Maximum year accepted by GNU `date`.
|
||||
pub const GNU_MAX_YEAR: u32 = 2_147_485_547;
|
||||
|
||||
/// Parsed datetime output.
|
||||
///
|
||||
/// - [`ParsedDateTime::InRange`] contains a standard [`jiff::Zoned`] value.
|
||||
/// - [`ParsedDateTime::Extended`] contains an out-of-range year representation
|
||||
/// (for example, years greater than `9999`) that cannot be represented by
|
||||
/// `jiff::Zoned`.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ParsedDateTime {
|
||||
InRange(Zoned),
|
||||
Extended(ExtendedDateTime),
|
||||
}
|
||||
|
||||
impl ParsedDateTime {
|
||||
pub fn as_zoned(&self) -> Option<&Zoned> {
|
||||
match self {
|
||||
ParsedDateTime::InRange(z) => Some(z),
|
||||
ParsedDateTime::Extended(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn into_zoned(self) -> Option<Zoned> {
|
||||
match self {
|
||||
ParsedDateTime::InRange(z) => Some(z),
|
||||
ParsedDateTime::Extended(_) => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Unwraps the `InRange` variant, panicking if this is an `Extended` value.
|
||||
///
|
||||
/// This is a convenience for contexts where the caller is certain the result
|
||||
/// is in range (e.g., tests). Prefer [`into_zoned`](Self::into_zoned) or
|
||||
/// pattern matching in production code.
|
||||
pub fn expect_in_range(self) -> Zoned {
|
||||
self.into_zoned()
|
||||
.expect("ParsedDateTime is not representable as jiff::Zoned")
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ParsedDateTime {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ParsedDateTime::InRange(z) => {
|
||||
let ns = z.datetime().time().subsec_nanosecond();
|
||||
if ns != 0 {
|
||||
write!(f, "{}", z.strftime("%Y-%m-%d %H:%M:%S%.9f%:z"))
|
||||
} else {
|
||||
write!(f, "{}", z.strftime("%Y-%m-%d %H:%M:%S%:z"))
|
||||
}
|
||||
}
|
||||
ParsedDateTime::Extended(dt) => write!(f, "{dt}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An `Extended` value never compares equal to a `Zoned`.
|
||||
impl PartialEq<Zoned> for ParsedDateTime {
|
||||
fn eq(&self, other: &Zoned) -> bool {
|
||||
match self {
|
||||
ParsedDateTime::InRange(z) => z == other,
|
||||
ParsedDateTime::Extended(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum ParseDateTimeError {
|
||||
InvalidInput,
|
||||
@@ -23,12 +94,7 @@ pub enum ParseDateTimeError {
|
||||
impl Display for ParseDateTimeError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ParseDateTimeError::InvalidInput => {
|
||||
write!(
|
||||
f,
|
||||
"Invalid input string: cannot be parsed as a relative time"
|
||||
)
|
||||
}
|
||||
ParseDateTimeError::InvalidInput => write!(f, "Invalid input string"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,8 +107,8 @@ impl From<items::error::Error> for ParseDateTimeError {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses a time string and returns a `Zoned` object representing the absolute
|
||||
/// time of the string.
|
||||
/// Parses a time string and returns a [`ParsedDateTime`] representing the
|
||||
/// absolute time of the string.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
@@ -51,29 +117,34 @@ impl From<items::error::Error> for ParseDateTimeError {
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use jiff::Zoned;
|
||||
/// use parse_datetime::parse_datetime;
|
||||
/// use parse_datetime::{parse_datetime, ParsedDateTime};
|
||||
///
|
||||
/// let time = parse_datetime("2023-06-03 12:00:01Z").unwrap();
|
||||
/// assert_eq!(time.strftime("%F %T").to_string(), "2023-06-03 12:00:01");
|
||||
/// match time {
|
||||
/// ParsedDateTime::InRange(z) => {
|
||||
/// assert_eq!(z.strftime("%F %T").to_string(), "2023-06-03 12:00:01");
|
||||
/// }
|
||||
/// ParsedDateTime::Extended(_) => unreachable!("unexpected for this input"),
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(Zoned)` - If the input string can be parsed as a time
|
||||
/// * `Err(ParseDateTimeError)` - If the input string cannot be parsed as a
|
||||
/// relative time
|
||||
/// * `Ok(ParsedDateTime)` - If the input string can be parsed as a time
|
||||
/// * `Err(ParseDateTimeError)` - If the input string cannot be parsed
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return `Err(ParseDateTimeError::InvalidInput)` if the
|
||||
/// input string cannot be parsed as a relative time.
|
||||
pub fn parse_datetime<S: AsRef<str> + Clone>(input: S) -> Result<Zoned, ParseDateTimeError> {
|
||||
/// input string cannot be parsed.
|
||||
pub fn parse_datetime<S: AsRef<str> + Clone>(
|
||||
input: S,
|
||||
) -> Result<ParsedDateTime, ParseDateTimeError> {
|
||||
items::parse_at_local(input).map_err(|e| e.into())
|
||||
}
|
||||
|
||||
/// Parses a time string at a specific date and returns a `Zoned` object
|
||||
/// Parses a time string at a specific date and returns a [`ParsedDateTime`]
|
||||
/// representing the absolute time of the string.
|
||||
///
|
||||
/// # Arguments
|
||||
@@ -85,31 +156,30 @@ pub fn parse_datetime<S: AsRef<str> + Clone>(input: S) -> Result<Zoned, ParseDat
|
||||
///
|
||||
/// ```
|
||||
/// use jiff::Zoned;
|
||||
/// use parse_datetime::parse_datetime_at_date;
|
||||
/// use parse_datetime::{parse_datetime_at_date, ParsedDateTime};
|
||||
///
|
||||
/// let now = Zoned::now();
|
||||
/// let after = parse_datetime_at_date(now, "2024-09-13UTC +3 days").unwrap();
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// "2024-09-16",
|
||||
/// after.strftime("%F").to_string()
|
||||
/// );
|
||||
/// match after {
|
||||
/// ParsedDateTime::InRange(z) => assert_eq!("2024-09-16", z.strftime("%F").to_string()),
|
||||
/// ParsedDateTime::Extended(_) => unreachable!("unexpected for this input"),
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(Zoned)` - If the input string can be parsed as a time
|
||||
/// * `Err(ParseDateTimeError)` - If the input string cannot be parsed as a
|
||||
/// relative time
|
||||
/// * `Ok(ParsedDateTime)` - If the input string can be parsed as a time
|
||||
/// * `Err(ParseDateTimeError)` - If the input string cannot be parsed
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This function will return `Err(ParseDateTimeError::InvalidInput)` if the
|
||||
/// input string cannot be parsed as a relative time.
|
||||
/// input string cannot be parsed.
|
||||
pub fn parse_datetime_at_date<S: AsRef<str> + Clone>(
|
||||
date: Zoned,
|
||||
input: S,
|
||||
) -> Result<Zoned, ParseDateTimeError> {
|
||||
) -> Result<ParsedDateTime, ParseDateTimeError> {
|
||||
items::parse_at_date(date, input).map_err(|e| e.into())
|
||||
}
|
||||
|
||||
@@ -131,35 +201,35 @@ mod tests {
|
||||
#[test]
|
||||
fn test_t_sep() {
|
||||
let dt = "2021-02-15T06:37:47 +0000";
|
||||
let actual = parse_datetime(dt).unwrap();
|
||||
let actual = parse_datetime(dt).unwrap().expect_in_range();
|
||||
assert_eq!(actual.timestamp().as_second(), TEST_TIME);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_space_sep() {
|
||||
let dt = "2021-02-15 06:37:47 +0000";
|
||||
let actual = parse_datetime(dt).unwrap();
|
||||
let actual = parse_datetime(dt).unwrap().expect_in_range();
|
||||
assert_eq!(actual.timestamp().as_second(), TEST_TIME);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_space_sep_offset() {
|
||||
let dt = "2021-02-14 22:37:47 -0800";
|
||||
let actual = parse_datetime(dt).unwrap();
|
||||
let actual = parse_datetime(dt).unwrap().expect_in_range();
|
||||
assert_eq!(actual.timestamp().as_second(), TEST_TIME);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_t_sep_offset() {
|
||||
let dt = "2021-02-14T22:37:47 -0800";
|
||||
let actual = parse_datetime(dt).unwrap();
|
||||
let actual = parse_datetime(dt).unwrap().expect_in_range();
|
||||
assert_eq!(actual.timestamp().as_second(), TEST_TIME);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_t_sep_single_digit_offset_no_space() {
|
||||
let dt = "2021-02-14T22:37:47-8";
|
||||
let actual = parse_datetime(dt).unwrap();
|
||||
let actual = parse_datetime(dt).unwrap().expect_in_range();
|
||||
assert_eq!(actual.timestamp().as_second(), TEST_TIME);
|
||||
}
|
||||
|
||||
@@ -184,7 +254,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_epoch_seconds() {
|
||||
let dt = "@1613371067";
|
||||
let actual = parse_datetime(dt).unwrap();
|
||||
let actual = parse_datetime(dt).unwrap().expect_in_range();
|
||||
assert_eq!(actual.timestamp().as_second(), TEST_TIME);
|
||||
}
|
||||
|
||||
@@ -215,14 +285,14 @@ mod tests {
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(expected, parse_datetime("1987-05-07").unwrap());
|
||||
assert_eq!(expected, parse_datetime("1987-5-07").unwrap());
|
||||
assert_eq!(expected, parse_datetime("1987-05-7").unwrap());
|
||||
assert_eq!(expected, parse_datetime("1987-5-7").unwrap());
|
||||
assert_eq!(expected, parse_datetime("5/7/1987").unwrap());
|
||||
assert_eq!(expected, parse_datetime("5/07/1987").unwrap());
|
||||
assert_eq!(expected, parse_datetime("05/7/1987").unwrap());
|
||||
assert_eq!(expected, parse_datetime("05/07/1987").unwrap());
|
||||
assert_eq!(parse_datetime("1987-05-07").unwrap(), expected);
|
||||
assert_eq!(parse_datetime("1987-5-07").unwrap(), expected);
|
||||
assert_eq!(parse_datetime("1987-05-7").unwrap(), expected);
|
||||
assert_eq!(parse_datetime("1987-5-7").unwrap(), expected);
|
||||
assert_eq!(parse_datetime("5/7/1987").unwrap(), expected);
|
||||
assert_eq!(parse_datetime("5/07/1987").unwrap(), expected);
|
||||
assert_eq!(parse_datetime("05/7/1987").unwrap(), expected);
|
||||
assert_eq!(parse_datetime("05/07/1987").unwrap(), expected);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,7 +315,7 @@ mod tests {
|
||||
|
||||
let expected = format!("{}{}", Zoned::now().strftime("%Y%m%d"), "0000+0700");
|
||||
for offset in offsets {
|
||||
let actual = parse_datetime(offset).unwrap();
|
||||
let actual = parse_datetime(offset).unwrap().expect_in_range();
|
||||
assert_eq!(expected, actual.strftime("%Y%m%d%H%M%z").to_string());
|
||||
}
|
||||
}
|
||||
@@ -255,7 +325,7 @@ mod tests {
|
||||
let offsets = vec!["UTC+00:15", "UTC+0015", "Z+00:15", "Z+0015"];
|
||||
let expected = format!("{}{}", Zoned::now().strftime("%Y%m%d"), "0000+0015");
|
||||
for offset in offsets {
|
||||
let actual = parse_datetime(offset).unwrap();
|
||||
let actual = parse_datetime(offset).unwrap().expect_in_range();
|
||||
assert_eq!(expected, actual.strftime("%Y%m%d%H%M%z").to_string());
|
||||
}
|
||||
}
|
||||
@@ -352,7 +422,9 @@ mod tests {
|
||||
use crate::parse_datetime_at_date;
|
||||
|
||||
fn get_formatted_date(date: &Zoned, weekday: &str) -> String {
|
||||
let result = parse_datetime_at_date(date.clone(), weekday).unwrap();
|
||||
let result = parse_datetime_at_date(date.clone(), weekday)
|
||||
.unwrap()
|
||||
.expect_in_range();
|
||||
|
||||
result.strftime("%F %T %9f").to_string()
|
||||
}
|
||||
@@ -415,12 +487,16 @@ mod tests {
|
||||
for offset in offsets {
|
||||
// positive offset
|
||||
let time = Timestamp::from_second(offset).unwrap();
|
||||
let dt = parse_datetime(format!("@{offset}")).unwrap();
|
||||
let dt = parse_datetime(format!("@{offset}"))
|
||||
.unwrap()
|
||||
.expect_in_range();
|
||||
assert_eq!(dt.timestamp(), time);
|
||||
|
||||
// negative offset
|
||||
let time = Timestamp::from_second(-offset).unwrap();
|
||||
let dt = parse_datetime(format!("@-{offset}")).unwrap();
|
||||
let dt = parse_datetime(format!("@-{offset}"))
|
||||
.unwrap()
|
||||
.expect_in_range();
|
||||
assert_eq!(dt.timestamp(), time);
|
||||
}
|
||||
}
|
||||
@@ -466,6 +542,24 @@ mod tests {
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parsed_datetime_accessors_cover_both_variants() {
|
||||
let in_range = parse_datetime("2023-06-03 12:00:01Z").unwrap();
|
||||
assert!(in_range.as_zoned().is_some());
|
||||
assert!(in_range.into_zoned().is_some());
|
||||
|
||||
let extended = parse_datetime("10000-01-01").unwrap();
|
||||
assert!(extended.as_zoned().is_none());
|
||||
assert!(extended.into_zoned().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parsed_datetime_extended_never_equals_zoned() {
|
||||
let extended = parse_datetime("10000-01-01").unwrap();
|
||||
let zoned = Zoned::now();
|
||||
assert_ne!(extended, zoned);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_invalid_datetime() {
|
||||
assert!(crate::parse_datetime("bogus +1 day").is_err());
|
||||
@@ -631,6 +725,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
parse_datetime("28 feb + 1 month")
|
||||
.expect("parse_datetime")
|
||||
.expect_in_range()
|
||||
.strftime("%m%d")
|
||||
.to_string(),
|
||||
"0328"
|
||||
@@ -650,6 +745,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
parse_datetime("28 feb 2023 + 1 day")
|
||||
.unwrap()
|
||||
.expect_in_range()
|
||||
.strftime("%m%d")
|
||||
.to_string(),
|
||||
"0301"
|
||||
@@ -661,6 +757,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
parse_datetime("2024-01-31 + 1 month")
|
||||
.unwrap()
|
||||
.expect_in_range()
|
||||
.strftime("%Y-%m-%dT%H:%M:%S")
|
||||
.to_string(),
|
||||
"2024-03-02T00:00:00",
|
||||
@@ -669,6 +766,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
parse_datetime("2024-02-29 + 1 month")
|
||||
.unwrap()
|
||||
.expect_in_range()
|
||||
.strftime("%Y-%m-%dT%H:%M:%S")
|
||||
.to_string(),
|
||||
"2024-03-29T00:00:00",
|
||||
@@ -685,19 +783,107 @@ mod tests {
|
||||
let input = "0000-03-02 00:00:00";
|
||||
assert_eq!(
|
||||
input,
|
||||
parse_datetime(input).unwrap().strftime(FMT).to_string()
|
||||
parse_datetime(input)
|
||||
.unwrap()
|
||||
.expect_in_range()
|
||||
.strftime(FMT)
|
||||
.to_string()
|
||||
);
|
||||
|
||||
let input = "2621-03-10 00:00:00";
|
||||
assert_eq!(
|
||||
input,
|
||||
parse_datetime(input).unwrap().strftime(FMT).to_string()
|
||||
parse_datetime(input)
|
||||
.unwrap()
|
||||
.expect_in_range()
|
||||
.strftime(FMT)
|
||||
.to_string()
|
||||
);
|
||||
|
||||
let input = "1038-03-10 00:00:00";
|
||||
assert_eq!(
|
||||
input,
|
||||
parse_datetime(input).unwrap().strftime(FMT).to_string()
|
||||
parse_datetime(input)
|
||||
.unwrap()
|
||||
.expect_in_range()
|
||||
.strftime(FMT)
|
||||
.to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod display {
|
||||
use jiff::{civil::DateTime, tz::TimeZone};
|
||||
|
||||
use crate::{parse_datetime_at_date, ExtendedDateTime, ParsedDateTime};
|
||||
|
||||
#[test]
|
||||
fn in_range_without_nanoseconds() {
|
||||
let base = "2024-06-15 08:30:00"
|
||||
.parse::<DateTime>()
|
||||
.unwrap()
|
||||
.to_zoned(TimeZone::UTC)
|
||||
.unwrap();
|
||||
let parsed = parse_datetime_at_date(base, "2024-06-15 08:30:00").unwrap();
|
||||
assert_eq!(parsed.to_string(), "2024-06-15 08:30:00+00:00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn in_range_with_nanoseconds() {
|
||||
let base = "2024-06-15 08:30:00.123456789"
|
||||
.parse::<DateTime>()
|
||||
.unwrap()
|
||||
.to_zoned(TimeZone::UTC)
|
||||
.unwrap();
|
||||
// Parsing "now" at a base with subsecond precision preserves it.
|
||||
let parsed = parse_datetime_at_date(base, "now").unwrap();
|
||||
assert_eq!(parsed.to_string(), "2024-06-15 08:30:00.123456789+00:00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extended_without_nanoseconds() {
|
||||
let dt = ExtendedDateTime::new(
|
||||
crate::DateParts {
|
||||
year: 12000,
|
||||
month: 3,
|
||||
day: 15,
|
||||
},
|
||||
crate::TimeParts {
|
||||
hour: 10,
|
||||
minute: 30,
|
||||
second: 45,
|
||||
nanosecond: 0,
|
||||
},
|
||||
3600,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
ParsedDateTime::Extended(dt).to_string(),
|
||||
"12000-03-15 10:30:45+01:00"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extended_with_nanoseconds() {
|
||||
let dt = ExtendedDateTime::new(
|
||||
crate::DateParts {
|
||||
year: 12000,
|
||||
month: 3,
|
||||
day: 15,
|
||||
},
|
||||
crate::TimeParts {
|
||||
hour: 10,
|
||||
minute: 30,
|
||||
second: 45,
|
||||
nanosecond: 123456789,
|
||||
},
|
||||
-18000,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
ParsedDateTime::Extended(dt).to_string(),
|
||||
"12000-03-15 10:30:45.123456789-05:00"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+2
-10
@@ -14,11 +14,7 @@ pub fn check_absolute(input: &str, expected: &str) {
|
||||
Err(e) => panic!("Failed to parse date from value '{input}': {e}"),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
parsed.strftime("%Y-%m-%d %H:%M:%S%:z").to_string(),
|
||||
expected,
|
||||
"Input value: {input}"
|
||||
);
|
||||
assert_eq!(parsed.to_string(), expected, "Input value: {input}");
|
||||
}
|
||||
|
||||
pub fn check_relative(now: Zoned, input: &str, expected: &str) {
|
||||
@@ -29,9 +25,5 @@ pub fn check_relative(now: Zoned, input: &str, expected: &str) {
|
||||
Err(e) => panic!("Failed to parse date from value '{input}': {e}"),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
parsed.strftime("%Y-%m-%d %H:%M:%S%:z").to_string(),
|
||||
expected,
|
||||
"Input value: {input}"
|
||||
);
|
||||
assert_eq!(parsed.to_string(), expected, "Input value: {input}");
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user