mirror of
https://github.com/uutils/parse_datetime.git
synced 2026-10-16 03:25:04 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
eb444cc124 | ||
|
|
f75711a932 | ||
|
|
de62667844 | ||
|
|
701f5a0200 | ||
|
|
2df367552d | ||
|
|
3fc6d60400 | ||
|
|
1c92c879ed | ||
|
|
5bc4b0a902 | ||
|
|
a066a83ca4 | ||
|
|
ed95575362 | ||
|
|
c6bfe2d15a | ||
|
|
59336d8db1 | ||
|
|
dabd1eab31 | ||
|
|
f1523281a0 | ||
|
|
5b0b578a4c | ||
|
|
2788bf4e3d | ||
|
|
04fa426eff | ||
|
|
fef8bf0cfc | ||
|
|
e25bf1530c | ||
|
|
fdd87ce6ad | ||
|
|
f451f94912 | ||
|
|
d4bace0d65 | ||
|
|
618dda7263 | ||
|
|
9f2a054284 | ||
|
|
390c747003 | ||
|
|
1c7fdea358 | ||
|
|
e42c4f0e2e | ||
|
|
4cf4db33f0 | ||
|
|
46fb737541 | ||
|
|
0562eec95b | ||
|
|
d435b3727a | ||
|
|
01efbda22a | ||
|
|
0ded151ae6 | ||
|
|
75aaf908c0 | ||
|
|
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 | ||
|
|
f68a01699e | ||
|
|
73b4b0a50c | ||
|
|
002ca41008 | ||
|
|
818cfa4ad0 | ||
|
|
5cf8a6fbdd | ||
|
|
9688db08a8 | ||
|
|
f10a950e23 | ||
|
|
14bbd4e848 | ||
|
|
dbfdea454f | ||
|
|
7e528ad609 | ||
|
|
6306843888 | ||
|
|
d5c441bd3f | ||
|
|
62cb7d9687 | ||
|
|
642bbbf214 | ||
|
|
6396c07d72 | ||
|
|
377ba0e040 | ||
|
|
ffb1ba35c5 | ||
|
|
eeffa242a1 | ||
|
|
3a1edfb177 | ||
|
|
866b6b446f | ||
|
|
13e038a503 | ||
|
|
7030df6cf0 | ||
|
|
cfbb973215 | ||
|
|
88a4a718b0 | ||
|
|
6462d410d8 | ||
|
|
92d095b53b | ||
|
|
ea143af391 | ||
|
|
835e46e6f1 | ||
|
|
31b8e58cc5 | ||
|
|
9de30f5fc3 | ||
|
|
d6ab892c29 | ||
|
|
1e4fc30f7b | ||
|
|
6ce151fb58 | ||
|
|
9f6605f20d | ||
|
|
a025a04d72 | ||
|
|
dbae631ff0 | ||
|
|
83278f0ddc | ||
|
|
ae518cac37 | ||
|
|
bc5d201b68 | ||
|
|
8c56c5cb13 | ||
|
|
9c95c9549b | ||
|
|
ba69fb8772 | ||
|
|
912bc4e706 | ||
|
|
c239ed0b36 | ||
|
|
3074fe3baa | ||
|
|
29e368662c | ||
|
|
1e757df39b | ||
|
|
7a32b1ee3d | ||
|
|
c898cd84de | ||
|
|
eb610870e1 | ||
|
|
9db7a54926 | ||
|
|
e43652c0ee | ||
|
|
d9c2ed2b11 | ||
|
|
6a16e61ab1 | ||
|
|
f0c499593e | ||
|
|
5453d6777a | ||
|
|
0c09d5bd17 |
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 }}
|
||||
+33
-27
@@ -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@v7
|
||||
- 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@v7
|
||||
- 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@v7
|
||||
- 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@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@v5
|
||||
- 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@v5
|
||||
- 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
|
||||
@@ -134,32 +128,44 @@ 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@v7
|
||||
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
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUN_FOR: 60
|
||||
# cargo-fuzz uses nightly features
|
||||
RUSTC_BOOTSTRAP: 1
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
- uses: actions/checkout@v7
|
||||
- name: Install `cargo-fuzz`
|
||||
run: cargo install cargo-fuzz
|
||||
uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-fuzz
|
||||
- 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
|
||||
# https://github.com/rust-fuzz/cargo-fuzz/issues/398
|
||||
cargo fuzz run --target host-tuple 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 --target host-tuple 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@v5
|
||||
with:
|
||||
mode: simulation
|
||||
run: cargo codspeed run
|
||||
Generated
+703
-96
File diff suppressed because it is too large.
Load diff
+11
-5
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "parse_datetime"
|
||||
description = "parsing human-readable time strings and converting them to a DateTime"
|
||||
version = "0.12.0"
|
||||
description = "parsing human-readable time strings and converting them to a Zoned datetime"
|
||||
version = "0.17.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/uutils/parse_datetime"
|
||||
@@ -9,10 +9,16 @@ readme = "README.md"
|
||||
rust-version = "1.71.1"
|
||||
|
||||
[dependencies]
|
||||
regex = "1.10.4"
|
||||
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"
|
||||
rstest = "0.27"
|
||||
|
||||
[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
|
||||
@@ -56,7 +77,7 @@ The `parse_datetime` and `parse_datetime_at_date` functions support absolute dat
|
||||
- "now" or "today"
|
||||
- "yesterday"
|
||||
- "tomorrow"
|
||||
- use "ago" for the past
|
||||
- use "ago" for the past or "hence" for the future
|
||||
- use "next" or "last" with `unit` (e.g., "next week", "last year")
|
||||
- unix timestamps (for example "@0" "@1344000")
|
||||
|
||||
@@ -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,155 @@
|
||||
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) {
|
||||
// Use a fixed reference date: the cost of resolving a weekday depends on
|
||||
// how many days separate it from "today", so relying on the current date
|
||||
// makes the measurement vary with the day the benchmark runs.
|
||||
// 2000-01-03 is a Monday, so "wednesday" resolves two days ahead.
|
||||
let base = jiff::civil::DateTime::from(jiff::civil::date(2000, 1, 3))
|
||||
.to_zoned(jiff::tz::TimeZone::UTC)
|
||||
.unwrap();
|
||||
c.bench_function("parse_weekday", |b| {
|
||||
b.iter(|| parse_datetime_at_date(base.clone(), "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
+174
-179
File diff suppressed because it is too large.
Load diff
@@ -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
+852
-56
File diff suppressed because it is too large.
Load diff
+64
-11
@@ -16,7 +16,7 @@
|
||||
//! > discarded.
|
||||
use winnow::{
|
||||
combinator::{alt, trace},
|
||||
seq, ModalResult, Parser,
|
||||
ModalResult, Parser,
|
||||
};
|
||||
|
||||
use crate::items::space;
|
||||
@@ -29,20 +29,46 @@ pub(crate) struct DateTime {
|
||||
pub(crate) time: time::Time,
|
||||
}
|
||||
|
||||
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)
|
||||
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.")
|
||||
}
|
||||
|
||||
/// Parse the separator and time of a combined item whose ISO date has already
|
||||
/// been parsed.
|
||||
pub(crate) fn parse_time_after_date(input: &mut &str, date: date::Date) -> ModalResult<DateTime> {
|
||||
// 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)]
|
||||
mod tests {
|
||||
use super::{parse, DateTime};
|
||||
use crate::items::{date::Date, time::Time};
|
||||
use winnow::{combinator::alt, ModalResult, Parser};
|
||||
|
||||
use super::{parse_time_after_date, DateTime};
|
||||
use crate::items::{date, date::Date, time::Time};
|
||||
|
||||
fn parse(input: &mut &str) -> ModalResult<DateTime> {
|
||||
let date = alt((date::iso1, date::iso2)).parse_next(input)?;
|
||||
parse_time_after_date(input, date)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn some_date() {
|
||||
@@ -73,4 +99,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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
+68
-24
@@ -27,7 +27,7 @@
|
||||
//! > ‘September’.
|
||||
|
||||
use winnow::{
|
||||
ascii::{alpha1, multispace1},
|
||||
ascii::alpha1,
|
||||
combinator::{alt, eof, opt, preceded, terminated},
|
||||
error::ErrMode,
|
||||
stream::AsChar,
|
||||
@@ -36,7 +36,7 @@ use winnow::{
|
||||
};
|
||||
|
||||
use super::{
|
||||
primitive::{ctx_err, dec_uint, s},
|
||||
primitive::{ctx_err, dec_uint, multispace1, s},
|
||||
year::{year_from_str, year_str},
|
||||
};
|
||||
|
||||
@@ -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,19 @@ 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)
|
||||
/// Parse a date that is not in ISO format. ISO dates are parsed by [`iso1`]
|
||||
/// and [`iso2`], which the caller tries first.
|
||||
pub(super) fn parse_non_iso(input: &mut &str) -> ModalResult<Date> {
|
||||
alt((us, literal1, literal2, literal3)).parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse `[year]-[month]-[day]`
|
||||
@@ -138,9 +140,10 @@ pub(super) fn iso1(input: &mut &str) -> ModalResult<Date> {
|
||||
let (year, _, month, _, day) =
|
||||
(year_str, s('-'), s(dec_uint), s('-'), s(dec_uint)).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::Cut(ctx_err(e)))
|
||||
.map_err(|e| ErrMode::Backtrack(ctx_err(e)))
|
||||
}
|
||||
|
||||
/// Parse `[year][month][day]`
|
||||
@@ -156,7 +159,7 @@ pub(super) fn iso2(input: &mut &str) -> ModalResult<Date> {
|
||||
|
||||
(year, month, day)
|
||||
.try_into()
|
||||
.map_err(|e| ErrMode::Cut(ctx_err(e)))
|
||||
.map_err(|e| ErrMode::Backtrack(ctx_err(e)))
|
||||
}
|
||||
|
||||
/// Parse `[year]/[month]/[day]` or `[month]/[day]/[year]` or `[month]/[day]`.
|
||||
@@ -178,19 +181,21 @@ fn us(input: &mut &str) -> ModalResult<Date> {
|
||||
let day = day_from_str(s2)?;
|
||||
(s1, n, day)
|
||||
.try_into()
|
||||
.map_err(|e| ErrMode::Cut(ctx_err(e)))
|
||||
.map_err(|e| ErrMode::Backtrack(ctx_err(e)))
|
||||
}
|
||||
Some(s2) => {
|
||||
// [month]/[day]/[year]
|
||||
let month = month_from_str(s1)?;
|
||||
(s2, month, n)
|
||||
.try_into()
|
||||
.map_err(|e| ErrMode::Cut(ctx_err(e)))
|
||||
.map_err(|e| ErrMode::Backtrack(ctx_err(e)))
|
||||
}
|
||||
None => {
|
||||
// [month]/[day]
|
||||
let month = month_from_str(s1)?;
|
||||
(month, n).try_into().map_err(|e| ErrMode::Cut(ctx_err(e)))
|
||||
(month, n)
|
||||
.try_into()
|
||||
.map_err(|e| ErrMode::Backtrack(ctx_err(e)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -213,10 +218,10 @@ fn literal1(input: &mut &str) -> ModalResult<Date> {
|
||||
match year {
|
||||
Some(year) => (year, month, day)
|
||||
.try_into()
|
||||
.map_err(|e| ErrMode::Cut(ctx_err(e))),
|
||||
.map_err(|e| ErrMode::Backtrack(ctx_err(e))),
|
||||
None => (month, day)
|
||||
.try_into()
|
||||
.map_err(|e| ErrMode::Cut(ctx_err(e))),
|
||||
.map_err(|e| ErrMode::Backtrack(ctx_err(e))),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,13 +247,25 @@ fn literal2(input: &mut &str) -> ModalResult<Date> {
|
||||
match year {
|
||||
Some(year) => (year, month, day)
|
||||
.try_into()
|
||||
.map_err(|e| ErrMode::Cut(ctx_err(e))),
|
||||
.map_err(|e| ErrMode::Backtrack(ctx_err(e))),
|
||||
None => (month, day)
|
||||
.try_into()
|
||||
.map_err(|e| ErrMode::Cut(ctx_err(e))),
|
||||
.map_err(|e| ErrMode::Backtrack(ctx_err(e))),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)
|
||||
@@ -274,17 +291,23 @@ fn literal_month(input: &mut &str) -> ModalResult<u8> {
|
||||
|
||||
fn month_from_str(s: &str) -> ModalResult<u8> {
|
||||
s.parse::<u8>()
|
||||
.map_err(|_| ErrMode::Cut(ctx_err("month must be a valid u8 number")))
|
||||
.map_err(|_| ErrMode::Backtrack(ctx_err("month must be a valid u8 number")))
|
||||
}
|
||||
|
||||
fn day_from_str(s: &str) -> ModalResult<u8> {
|
||||
s.parse::<u8>()
|
||||
.map_err(|_| ErrMode::Cut(ctx_err("day must be a valid u8 number")))
|
||||
.map_err(|_| ErrMode::Backtrack(ctx_err("day must be a valid u8 number")))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{parse, Date};
|
||||
use winnow::{combinator::alt, ModalResult, Parser};
|
||||
|
||||
use super::Date;
|
||||
|
||||
fn parse(input: &mut &str) -> ModalResult<Date> {
|
||||
alt((super::iso1, super::iso2, super::parse_non_iso)).parse_next(input)
|
||||
}
|
||||
|
||||
// Test cases from the GNU docs:
|
||||
//
|
||||
@@ -546,6 +569,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
@@ -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,
|
||||
|
||||
+391
-42
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
+37
-1
@@ -35,7 +35,7 @@ fn text_ordinal(input: &mut &str) -> ModalResult<i32> {
|
||||
"fifth" => 5,
|
||||
"sixth" => 6,
|
||||
"seventh" => 7,
|
||||
"eight" => 8,
|
||||
"eighth" => 8,
|
||||
"ninth" => 9,
|
||||
"tenth" => 10,
|
||||
"eleventh" => 11,
|
||||
@@ -45,3 +45,39 @@ fn text_ordinal(input: &mut &str) -> ModalResult<i32> {
|
||||
})
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ordinal;
|
||||
|
||||
#[test]
|
||||
fn text_ordinal() {
|
||||
for (s, expected) in [
|
||||
("last", -1),
|
||||
("this", 0),
|
||||
("next", 1),
|
||||
("first", 1),
|
||||
("third", 3),
|
||||
("fourth", 4),
|
||||
("fifth", 5),
|
||||
("sixth", 6),
|
||||
("seventh", 7),
|
||||
("eighth", 8),
|
||||
("ninth", 9),
|
||||
("tenth", 10),
|
||||
("eleventh", 11),
|
||||
("twelfth", 12),
|
||||
] {
|
||||
let mut input = s;
|
||||
assert_eq!(ordinal(&mut input).unwrap(), expected, "{s}");
|
||||
assert!(input.is_empty(), "{s}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_text_ordinal() {
|
||||
for mut s in ["eight", "second", "foo"] {
|
||||
assert!(ordinal(&mut s).is_err(), "{s}");
|
||||
}
|
||||
}
|
||||
}
|
||||
+113
-1
@@ -6,7 +6,7 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use winnow::{
|
||||
ascii::{digit1, multispace0, Uint},
|
||||
ascii::{digit1, Uint},
|
||||
combinator::{alt, delimited, not, opt, peek, preceded, repeat, separated},
|
||||
error::{ContextError, ParserError, StrContext, StrContextValue},
|
||||
stream::AsChar,
|
||||
@@ -26,6 +26,29 @@ where
|
||||
preceded(space, p)
|
||||
}
|
||||
|
||||
/// Same as winnow's `multispace0`, but also accepting `\v` and `\f`, like
|
||||
/// the whitespace GNU `date` accepts between tokens
|
||||
fn multispace0<'a, E>(input: &mut &'a str) -> winnow::Result<&'a str, E>
|
||||
where
|
||||
E: ParserError<&'a str>,
|
||||
{
|
||||
let len = input
|
||||
.bytes()
|
||||
.take_while(|b| matches!(b, b' ' | b'\t' | b'\n' | b'\x0B' | b'\x0C' | b'\r'))
|
||||
.count();
|
||||
let (ws, rest) = input.split_at(len);
|
||||
*input = rest;
|
||||
Ok(ws)
|
||||
}
|
||||
|
||||
/// Same as [`multispace0`], but requiring at least one character
|
||||
pub(super) fn multispace1<'a, E>(input: &mut &'a str) -> winnow::Result<&'a str, E>
|
||||
where
|
||||
E: ParserError<&'a str>,
|
||||
{
|
||||
take_while(1.., (' ', '\t', '\n', '\x0B', '\x0C', '\r')).parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse the space in-between tokens
|
||||
///
|
||||
/// You probably want to use the [`s`] combinator instead.
|
||||
@@ -33,6 +56,11 @@ pub(super) fn space<'a, E>(input: &mut &'a str) -> winnow::Result<(), E>
|
||||
where
|
||||
E: ParserError<&'a str>,
|
||||
{
|
||||
// Fast path: most calls see no comment or ignorable sign
|
||||
multispace0.parse_next(input)?;
|
||||
if !input.starts_with(['(', '-', '+']) {
|
||||
return Ok(());
|
||||
}
|
||||
separated(0.., multispace0, alt((comment, ignored_hyphen_or_plus))).parse_next(input)
|
||||
}
|
||||
|
||||
@@ -142,3 +170,87 @@ 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"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn space_skips_comments_and_ignored_signs() {
|
||||
for (input, rest) in [
|
||||
("", ""),
|
||||
("abc", "abc"),
|
||||
(" \t abc", "abc"),
|
||||
("(foo) abc", "abc"),
|
||||
(" (a (b)) abc", "abc"),
|
||||
("- abc", "abc"),
|
||||
("+ (c) abc", "abc"),
|
||||
("- (c) 12", "12"),
|
||||
// A sign followed by a digit is not ignored.
|
||||
("- 12", "- 12"),
|
||||
(" +12", "+12"),
|
||||
// An unbalanced comment is not skipped.
|
||||
("(foo abc", "(foo abc"),
|
||||
] {
|
||||
let mut s = input;
|
||||
space::<ContextError>(&mut s).unwrap();
|
||||
assert_eq!(s, rest, "{input:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
+34
-9
@@ -59,9 +59,19 @@ impl TryFrom<Relative> for jiff::Span {
|
||||
Relative::Days(days) => jiff::Span::new().try_days(days),
|
||||
Relative::Hours(hours) => jiff::Span::new().try_hours(hours),
|
||||
Relative::Minutes(minutes) => jiff::Span::new().try_minutes(minutes),
|
||||
Relative::Seconds(seconds, nanoseconds) => jiff::Span::new()
|
||||
.try_seconds(seconds)
|
||||
.and_then(|span| span.try_nanoseconds(nanoseconds)),
|
||||
Relative::Seconds(seconds, nanoseconds) => {
|
||||
// -0.25 is stored as (-1, 750_000_000), but a `jiff::Span` needs
|
||||
// both fields with one sign, so move the fraction onto it.
|
||||
let (seconds, nanoseconds) = if seconds < 0 && nanoseconds > 0 {
|
||||
(seconds + 1, -i64::from(1_000_000_000 - nanoseconds))
|
||||
} else {
|
||||
(seconds, i64::from(nanoseconds))
|
||||
};
|
||||
|
||||
jiff::Span::new()
|
||||
.try_seconds(seconds)
|
||||
.and_then(|span| span.try_nanoseconds(nanoseconds))
|
||||
}
|
||||
}
|
||||
.map_err(|_| "relative value is invalid")
|
||||
}
|
||||
@@ -83,9 +93,9 @@ 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,
|
||||
direction,
|
||||
)
|
||||
.verify_map(|(sign, (sec, nsec), _, ago)| {
|
||||
let sec = i64::try_from(sec).ok()?;
|
||||
@@ -102,7 +112,7 @@ fn seconds(input: &mut &str) -> ModalResult<Relative> {
|
||||
}
|
||||
|
||||
fn displacement(input: &mut &str) -> ModalResult<Relative> {
|
||||
(opt(ordinal), s(alpha1), ago)
|
||||
(opt(ordinal), s(alpha1), direction)
|
||||
.verify_map(|(n, unit, ago): (Option<i32>, &str, bool)| {
|
||||
let multiplier = n.unwrap_or(1) * if ago { -1 } else { 1 };
|
||||
Some(match unit.strip_suffix('s').unwrap_or(unit) {
|
||||
@@ -120,8 +130,10 @@ fn displacement(input: &mut &str) -> ModalResult<Relative> {
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
fn ago(input: &mut &str) -> ModalResult<bool> {
|
||||
opt(s("ago")).map(|o| o.is_some()).parse_next(input)
|
||||
fn direction(input: &mut &str) -> ModalResult<bool> {
|
||||
opt(alt((s("ago").value(true), s("hence").value(false))))
|
||||
.map(|backward| backward.unwrap_or(false))
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -138,11 +150,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 +164,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)),
|
||||
|
||||
Loaded 20 of 29 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user