67 Commits

Author SHA1 Message Date
Sylvestre Ledru 7440dd9ecb docs: update crate description to reflect Zoned return type
The crate now returns jiff Zoned objects, not DateTime.
2026-04-01 10:42:09 +02:00
renovate[bot] e5b92637b1 chore(deps): update moonrepo/setup-rust action to v1 2026-04-01 10:26:37 +02:00
Sylvestre Ledru 16ce783763 Add assertions for month and day in date parsing test 2026-04-01 10:26:21 +02:00
Sylvestre Ledru 62f26a49b3 tests: cover Display fractional-second formatting 2026-04-01 10:26:21 +02:00
Sylvestre Ledru 01687f304d Display: preserve fractional seconds when present
When nanoseconds are non-zero, include them in Display output as
`.NNNNNNNNN` between seconds and the offset. This avoids silently
dropping sub-second precision from the formatted representation.
2026-04-01 10:26:21 +02:00
Sylvestre Ledru 3767622cc3 ci: add fuzz_large_year target to CI workflow 2026-04-01 10:26:21 +02:00
Sylvestre Ledru 93f2eafcf7 docs: add ExtendedDateTime usage example to README 2026-04-01 10:26:21 +02:00
Sylvestre Ledru 42233b0c81 Add large-year coverage to fuzz target
Add a dedicated fuzz_large_year target with structured inputs biased
toward large years (near 9999 boundary, up to GNU_MAX_YEAR). Keep the
original fuzz_parse_datetime target unchanged for raw-bytes fuzzing.
2026-04-01 10:26:21 +02:00
Sylvestre Ledru e9b8f7a43e Deduplicate test helpers using Display and expect_in_range
Replace hand-rolled format_offset_colon / format_for_assert helpers
with the new Display impls. Replace local expect_in_range_datetime
test helpers with ParsedDateTime::expect_in_range().
2026-04-01 10:26:21 +02:00
Sylvestre Ledru 22da5707a6 Remove Item::Timestamp enum variant
The Timestamp variant was #[cfg(test)]-gated dead code outside tests.
The parse_timestamp function already calls set_timestamp on the builder
directly, bypassing the TryFrom<Vec<Item>> path. Remove the variant and
refactor builder tests to exercise set_timestamp directly instead.
2026-04-01 10:26:21 +02:00
Sylvestre Ledru 2efa5c1d7b Add doc comments on expect_in_range and PartialEq<Zoned>
Document that expect_in_range is intended for tests/trusted contexts
and that Extended values never compare equal to a Zoned.
2026-04-01 10:26:21 +02:00
Sylvestre Ledru f7aa4540c9 Add Display impls for ExtendedDateTime and ParsedDateTime
Provide a standard formatting path so callers (and tests) can use
`.to_string()` instead of hand-rolling offset formatting everywhere.
2026-04-01 10:26:21 +02:00
renovate[bot] 72cada4740 chore(deps): update rust crate codspeed-criterion-compat to v4 2026-04-01 10:22:53 +02:00
Daniel Hofstetter 108ebcb04a ci: use "cargo check" for MSRV check (#278) 2026-04-01 09:56:10 +02:00
Sylvestre Ledru fe0fd40ea5 Add benchmarks for extended year / ParsedDateTime API (#276)
Cover the extended year functionality introduced in #263:
- Basic extended year parsing (year 10000)
- Rollover from in-range to extended (9999-12-31 +1 day)
- Relative parsing with extended year base date
- Large year parsing (year 999999)
2026-04-01 08:01:02 +02:00
codspeed-hq[bot] 30ea3911d7 Add CodSpeed performance benchmarks (#273)
Co-authored-by: codspeed-hq[bot] <117304815+codspeed-hq[bot]@users.noreply.github.com>
2026-03-31 23:48:09 +02:00
abhishekpradhan ee4971cc73 Add extended runtime + ParsedDateTime API for large years (#263) 2026-03-31 09:51:07 +02:00
oech3 f6f1257961 ci: simplify, reproducible and fast setup 2026-03-31 09:32:44 +02:00
renovate[bot] 82b82df5b9 chore(deps): update rust crate winnow to v1.0.1 2026-03-30 15:57:54 +02:00
renovate[bot] d46a321e82 chore(deps): update codecov/codecov-action action to v6 2026-03-27 07:23:15 +01:00
renovate[bot] cfcf9c307d fix(deps): update rust crate winnow to v1 2026-03-18 07:07:44 +01:00
renovate[bot] d3367318eb chore(deps): update rust crate winnow to v0.7.15 2026-03-06 07:20:24 +01:00
renovate[bot] 8c59af1c99 chore(deps): update rust crate jiff to v0.2.23 2026-03-04 07:03:13 +01:00
renovate[bot] 7aeb87d88e chore(deps): update rust crate jiff to v0.2.22 2026-03-01 10:43:59 +01:00
renovate[bot] 65f9eac853 chore(deps): update rust crate jiff to v0.2.21 2026-02-23 07:15:17 +01:00
abhishekpradhan 4a1b3f3777 Groundwork: widen year parsing bounds for GNU max year (#261)
---------

Co-authored-by: Sylvestre Ledru <sylvestre@debian.org>
2026-02-22 17:17:44 +01:00
Sylvestre Ledru f68a01699e prepare release 0.14.0 2026-02-14 18:55:26 +01:00
Sylvestre Ledru 73b4b0a50c improve the code coverage 2026-02-14 18:19:31 +01:00
Sylvestre Ledru 002ca41008 tests: add edge case tests for 0 years and 0 months
Add tests to verify that adding 0 years or 0 months returns the
original date unchanged, including edge cases with Feb 29.
2026-02-14 18:19:31 +01:00
Sylvestre Ledru 818cfa4ad0 date: add more tests for cross-TZ-mishandled test - embedded timezone
NOTE: This commit only adds tests as the implementation was already
working correctly. These tests document and verify the expected
behavior for the embedded timezone edge case referenced in Debian
bug #851934.
2026-02-14 18:19:31 +01:00
Sylvestre Ledru 5cf8a6fbdd date: add more tests leap year overflow 2026-02-14 18:19:31 +01:00
Sylvestre Ledru 9688db08a8 Fix the coverage (#260) 2026-02-14 17:53:31 +01:00
cerdelen f10a950e23 parse_datetime: better variable name 2026-02-14 11:21:39 +01:00
cerdelen 14bbd4e848 parse_datetime: relative Year overflow for leap years 2026-02-14 11:21:39 +01:00
cerdelen dbfdea454f parse_datetime: Remove unnessecary import 2026-02-14 11:21:39 +01:00
cerdelen 7e528ad609 parse_datetime: Implement GNU style overflow/underflow
In GNU if using date with relative Months with the -d flag and the target Month does not have the source day (for example 31) GNU overflows the diff in days.
2026-02-14 11:21:39 +01:00
cerdelen 6306843888 parse_datetime: add regression tests 2026-02-14 11:21:39 +01:00
cerdelen d5c441bd3f parse_datetime: fix overflow issue 2026-02-14 11:21:39 +01:00
cerdelen 62cb7d9687 parse_datetime: Fix relative date adjustments for months using the actual days 2026-02-14 11:21:39 +01:00
Chris Dryden 642bbbf214 Merge pull request #255 from ChrisDryden/fix-tz-prefix-with-base-date
fix: TZ prefix should override base date timezone
2026-02-12 09:29:36 +01:00
renovate[bot] 6396c07d72 chore(deps): update rust crate jiff to v0.2.20 2026-02-12 07:06:17 +01:00
renovate[bot] 377ba0e040 chore(deps): update rust crate libfuzzer-sys to v0.4.12 2026-02-11 07:03:20 +01:00
Daniel Hofstetter ffb1ba35c5 Merge pull request #256 from uutils/renovate/jiff-0.x-lockfile
chore(deps): update rust crate jiff to v0.2.19
2026-02-06 07:52:19 +01:00
renovate[bot] eeffa242a1 chore(deps): update rust crate jiff to v0.2.19 2026-02-06 00:33:10 +00:00
Daniel Hofstetter 3a1edfb177 Merge pull request #251 from uutils/renovate/jiff-0.x-lockfile
chore(deps): update rust crate jiff to v0.2.18
2026-01-06 07:09:04 +01:00
renovate[bot] 866b6b446f chore(deps): update rust crate jiff to v0.2.18 2026-01-06 05:15:24 +00:00
Daniel Hofstetter 13e038a503 Merge pull request #250 from uutils/renovate/jiff-0.x-lockfile
chore(deps): update rust crate jiff to v0.2.17
2025-12-25 07:09:32 +01:00
renovate[bot] 7030df6cf0 chore(deps): update rust crate jiff to v0.2.17 2025-12-24 21:35:09 +00:00
Daniel Hofstetter cfbb973215 Merge pull request #249 from uutils/renovate/winnow-0.x-lockfile
chore(deps): update rust crate winnow to v0.7.14
2025-11-27 06:53:55 +01:00
renovate[bot] 88a4a718b0 chore(deps): update rust crate winnow to v0.7.14 2025-11-26 19:44:53 +00:00
Daniel Hofstetter 6462d410d8 Merge pull request #248 from cakebaker/bump_version_to_0_13_3
chore: bump version to `0.13.3`
2025-11-21 10:12:43 +01:00
Daniel Hofstetter 92d095b53b chore: bump version to 0.13.3 2025-11-21 09:52:19 +01:00
Daniel Hofstetter ea143af391 Merge pull request #247 from cakebaker/bump_dependencies
chore: update dependencies
2025-11-21 09:49:15 +01:00
Daniel Hofstetter 835e46e6f1 chore: update dependencies 2025-11-21 09:44:58 +01:00
Daniel Hofstetter 31b8e58cc5 Merge pull request #246 from 3v1n0/fix-parsing-relative-times
relative: Support parsing floating relative values with spaces
2025-11-21 09:16:18 +01:00
Daniel Hofstetter 9de30f5fc3 Merge pull request #245 from uutils/renovate/actions-checkout-6.x
chore(deps): update actions/checkout action to v6
2025-11-21 06:58:39 +01:00
Marco Trevisan (Treviño) d6ab892c29 relative: Add more tests for parsing signed values with spaces 2025-11-21 06:37:09 +01:00
Marco Trevisan (Treviño) 1e4fc30f7b relative: Support parsing floating relative values with spaces
In case a string such as "now + 1.5 seconds" was parsed we were failing.

This happened because after processing now, the parser was getting
to the point in which the string contained "+ 1.5", and once the sign
was processed, the remaining " 1.5" string conained a space that was
causing sec_and_nsec to fail.

Instead of failing at this point, just strip the spaces after the sign has
been processed.

Note in fact that "0+0.0 seconds" was working fine

Closes: https://github.com/uutils/coreutils/issues/8618
2025-11-21 06:37:09 +01:00
renovate[bot] 6ce151fb58 chore(deps): update actions/checkout action to v6 2025-11-20 18:43:38 +00:00
Daniel Hofstetter 9f6605f20d Merge pull request #244 from cakebaker/bump_version_to_0_13_2
chore: bump version to `0.13.2`
2025-11-09 18:27:31 +01:00
Daniel Hofstetter a025a04d72 chore: bump version to 0.13.2 2025-11-09 17:49:27 +01:00
Sylvestre Ledru dbae631ff0 Merge pull request #241 from cakebaker/literal3
feat: support formats `Nov-14-2024` and `November-14-2024`
2025-11-09 15:03:15 +01:00
Daniel Hofstetter 83278f0ddc Merge pull request #243 from uutils/renovate/jiff-0.x-lockfile
chore(deps): update rust crate jiff to v0.2.16
2025-11-07 17:39:07 +01:00
renovate[bot] ae518cac37 chore(deps): update rust crate jiff to v0.2.16 2025-11-07 15:41:39 +00:00
Daniel Hofstetter bc5d201b68 Merge pull request #242 from cakebaker/update_dependencies
chore: update dependencies
2025-10-30 09:29:46 +01:00
Daniel Hofstetter 8c56c5cb13 chore: update dependencies 2025-10-29 11:13:04 +01:00
Daniel Hofstetter 9c95c9549b feat: support formats Nov-14-2024/November-14-2024 2025-10-29 10:40:22 +01:00
24 changed files with 3507 additions and 398 deletions
+30 -27
View File
@@ -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@v5
- uses: dtolnay/rust-toolchain@stable
- uses: actions/checkout@v6
- run: cargo check
test:
@@ -25,17 +25,14 @@ jobs:
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
- uses: actions/checkout@v6
- run: cargo test
fmt:
name: cargo fmt --all -- --check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
- run: rustup component add rustfmt
- uses: actions/checkout@v6
- run: cargo fmt --all -- --check
clippy:
@@ -45,9 +42,7 @@ jobs:
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
- run: rustup component add clippy
- uses: actions/checkout@v6
- 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@v5
- uses: actions/checkout@v6
- 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@v5
- 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
@@ -134,17 +128,19 @@ jobs:
# display coverage files
grcov . --binary-path="${COVERAGE_REPORT_DIR}" --output-type files --ignore build.rs --ignore "vendor/*" --ignore "/*" --ignore "[a-zA-Z]:/*" --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()" | sort --unique
# generate coverage report
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\()"
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@v6
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ${{ steps.coverage.outputs.report }}
## flags: IntegrationTests, UnitTests, ${{ steps.vars.outputs.CODECOV_FLAGS }}
flags: ${{ steps.vars.outputs.CODECOV_FLAGS }}
name: codecov-umbrella
fail_ci_if_error: false
verbose: true
directory: ./
root_dir: ./
fuzz:
name: Run the fuzzers
@@ -152,14 +148,21 @@ jobs:
env:
RUN_FOR: 60
steps:
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@nightly
- uses: actions/checkout@v6
- 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
+37
View File
@@ -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
+637 -56
View File
File diff suppressed because it is too large Load Diff
+10 -3
View File
@@ -1,7 +1,7 @@
[package]
name = "parse_datetime"
description = "parsing human-readable time strings and converting them to a DateTime"
version = "0.13.1"
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
+32 -9
View File
@@ -3,8 +3,10 @@
[![Crates.io](https://img.shields.io/crates/v/parse_datetime.svg)](https://crates.io/crates/parse_datetime)
[![License](http://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/uutils/parse_datetime/blob/main/LICENSE)
[![CodeCov](https://codecov.io/gh/uutils/parse_datetime/branch/main/graph/badge.svg)](https://codecov.io/gh/uutils/parse_datetime)
[![CodSpeed](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](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
+146
View File
@@ -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);
+72 -171
View File
@@ -2,20 +2,14 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "arbitrary"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
dependencies = [
"derive_arbitrary",
]
[[package]]
name = "autocfg"
@@ -25,9 +19,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "cc"
version = "1.2.37"
version = "1.2.58"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65193589c6404eb80b450d618eaf9a2cafaaafd57ecce47370519ef674a7bd44"
checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -37,56 +31,69 @@ dependencies = [
[[package]]
name = "cfg-if"
version = "1.0.3"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[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.1"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d"
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "fuzz_parse_datetime"
version = "0.2.0"
dependencies = [
"arbitrary",
"jiff",
"libfuzzer-sys",
"parse_datetime",
]
[[package]]
name = "getrandom"
version = "0.3.3"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasi",
"wasip2",
]
[[package]]
name = "jiff"
version = "0.2.15"
version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49"
checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359"
dependencies = [
"jiff-static",
"jiff-tzdb-platform",
"log",
"portable-atomic",
"portable-atomic-util",
"serde",
"serde_core",
"windows-sys",
]
[[package]]
name = "jiff-static"
version = "0.2.15"
version = "0.2.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4"
checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4"
dependencies = [
"proc-macro2",
"quote",
@@ -95,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"
@@ -120,15 +127,15 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.175"
version = "0.2.183"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
[[package]]
name = "libfuzzer-sys"
version = "0.4.10"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404"
checksum = "f12a681b7dd8ce12bff52488013ba614b869148d54dd79836ab85aafdd53f08d"
dependencies = [
"arbitrary",
"cc",
@@ -136,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.5"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
[[package]]
name = "num-traits"
@@ -157,43 +164,42 @@ dependencies = [
[[package]]
name = "parse_datetime"
version = "0.11.0"
version = "0.15.0"
dependencies = [
"jiff",
"num-traits",
"regex",
"winnow",
]
[[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.101"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
@@ -204,58 +210,20 @@ version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "regex"
version = "1.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
[[package]]
name = "serde"
version = "1.0.221"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "341877e04a22458705eb4e131a1508483c877dca2792b3781d4e5d8a6019ec43"
dependencies = [
"serde_core",
]
[[package]]
name = "serde_core"
version = "1.0.221"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c459bc0a14c840cb403fc14b148620de1e0778c96ecd6e0c8c3cacb6d8d00fe"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.221"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6185cf75117e20e62b1ff867b9518577271e58abe0037c40bb4794969355ab0"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
@@ -270,9 +238,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "syn"
version = "2.0.106"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
@@ -281,112 +249,45 @@ dependencies = [
[[package]]
name = "unicode-ident"
version = "1.0.19"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
[[package]]
name = "wasi"
version = "0.14.5+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4494f6290a82f5fe584817a676a34b9d6763e8d9d18204009fb31dceca98fd4"
dependencies = [
"wasip2",
]
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "wasip2"
version = "1.0.0+wasi-0.2.4"
version = "1.0.2+wasi-0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03fa2761397e5bd52002cd7e73110c71af2109aca4e521a9f40473fe685b0a24"
checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5"
dependencies = [
"wit-bindgen",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.59.0"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-targets",
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.7.13"
version = "0.7.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
dependencies = [
"memchr",
]
[[package]]
name = "wit-bindgen"
version = "0.45.1"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c573471f125075647d03df72e026074b7203790d41351cd6edc96f46bcccd36"
checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
+8
View File
@@ -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
+64
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+719 -17
View File
File diff suppressed because it is too large Load Diff
+42 -9
View File
@@ -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,17 +118,17 @@ 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")
}
}
pub(super) fn parse(input: &mut &str) -> ModalResult<Date> {
alt((iso1, iso2, us, literal1, literal2)).parse_next(input)
alt((iso1, iso2, us, literal1, literal2, literal3)).parse_next(input)
}
/// Parse `[year]-[month]-[day]`
@@ -252,6 +252,18 @@ fn literal2(input: &mut &str) -> ModalResult<Date> {
}
}
/// Parse `November-14-2022` and `Nov-14-2022`. Unlike `literal2`, the year is mandatory
/// to match the behavior of GNU `date`.
fn literal3(input: &mut &str) -> ModalResult<Date> {
let (month, _, day, _, year) =
(s(literal_month), s('-'), s(dec_uint), s('-'), year_str).parse_next(input)?;
// Map err to Backtrack instead of Cut to avoid early termination of parsing
(year, month, day)
.try_into()
.map_err(|e| ErrMode::Backtrack(ctx_err(e)))
}
/// Parse the name of a month (case-insensitive)
fn literal_month(input: &mut &str) -> ModalResult<u8> {
s(alpha1)
@@ -549,6 +561,27 @@ mod tests {
assert_eq!(s, ", 2022a");
}
#[test]
fn literal3() {
let reference = Date {
year: Some(2022),
month: 11,
day: 14,
};
for mut s in [
"november-14-2022",
"november----14----2022",
"november - 14 - 2022",
"nov-14-2022",
"nov---14---2022",
"nov - 14 - 2022",
] {
let old_s = s.to_owned();
assert_eq!(parse(&mut s).unwrap(), reference, "Format string: {old_s}");
}
}
#[test]
fn with_year() {
let reference = Date {
+1 -1
View File
@@ -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
View File
@@ -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}");
+17
View File
@@ -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);
}
}
+61
View File
@@ -142,3 +142,64 @@ pub(super) fn ctx_err(reason: &'static str) -> ContextError {
err.push(StrContext::Expected(StrContextValue::Description(reason)));
err
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_dec_int() {
for (input, expected) in [
("123", 123), // positive without sign
("+123", 123), // positive with '+' sign
("-123", -123), // negative with '-' sign
("0", 0), // zero
("+0", 0), // zero with '+' sign
("-0", 0), // zero with '-' sign (parses as 0)
("012", 12), // zero-prefixed (the main reason for this function)
("+012", 12), // zero-prefixed with '+' sign
("-012", -12), // zero-prefixed with '-' sign
("00123", 123), // multiple leading zeros
("2147483647", 2147483647), // i32::MAX
("-2147483648", -2147483648), // i32::MIN
] {
let mut s = input;
assert_eq!(
dec_int::<ContextError>(&mut s).unwrap(),
expected,
"{input}"
);
}
for input in [
"", // empty string
"+", // sign without digits
"-", // sign without digits
"abc", // non-numeric
"12a", // starts with digits but has non-digit after (but should parse "12" successfully)
] {
let mut s = input;
let result = dec_int::<ContextError>(&mut s);
// Note: "12a" will actually succeed and parse "12", leaving "a" unparsed
if input == "12a" {
assert_eq!(result.unwrap(), 12, "{input}");
assert_eq!(s, "a", "Should leave 'a' unparsed");
} else {
assert!(result.is_err(), "{input} should fail");
}
}
// Test overflow cases
for input in [
"2147483648", // i32::MAX + 1
"-2147483649", // i32::MIN - 1
"99999999999", // way too large
] {
let mut s = input;
assert!(
dec_int::<ContextError>(&mut s).is_err(),
"{input} should overflow"
);
}
}
}
+15 -2
View File
@@ -83,7 +83,7 @@ pub(super) fn parse(input: &mut &str) -> ModalResult<Relative> {
fn seconds(input: &mut &str) -> ModalResult<Relative> {
(
opt(alt((s('+').value(1), s('-').value(-1)))),
sec_and_nsec,
s(sec_and_nsec),
s(alpha1).verify(|s: &str| matches!(s, "seconds" | "second" | "sec" | "secs")),
ago,
)
@@ -138,11 +138,13 @@ mod tests {
("secs", Relative::Seconds(1, 0)),
("second ago", Relative::Seconds(-1, 0)),
("3 seconds", Relative::Seconds(3, 0)),
("+ 3 seconds", Relative::Seconds(3, 0)),
("3.5 seconds", Relative::Seconds(3, 500_000_000)),
("-3.5 seconds", Relative::Seconds(-4, 500_000_000)),
("+3.5 seconds", Relative::Seconds(3, 500_000_000)),
("+ 3.5 seconds", Relative::Seconds(3, 500_000_000)),
("3.5 seconds ago", Relative::Seconds(-4, 500_000_000)),
("-3.5 seconds ago", Relative::Seconds(3, 500_000_000)),
("- 3.5 seconds ago", Relative::Seconds(3, 500_000_000)),
// Minutes
("minute", Relative::Minutes(1)),
("minutes", Relative::Minutes(1)),
@@ -150,29 +152,40 @@ mod tests {
("mins", Relative::Minutes(1)),
("10 minutes", Relative::Minutes(10)),
("-10 minutes", Relative::Minutes(-10)),
("- 10 minutes", Relative::Minutes(-10)),
("10 minutes ago", Relative::Minutes(-10)),
("-10 minutes ago", Relative::Minutes(10)),
("- 10 minutes ago", Relative::Minutes(10)),
("-10 minutes ago", Relative::Minutes(10)),
("- 10 minutes ago", Relative::Minutes(10)),
// Hours
("hour", Relative::Hours(1)),
("hours", Relative::Hours(1)),
("10 hours", Relative::Hours(10)),
("+10 hours", Relative::Hours(10)),
("+ 10 hours", Relative::Hours(10)),
("-10 hours", Relative::Hours(-10)),
("- 10 hours", Relative::Hours(-10)),
("10 hours ago", Relative::Hours(-10)),
("-10 hours ago", Relative::Hours(10)),
("- 10 hours ago", Relative::Hours(10)),
// Days
("day", Relative::Days(1)),
("days", Relative::Days(1)),
("10 days", Relative::Days(10)),
("+10 days", Relative::Days(10)),
("+ 10 days", Relative::Days(10)),
("-10 days", Relative::Days(-10)),
("- 10 days", Relative::Days(-10)),
("10 days ago", Relative::Days(-10)),
("-10 days ago", Relative::Days(10)),
("- 10 days ago", Relative::Days(10)),
// Multiple days
("fortnight", Relative::Days(14)),
("fortnights", Relative::Days(14)),
("2 fortnights ago", Relative::Days(-28)),
("+2 fortnights ago", Relative::Days(-28)),
("+ 2 fortnights ago", Relative::Days(-28)),
("week", Relative::Days(7)),
("weeks", Relative::Days(7)),
("2 weeks ago", Relative::Days(-14)),
+1 -1
View File
@@ -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
View File
@@ -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"
);
}
}
+272 -50
View File
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More