mirror of
https://github.com/uutils/parse_datetime.git
synced 2026-10-16 03:25:04 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb1cb02a33 | ||
|
|
52554677dd | ||
|
|
516a3a934d | ||
|
|
1b014a15a3 | ||
|
|
bf5cf43375 | ||
|
|
edb83cceba | ||
|
|
a3b8d128fa | ||
|
|
e717803a5e | ||
|
|
b6c2256daf | ||
|
|
3355829f3d | ||
|
|
80556965f3 | ||
|
|
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 |
No files matched your search
@@ -0,0 +1,15 @@
|
||||
name: Security audit
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 0 * * *"
|
||||
jobs:
|
||||
audit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: rustsec/audit-check@v2
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
+26
-25
@@ -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"
|
||||
|
||||
@@ -14,8 +15,7 @@ jobs:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: actions/checkout@v7
|
||||
- run: cargo check
|
||||
|
||||
test:
|
||||
@@ -25,17 +25,14 @@ jobs:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- uses: actions/checkout@v7
|
||||
- run: cargo test
|
||||
|
||||
fmt:
|
||||
name: cargo fmt --all -- --check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- run: rustup component add rustfmt
|
||||
- uses: actions/checkout@v7
|
||||
- run: cargo fmt --all -- --check
|
||||
|
||||
clippy:
|
||||
@@ -45,9 +42,7 @@ jobs:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- run: rustup component add clippy
|
||||
- uses: actions/checkout@v7
|
||||
- run: cargo clippy --all-targets -- -D warnings
|
||||
|
||||
min_version:
|
||||
@@ -57,11 +52,11 @@ jobs:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: ${{ env.RUST_MIN_SRV }}
|
||||
- run: cargo test
|
||||
- run: cargo check
|
||||
|
||||
coverage:
|
||||
name: Code Coverage
|
||||
@@ -74,15 +69,15 @@ jobs:
|
||||
- { os: macos-latest , features: macos }
|
||||
- { os: windows-latest , features: windows }
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
- 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 }}
|
||||
@@ -154,14 +148,21 @@ jobs:
|
||||
env:
|
||||
RUN_FOR: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
- uses: actions/checkout@v7
|
||||
- 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@v7
|
||||
|
||||
- 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
+742
-79
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 = "5.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
+140
-52
@@ -7,18 +7,27 @@ name = "arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
|
||||
dependencies = [
|
||||
"derive_arbitrary",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.0"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "1.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.47"
|
||||
version = "1.2.62"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd405d82c84ff7f35739f175f67d8b9fb7687a0e84ccdc78bd3568839827cf07"
|
||||
checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"jobserver",
|
||||
@@ -33,15 +42,60 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.5"
|
||||
name = "defmt"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844"
|
||||
checksum = "a6e524506490a1953d237cb87b1cfc1e46f88c18f10a22dfe0f507dc6bfc7f7f"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"defmt-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "defmt-macros"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0a27770e9c8f719a79d8b638281f4d828f77d8fd61e0bd94451b9b85e576a0b"
|
||||
dependencies = [
|
||||
"defmt-parser",
|
||||
"proc-macro-error2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "defmt-parser"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10d60334b3b2e7c9d91ef8150abfb6fa4c1c39ebbcf4a81c2e346aad939fee3e"
|
||||
dependencies = [
|
||||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_arbitrary"
|
||||
version = "1.4.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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,24 +114,25 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "jiff"
|
||||
version = "0.2.16"
|
||||
version = "0.2.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49cce2b81f2098e7e3efc35bc2e0a6b7abec9d34128283d7a26fa8f32a6dbb35"
|
||||
checksum = "ccfe6121cbe750cf81efa362d85c0bde7ea298ec43092d3a193baca59cdbd634"
|
||||
dependencies = [
|
||||
"defmt",
|
||||
"jiff-static",
|
||||
"jiff-tzdb-platform",
|
||||
"log",
|
||||
"portable-atomic",
|
||||
"portable-atomic-util",
|
||||
"serde_core",
|
||||
"windows-sys",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiff-static"
|
||||
version = "0.2.16"
|
||||
version = "0.2.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69"
|
||||
checksum = "e165e897f662d428f3cd3828a919dbe067c2d42bb1031eede74ef9d27ecdedd2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -86,9 +141,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 +166,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.177"
|
||||
version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[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 +182,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 +203,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "parse_datetime"
|
||||
version = "0.13.3"
|
||||
version = "0.15.0"
|
||||
dependencies = [
|
||||
"jiff",
|
||||
"num-traits",
|
||||
@@ -157,33 +212,55 @@ 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.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
|
||||
checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618"
|
||||
dependencies = [
|
||||
"portable-atomic",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.103"
|
||||
name = "proc-macro-error-attr2"
|
||||
version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8"
|
||||
checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro-error2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
|
||||
dependencies = [
|
||||
"proc-macro-error-attr2",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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 +299,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",
|
||||
@@ -232,16 +309,36 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.22"
|
||||
name = "thiserror"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "2.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "wasip2"
|
||||
version = "1.0.1+wasi-0.2.4"
|
||||
version = "1.0.3+wasi-0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
|
||||
checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
@@ -252,26 +349,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.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
|
||||
checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.46.0"
|
||||
version = "0.57.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
|
||||
checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
||||
@@ -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
+53
-8
@@ -16,7 +16,7 @@
|
||||
//! > discarded.
|
||||
use winnow::{
|
||||
combinator::{alt, trace},
|
||||
seq, ModalResult, Parser,
|
||||
ModalResult, Parser,
|
||||
};
|
||||
|
||||
use crate::items::space;
|
||||
@@ -29,14 +29,32 @@ pub(crate) struct DateTime {
|
||||
pub(crate) time: time::Time,
|
||||
}
|
||||
|
||||
fn remaining_starts_with_meridiem(input: &str) -> bool {
|
||||
let trimmed = input.trim_start();
|
||||
trimmed.starts_with("am")
|
||||
|| trimmed.starts_with("pm")
|
||||
|| trimmed.starts_with("a.m.")
|
||||
|| trimmed.starts_with("p.m.")
|
||||
}
|
||||
|
||||
pub(crate) fn parse(input: &mut &str) -> ModalResult<DateTime> {
|
||||
seq!(DateTime {
|
||||
date: trace("iso_date", alt((date::iso1, date::iso2))),
|
||||
// Note: the `T` is lowercased by the main parse function
|
||||
_: alt((s('t').void(), (' ', space).void())),
|
||||
time: trace("iso_time", time::iso),
|
||||
})
|
||||
.parse_next(input)
|
||||
let date = trace("iso_date", alt((date::iso1, date::iso2))).parse_next(input)?;
|
||||
// Note: the `T` is lowercased by the main parse function
|
||||
alt((s('t').void(), (' ', space).void())).parse_next(input)?;
|
||||
|
||||
let mut iso_input = *input;
|
||||
if let Ok(parsed_time) = trace("iso_time", time::iso).parse_next(&mut iso_input) {
|
||||
if !remaining_starts_with_meridiem(iso_input) {
|
||||
*input = iso_input;
|
||||
return Ok(DateTime {
|
||||
date,
|
||||
time: parsed_time,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let time = trace("iso_time", time::parse).parse_next(input)?;
|
||||
Ok(DateTime { date, time })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -73,4 +91,31 @@ mod tests {
|
||||
assert_eq!(parse(&mut s).ok(), reference, "Failed string: {old_s}")
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn date_and_time_ampm() {
|
||||
let reference = Some(DateTime {
|
||||
date: Date {
|
||||
day: 15,
|
||||
month: 6,
|
||||
year: Some(2024),
|
||||
},
|
||||
time: Time {
|
||||
hour: 15,
|
||||
minute: 0,
|
||||
second: 0,
|
||||
nanosecond: 0,
|
||||
offset: None,
|
||||
},
|
||||
});
|
||||
|
||||
for mut s in [
|
||||
"2024-06-15 3:00 pm",
|
||||
"2024-06-15 3:00pm",
|
||||
"2024-06-15 3:00 p.m.",
|
||||
] {
|
||||
let old_s = s.to_owned();
|
||||
assert_eq!(parse(&mut s).ok(), reference, "Failed string: {old_s}");
|
||||
}
|
||||
}
|
||||
}
|
||||
+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,
|
||||
|
||||
+227
-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!(
|
||||
@@ -360,6 +377,14 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn date_and_time_ampm() {
|
||||
// https://github.com/uutils/parse_datetime/issues/282
|
||||
assert_eq!("12:00", test_eq_fmt("%H:%M", "2024-06-15 12:00 PM"));
|
||||
assert_eq!("11:30", test_eq_fmt("%H:%M", "2024-06-15 11:30am"));
|
||||
assert_eq!("15:00", test_eq_fmt("%H:%M", "2024-06-15 3:00 PM"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
let result = parse(&mut "");
|
||||
@@ -408,13 +433,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 +473,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 +765,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 {
|
||||
@@ -272,6 +281,7 @@ fn timezone_name_to_offset(input: &str) -> ModalResult<Offset> {
|
||||
"w" => Ok("-10"),
|
||||
"v" => Ok("-9"),
|
||||
"utc" => Ok("+0"),
|
||||
"ut" => Ok("+0"),
|
||||
"u" => Ok("-8"),
|
||||
"t" => Ok("-7"),
|
||||
"sst" => Ok("-11"),
|
||||
@@ -414,6 +424,7 @@ mod tests {
|
||||
fn timezone_name_without_offset() {
|
||||
for (input, expected) in [
|
||||
("utc", off(false, 0, 0)), // UTC
|
||||
("ut", off(false, 0, 0)), // Universal Time = UTC (issue #280)
|
||||
("gmt", off(false, 0, 0)), // UTC
|
||||
("z", off(false, 0, 0)), // UTC
|
||||
("west", off(false, 1, 0)), // positive offset
|
||||
@@ -465,4 +476,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"
|
||||
);
|
||||
}
|
||||
}
|
||||
+404
-50
File diff suppressed because it is too large.
Load diff
Loaded 20 of 24 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user