mirror of
https://github.com/uutils/parse_datetime.git
synced 2026-10-16 03:25:04 -07:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb1cb02a33 | ||
|
|
52554677dd | ||
|
|
516a3a934d | ||
|
|
1b014a15a3 | ||
|
|
bf5cf43375 | ||
|
|
edb83cceba | ||
|
|
a3b8d128fa | ||
|
|
e717803a5e | ||
|
|
b6c2256daf | ||
|
|
3355829f3d | ||
|
|
80556965f3 | ||
|
|
b83d7a953c | ||
|
|
4b412e03d2 | ||
|
|
c312311cf0 | ||
|
|
672a2acbc4 | ||
|
|
665eb95a13 | ||
|
|
0371374e7e | ||
|
|
1129eeaeaf | ||
|
|
400969043d | ||
|
|
d464f3a2b1 | ||
|
|
025feb86f8 | ||
|
|
53ed79bcfc | ||
|
|
d5bd1e8e4a | ||
|
|
7e9a8b502c | ||
|
|
7440dd9ecb | ||
|
|
e5b92637b1 | ||
|
|
16ce783763 | ||
|
|
62f26a49b3 | ||
|
|
01687f304d | ||
|
|
3767622cc3 | ||
|
|
93f2eafcf7 | ||
|
|
42233b0c81 | ||
|
|
e9b8f7a43e | ||
|
|
22da5707a6 | ||
|
|
2efa5c1d7b | ||
|
|
f7aa4540c9 | ||
|
|
72cada4740 | ||
|
|
108ebcb04a | ||
|
|
fe0fd40ea5 | ||
|
|
30ea3911d7 | ||
|
|
ee4971cc73 | ||
|
|
f6f1257961 | ||
|
|
82b82df5b9 | ||
|
|
d46a321e82 | ||
|
|
cfcf9c307d | ||
|
|
d3367318eb | ||
|
|
8c59af1c99 | ||
|
|
7aeb87d88e | ||
|
|
65f9eac853 | ||
|
|
4a1b3f3777 | ||
|
|
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 |
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 }}
|
||||
+30
-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,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@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
|
||||
@@ -152,14 +148,21 @@ jobs:
|
||||
env:
|
||||
RUN_FOR: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
- uses: actions/checkout@v7
|
||||
- name: emulate nightly toolchain
|
||||
run: echo "RUSTC_BOOTSTRAP=1" >> "${GITHUB_ENV}"
|
||||
- name: Install `cargo-fuzz`
|
||||
run: cargo install cargo-fuzz
|
||||
run: cargo install cargo-fuzz --locked
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Run from_str for XX seconds
|
||||
- name: Run fuzz_parse_datetime for XX seconds
|
||||
shell: bash
|
||||
run: |
|
||||
## Run it
|
||||
cd fuzz
|
||||
cargo +nightly fuzz run fuzz_parse_datetime -- -max_total_time=${{ env.RUN_FOR }} -detect_leaks=0
|
||||
cargo fuzz run fuzz_parse_datetime -- -max_total_time=${{ env.RUN_FOR }} -detect_leaks=0
|
||||
- name: Run fuzz_large_year for XX seconds
|
||||
shell: bash
|
||||
run: |
|
||||
## Run it
|
||||
cd fuzz
|
||||
cargo fuzz run fuzz_large_year -- -max_total_time=${{ env.RUN_FOR }} -detect_leaks=0
|
||||
@@ -0,0 +1,37 @@
|
||||
name: CodSpeed
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
pull_request:
|
||||
# `workflow_dispatch` allows CodSpeed to trigger backtest
|
||||
# performance analysis in order to generate initial data.
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write
|
||||
|
||||
jobs:
|
||||
codspeed:
|
||||
name: Run benchmarks
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- name: Setup rust toolchain, cache and cargo-codspeed binary
|
||||
uses: moonrepo/setup-rust@v1
|
||||
with:
|
||||
channel: stable
|
||||
cache-target: release
|
||||
bins: cargo-codspeed
|
||||
|
||||
- name: Build the benchmark target(s)
|
||||
run: cargo codspeed build
|
||||
|
||||
- name: Run the benchmarks
|
||||
uses: CodSpeedHQ/action@v4
|
||||
with:
|
||||
mode: simulation
|
||||
run: cargo codspeed run
|
||||
Generated
+693
-85
File diff suppressed because it is too large.
Load diff
+10
-4
@@ -1,7 +1,7 @@
|
||||
[package]
|
||||
name = "parse_datetime"
|
||||
description = "parsing human-readable time strings and converting them to a DateTime"
|
||||
version = "0.13.0"
|
||||
description = "parsing human-readable time strings and converting them to a Zoned datetime"
|
||||
version = "0.15.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/uutils/parse_datetime"
|
||||
@@ -9,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"
|
||||
|
||||
[dev-dependencies.codspeed-criterion-compat]
|
||||
version = "5.0"
|
||||
|
||||
[[bench]]
|
||||
name = "parse_datetime"
|
||||
harness = false
|
||||
@@ -3,8 +3,10 @@
|
||||
[](https://crates.io/crates/parse_datetime)
|
||||
[](https://github.com/uutils/parse_datetime/blob/main/LICENSE)
|
||||
[](https://codecov.io/gh/uutils/parse_datetime)
|
||||
[](https://codspeed.io/uutils/parse_datetime?utm_source=badge)
|
||||
|
||||
A Rust crate for parsing human-readable relative time strings and human-readable datetime strings and converting them to a jiff's `Zoned` object.
|
||||
A Rust crate for parsing human-readable relative time strings and
|
||||
human-readable datetime strings.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -26,25 +28,44 @@ Then, import the crate and use the `parse_datetime_at_date` function:
|
||||
|
||||
```rs
|
||||
use jiff::{ToSpan, Zoned};
|
||||
use parse_datetime::parse_datetime_at_date;
|
||||
use parse_datetime::{parse_datetime_at_date, ParsedDateTime};
|
||||
|
||||
let now = Zoned::now();
|
||||
let after = parse_datetime_at_date(now.clone(), "+3 days");
|
||||
|
||||
assert_eq!(
|
||||
now.checked_add(3.days()).unwrap(),
|
||||
after.unwrap()
|
||||
);
|
||||
match after.unwrap() {
|
||||
ParsedDateTime::InRange(z) => assert_eq!(now.checked_add(3.days()).unwrap(), z),
|
||||
ParsedDateTime::Extended(_) => unreachable!("unexpected for this input"),
|
||||
}
|
||||
```
|
||||
|
||||
For DateTime parsing, import the `parse_datetime` function:
|
||||
|
||||
```rs
|
||||
use jiff::{civil::{date, time} ,Zoned};
|
||||
use parse_datetime::parse_datetime;
|
||||
use parse_datetime::{parse_datetime, ParsedDateTime};
|
||||
|
||||
let dt = parse_datetime("2021-02-14 06:37:47");
|
||||
assert_eq!(dt.unwrap(), Zoned::now().with().date(date(2021, 2, 14)).time(time(6, 37, 47, 0)).build().unwrap());
|
||||
match dt.unwrap() {
|
||||
ParsedDateTime::InRange(z) => assert_eq!(z, Zoned::now().with().date(date(2021, 2, 14)).time(time(6, 37, 47, 0)).build().unwrap()),
|
||||
ParsedDateTime::Extended(_) => unreachable!("unexpected for this input"),
|
||||
}
|
||||
```
|
||||
|
||||
For years beyond jiff's representable range (e.g., year 10000+), the result is an `ExtendedDateTime`:
|
||||
|
||||
```rs
|
||||
use parse_datetime::{parse_datetime, ParsedDateTime};
|
||||
|
||||
let dt = parse_datetime("12000-01-01").unwrap();
|
||||
match dt {
|
||||
ParsedDateTime::Extended(ext) => {
|
||||
assert_eq!(ext.year, 12000);
|
||||
assert_eq!(ext.month, 1);
|
||||
assert_eq!(ext.day, 1);
|
||||
}
|
||||
ParsedDateTime::InRange(_) => unreachable!("year 12000 is out of jiff range"),
|
||||
}
|
||||
```
|
||||
|
||||
### Supported Formats
|
||||
@@ -69,7 +90,9 @@ The `parse_datetime` and `parse_datetime_at_date` functions support absolute dat
|
||||
|
||||
The `parse_datetime` and `parse_datetime_at_date` function return:
|
||||
|
||||
- `Ok(Zoned)` - If the input string can be parsed as a `Zoned` object
|
||||
- `Ok(ParsedDateTime)` - If the input string can be parsed
|
||||
- `ParsedDateTime::InRange(Zoned)` for years supported by `jiff::Zoned`
|
||||
- `ParsedDateTime::Extended(ExtendedDateTime)` for out-of-range years (for example `>9999`)
|
||||
- `Err(ParseDateTimeError::InvalidInput)` - If the input string cannot be parsed
|
||||
|
||||
## Fuzzer
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
use codspeed_criterion_compat::{criterion_group, criterion_main, Criterion};
|
||||
use jiff::Zoned;
|
||||
use parse_datetime::{parse_datetime, parse_datetime_at_date};
|
||||
|
||||
fn bench_iso_datetime(c: &mut Criterion) {
|
||||
c.bench_function("parse_iso_datetime", |b| {
|
||||
b.iter(|| parse_datetime("2021-02-14 06:37:47 +0000"))
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_iso_datetime_t_sep(c: &mut Criterion) {
|
||||
c.bench_function("parse_iso_datetime_t_separator", |b| {
|
||||
b.iter(|| parse_datetime("2021-02-14T22:37:47-0800"))
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_date_only(c: &mut Criterion) {
|
||||
c.bench_function("parse_date_only", |b| {
|
||||
b.iter(|| parse_datetime("1997-01-01"))
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_date_slash_format(c: &mut Criterion) {
|
||||
c.bench_function("parse_date_slash_format", |b| {
|
||||
b.iter(|| parse_datetime("05/07/1987"))
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_epoch_timestamp(c: &mut Criterion) {
|
||||
c.bench_function("parse_epoch_timestamp", |b| {
|
||||
b.iter(|| parse_datetime("@1613371067"))
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_relative_time(c: &mut Criterion) {
|
||||
let now = Zoned::now();
|
||||
c.bench_function("parse_relative_time", |b| {
|
||||
b.iter(|| parse_datetime_at_date(now.clone(), "+3 days"))
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_relative_time_complex(c: &mut Criterion) {
|
||||
let now = Zoned::now();
|
||||
c.bench_function("parse_relative_time_complex", |b| {
|
||||
b.iter(|| parse_datetime_at_date(now.clone(), "1 year 3 months 2 days ago"))
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_relative_keywords(c: &mut Criterion) {
|
||||
c.bench_function("parse_yesterday", |b| {
|
||||
b.iter(|| parse_datetime("yesterday"))
|
||||
});
|
||||
c.bench_function("parse_tomorrow", |b| b.iter(|| parse_datetime("tomorrow")));
|
||||
c.bench_function("parse_now", |b| b.iter(|| parse_datetime("now")));
|
||||
}
|
||||
|
||||
fn bench_weekday(c: &mut Criterion) {
|
||||
c.bench_function("parse_weekday", |b| b.iter(|| parse_datetime("wednesday")));
|
||||
}
|
||||
|
||||
fn bench_timezone_offset(c: &mut Criterion) {
|
||||
c.bench_function("parse_timezone_offset", |b| {
|
||||
b.iter(|| parse_datetime("UTC+07:00"))
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_datetime_with_delta(c: &mut Criterion) {
|
||||
c.bench_function("parse_datetime_with_delta", |b| {
|
||||
b.iter(|| parse_datetime("1997-01-01 00:00:00 +0000 +1 year"))
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_ctime_format(c: &mut Criterion) {
|
||||
c.bench_function("parse_ctime_format", |b| {
|
||||
b.iter(|| parse_datetime("Wed Jan 1 00:00:00 1997"))
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_datetime_with_timezone_name(c: &mut Criterion) {
|
||||
c.bench_function("parse_datetime_with_tz_name", |b| {
|
||||
b.iter(|| parse_datetime("1997-01-19 08:17:48 BRT"))
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_datetime_ending_in_z(c: &mut Criterion) {
|
||||
c.bench_function("parse_datetime_ending_in_z", |b| {
|
||||
b.iter(|| parse_datetime("2023-06-03 12:00:01Z"))
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_invalid_input(c: &mut Criterion) {
|
||||
c.bench_function("parse_invalid_input", |b| {
|
||||
b.iter(|| parse_datetime("NotADate"))
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_extended_year(c: &mut Criterion) {
|
||||
c.bench_function("parse_extended_year", |b| {
|
||||
b.iter(|| parse_datetime("10000-01-01"))
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_extended_year_rollover(c: &mut Criterion) {
|
||||
c.bench_function("parse_extended_year_rollover", |b| {
|
||||
b.iter(|| parse_datetime("9999-12-31 +1 day"))
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_extended_year_relative(c: &mut Criterion) {
|
||||
let base = jiff::civil::DateTime::from(jiff::civil::date(2000, 1, 1))
|
||||
.to_zoned(jiff::tz::TimeZone::UTC)
|
||||
.unwrap();
|
||||
c.bench_function("parse_extended_year_relative", |b| {
|
||||
b.iter(|| parse_datetime_at_date(base.clone(), "10000-01-01 +1 day"))
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_extended_large_year(c: &mut Criterion) {
|
||||
c.bench_function("parse_extended_large_year", |b| {
|
||||
b.iter(|| parse_datetime("999999-06-15"))
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
benches,
|
||||
bench_iso_datetime,
|
||||
bench_iso_datetime_t_sep,
|
||||
bench_date_only,
|
||||
bench_date_slash_format,
|
||||
bench_epoch_timestamp,
|
||||
bench_relative_time,
|
||||
bench_relative_time_complex,
|
||||
bench_relative_keywords,
|
||||
bench_weekday,
|
||||
bench_timezone_offset,
|
||||
bench_datetime_with_delta,
|
||||
bench_ctime_format,
|
||||
bench_datetime_with_timezone_name,
|
||||
bench_datetime_ending_in_z,
|
||||
bench_invalid_input,
|
||||
bench_extended_year,
|
||||
bench_extended_year_rollover,
|
||||
bench_extended_year_relative,
|
||||
bench_extended_large_year,
|
||||
);
|
||||
criterion_main!(benches);
|
||||
Generated
+151
-178
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
+776
-40
File diff suppressed because it is too large.
Load diff
+53
-8
@@ -16,7 +16,7 @@
|
||||
//! > discarded.
|
||||
use winnow::{
|
||||
combinator::{alt, trace},
|
||||
seq, ModalResult, Parser,
|
||||
ModalResult, Parser,
|
||||
};
|
||||
|
||||
use crate::items::space;
|
||||
@@ -29,14 +29,32 @@ pub(crate) struct DateTime {
|
||||
pub(crate) time: time::Time,
|
||||
}
|
||||
|
||||
fn remaining_starts_with_meridiem(input: &str) -> bool {
|
||||
let trimmed = input.trim_start();
|
||||
trimmed.starts_with("am")
|
||||
|| trimmed.starts_with("pm")
|
||||
|| trimmed.starts_with("a.m.")
|
||||
|| trimmed.starts_with("p.m.")
|
||||
}
|
||||
|
||||
pub(crate) fn parse(input: &mut &str) -> ModalResult<DateTime> {
|
||||
seq!(DateTime {
|
||||
date: trace("iso_date", alt((date::iso1, date::iso2))),
|
||||
// Note: the `T` is lowercased by the main parse function
|
||||
_: alt((s('t').void(), (' ', space).void())),
|
||||
time: trace("iso_time", time::iso),
|
||||
})
|
||||
.parse_next(input)
|
||||
let date = trace("iso_date", alt((date::iso1, date::iso2))).parse_next(input)?;
|
||||
// Note: the `T` is lowercased by the main parse function
|
||||
alt((s('t').void(), (' ', space).void())).parse_next(input)?;
|
||||
|
||||
let mut iso_input = *input;
|
||||
if let Ok(parsed_time) = trace("iso_time", time::iso).parse_next(&mut iso_input) {
|
||||
if !remaining_starts_with_meridiem(iso_input) {
|
||||
*input = iso_input;
|
||||
return Ok(DateTime {
|
||||
date,
|
||||
time: parsed_time,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
let time = trace("iso_time", time::parse).parse_next(input)?;
|
||||
Ok(DateTime { date, time })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -73,4 +91,31 @@ mod tests {
|
||||
assert_eq!(parse(&mut s).ok(), reference, "Failed string: {old_s}")
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn date_and_time_ampm() {
|
||||
let reference = Some(DateTime {
|
||||
date: Date {
|
||||
day: 15,
|
||||
month: 6,
|
||||
year: Some(2024),
|
||||
},
|
||||
time: Time {
|
||||
hour: 15,
|
||||
minute: 0,
|
||||
second: 0,
|
||||
nanosecond: 0,
|
||||
offset: None,
|
||||
},
|
||||
});
|
||||
|
||||
for mut s in [
|
||||
"2024-06-15 3:00 pm",
|
||||
"2024-06-15 3:00pm",
|
||||
"2024-06-15 3:00 p.m.",
|
||||
] {
|
||||
let old_s = s.to_owned();
|
||||
assert_eq!(parse(&mut s).ok(), reference, "Failed string: {old_s}");
|
||||
}
|
||||
}
|
||||
}
|
||||
+56
-20
@@ -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]`
|
||||
@@ -138,9 +138,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 +157,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 +179,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 +216,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 +245,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,12 +289,12 @@ 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)]
|
||||
@@ -546,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
@@ -33,7 +33,7 @@ use super::primitive::{dec_uint, plus_or_minus, s};
|
||||
/// - `nanosecond` is always in the range of `0..1_000_000_000`.
|
||||
/// - Negative timestamps are represented by a negative `second` value and a
|
||||
/// positive `nanosecond` value.
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
pub(super) struct Timestamp {
|
||||
second: i64,
|
||||
nanosecond: u32,
|
||||
|
||||
+227
-21
@@ -48,6 +48,7 @@ mod primitive;
|
||||
|
||||
pub(crate) mod error;
|
||||
|
||||
use crate::ParsedDateTime;
|
||||
use jiff::Zoned;
|
||||
use primitive::space;
|
||||
use winnow::{
|
||||
@@ -62,7 +63,6 @@ use error::Error;
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
enum Item {
|
||||
Timestamp(epoch::Timestamp),
|
||||
DateTime(combined::DateTime),
|
||||
Date(date::Date),
|
||||
Time(time::Time),
|
||||
@@ -73,18 +73,21 @@ enum Item {
|
||||
Pure(String),
|
||||
}
|
||||
|
||||
/// Parse a date and time string and build a `Zoned` object. The parsed result
|
||||
/// is resolved against the given base date and time.
|
||||
pub(crate) fn parse_at_date<S: AsRef<str> + Clone>(base: Zoned, input: S) -> Result<Zoned, Error> {
|
||||
/// Parse a date and time string and resolve it against the given base date and
|
||||
/// time, returning a [`ParsedDateTime`] result.
|
||||
pub(crate) fn parse_at_date<S: AsRef<str> + Clone>(
|
||||
base: Zoned,
|
||||
input: S,
|
||||
) -> Result<ParsedDateTime, Error> {
|
||||
match parse(&mut input.as_ref()) {
|
||||
Ok(builder) => builder.set_base(base).build(),
|
||||
Err(e) => Err(e.into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a date and time string and build a `Zoned` object. The parsed result
|
||||
/// is resolved against the current local date and time.
|
||||
pub(crate) fn parse_at_local<S: AsRef<str> + Clone>(input: S) -> Result<Zoned, Error> {
|
||||
/// Parse a date and time string and resolve it against the current local date
|
||||
/// and time, returning a [`ParsedDateTime`] result.
|
||||
pub(crate) fn parse_at_local<S: AsRef<str> + Clone>(input: S) -> Result<ParsedDateTime, Error> {
|
||||
match parse(&mut input.as_ref()) {
|
||||
Ok(builder) => builder.build(), // the builder uses current local date and time if no base is given.
|
||||
Err(e) => Err(e.into()),
|
||||
@@ -209,12 +212,9 @@ fn parse_timestamp(input: &mut &str) -> ModalResult<DateTimeBuilder> {
|
||||
trace(
|
||||
"parse_timestamp",
|
||||
// Expect exactly one timestamp and then EOF (allowing trailing spaces).
|
||||
terminated(epoch::parse.map(Item::Timestamp), preceded(space, eof)),
|
||||
terminated(epoch::parse, preceded(space, eof)),
|
||||
)
|
||||
.verify_map(|item: Item| match item {
|
||||
Item::Timestamp(ts) => DateTimeBuilder::new().set_timestamp(ts).ok(),
|
||||
_ => None,
|
||||
})
|
||||
.verify_map(|ts| DateTimeBuilder::new().set_timestamp(ts).ok())
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
@@ -277,12 +277,13 @@ fn expect_error(input: &mut &str, reason: &'static str) -> ErrMode<ContextError>
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::ParsedDateTime;
|
||||
use jiff::{civil::DateTime, tz::TimeZone, ToSpan, Zoned};
|
||||
|
||||
use super::*;
|
||||
|
||||
fn at_date(builder: DateTimeBuilder, base: Zoned) -> Zoned {
|
||||
builder.set_base(base).build().unwrap()
|
||||
builder.set_base(base).build().unwrap().expect_in_range()
|
||||
}
|
||||
|
||||
fn at_utc(builder: DateTimeBuilder) -> Zoned {
|
||||
@@ -299,6 +300,22 @@ mod tests {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn assert_extended_datetime(input: &str, base: Zoned, expected: &str) {
|
||||
let parsed = parse_at_date(base, input).unwrap();
|
||||
assert!(
|
||||
matches!(parsed, ParsedDateTime::Extended(_)),
|
||||
"expected extended datetime, got in-range for: {input}"
|
||||
);
|
||||
assert_eq!(parsed.to_string(), expected, "{input}");
|
||||
}
|
||||
|
||||
fn expect_extended_datetime(parsed: ParsedDateTime) -> crate::ExtendedDateTime {
|
||||
match parsed {
|
||||
ParsedDateTime::Extended(dt) => dt,
|
||||
ParsedDateTime::InRange(z) => panic!("expected extended datetime, got in-range: {z}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn date_and_time() {
|
||||
assert_eq!(
|
||||
@@ -360,6 +377,14 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn date_and_time_ampm() {
|
||||
// https://github.com/uutils/parse_datetime/issues/282
|
||||
assert_eq!("12:00", test_eq_fmt("%H:%M", "2024-06-15 12:00 PM"));
|
||||
assert_eq!("11:30", test_eq_fmt("%H:%M", "2024-06-15 11:30am"));
|
||||
assert_eq!("15:00", test_eq_fmt("%H:%M", "2024-06-15 3:00 PM"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
let result = parse(&mut "");
|
||||
@@ -408,13 +433,12 @@ mod tests {
|
||||
let result = parse(&mut "2025-05-19 @1690466034");
|
||||
assert!(result.is_err());
|
||||
|
||||
// Pure number as year (too large).
|
||||
// Pure number as year (large years are accepted).
|
||||
let result = parse(&mut "jul 18 12:30 10000");
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("year must be no greater than 9999"));
|
||||
assert!(result.is_ok());
|
||||
let built = result.unwrap().build().unwrap();
|
||||
let dt = expect_extended_datetime(built);
|
||||
assert_eq!(dt.year, 10000);
|
||||
|
||||
// Pure number as time (too long).
|
||||
let result = parse(&mut "01:02 12345");
|
||||
@@ -449,6 +473,184 @@ mod tests {
|
||||
.contains("invalid minute in pure number"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_base_year_with_yearless_date_errors() {
|
||||
let base = DateTime::new(-1, 1, 1, 0, 0, 0, 0)
|
||||
.unwrap()
|
||||
.to_zoned(TimeZone::UTC)
|
||||
.unwrap();
|
||||
let result = parse_at_date(base, "11/14");
|
||||
assert!(result.is_err());
|
||||
assert!(result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("base year must be non-negative"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn boundary_rollover_from_9999_falls_back_to_extended() {
|
||||
let base = "2000-01-01 00:00:00"
|
||||
.parse::<DateTime>()
|
||||
.unwrap()
|
||||
.to_zoned(TimeZone::UTC)
|
||||
.unwrap();
|
||||
assert_extended_datetime("9999-12-31 +1 day", base, "10000-01-01 00:00:00+00:00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn boundary_year_9999_absolute_date_parses() {
|
||||
let base = "2000-01-01 00:00:00"
|
||||
.parse::<DateTime>()
|
||||
.unwrap()
|
||||
.to_zoned(TimeZone::UTC)
|
||||
.unwrap();
|
||||
assert_extended_datetime("9999-12-31", base, "9999-12-31 00:00:00+00:00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn boundary_year_9999_with_time_parses() {
|
||||
let base = "2000-01-01 00:00:00"
|
||||
.parse::<DateTime>()
|
||||
.unwrap()
|
||||
.to_zoned(TimeZone::UTC)
|
||||
.unwrap();
|
||||
assert_extended_datetime("9999-12-31 12:00", base, "9999-12-31 12:00:00+00:00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn boundary_year_9999_with_utc_timezone_parses() {
|
||||
let base = "2000-01-01 00:00:00"
|
||||
.parse::<DateTime>()
|
||||
.unwrap()
|
||||
.to_zoned(TimeZone::UTC)
|
||||
.unwrap();
|
||||
assert_extended_datetime("9999-12-31 12:00 UTC", base, "9999-12-31 12:00:00+00:00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn boundary_year_9999_with_explicit_offset_parses() {
|
||||
let base = "2000-01-01 00:00:00"
|
||||
.parse::<DateTime>()
|
||||
.unwrap()
|
||||
.to_zoned(TimeZone::UTC)
|
||||
.unwrap();
|
||||
assert_extended_datetime("9999-12-31 12:00 +01:00", base, "9999-12-31 12:00:00+01:00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_year_relative_parity_months_and_years() {
|
||||
let base = "2000-01-01 00:00:00"
|
||||
.parse::<DateTime>()
|
||||
.unwrap()
|
||||
.to_zoned(TimeZone::UTC)
|
||||
.unwrap();
|
||||
|
||||
assert_extended_datetime(
|
||||
"10000-01-31 +2 months",
|
||||
base.clone(),
|
||||
"10000-03-31 00:00:00+00:00",
|
||||
);
|
||||
assert_extended_datetime(
|
||||
"10000-01-31 +3 months",
|
||||
base.clone(),
|
||||
"10000-05-01 00:00:00+00:00",
|
||||
);
|
||||
assert_extended_datetime(
|
||||
"10000-08-31 +6 months",
|
||||
base.clone(),
|
||||
"10001-03-03 00:00:00+00:00",
|
||||
);
|
||||
assert_extended_datetime(
|
||||
"10000-05-31 3 months ago",
|
||||
base.clone(),
|
||||
"10000-03-02 00:00:00+00:00",
|
||||
);
|
||||
assert_extended_datetime("10000-02-29 +1 year", base, "10001-03-01 00:00:00+00:00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_year_above_gnu_max() {
|
||||
assert!(parse_at_local("2147485548-01-01").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_year_can_return_in_range_after_relative() {
|
||||
let base = "2000-01-01 00:00:00"
|
||||
.parse::<DateTime>()
|
||||
.unwrap()
|
||||
.to_zoned(TimeZone::UTC)
|
||||
.unwrap();
|
||||
let result = parse_at_date(base, "10000-01-01 -1000 years").unwrap();
|
||||
let z = result.expect_in_range();
|
||||
assert_eq!(
|
||||
z.strftime("%Y-%m-%d %H:%M:%S%:z").to_string(),
|
||||
"9000-01-01 00:00:00+00:00"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_year_can_return_in_range_after_relative_with_named_timezone_rule() {
|
||||
let base = "2000-01-01 00:00:00"
|
||||
.parse::<DateTime>()
|
||||
.unwrap()
|
||||
.to_zoned(TimeZone::UTC)
|
||||
.unwrap();
|
||||
let result = parse_at_date(base, "TZ=\"Europe/Paris\" 10000-01-01 -1000 years").unwrap();
|
||||
let z = result.expect_in_range();
|
||||
assert_eq!(
|
||||
z.strftime("%Y-%m-%d %H:%M:%S%:z").to_string(),
|
||||
"9000-01-01 00:00:00+01:00"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_year_time_with_explicit_offset_is_extended() {
|
||||
let base = "2000-01-01 00:00:00"
|
||||
.parse::<DateTime>()
|
||||
.unwrap()
|
||||
.to_zoned(TimeZone::UTC)
|
||||
.unwrap();
|
||||
let dt =
|
||||
expect_extended_datetime(parse_at_date(base, "10000-01-01 12:34:56+02:00").unwrap());
|
||||
assert_eq!((dt.year, dt.month, dt.day), (10000, 1, 1));
|
||||
assert_eq!((dt.hour, dt.minute, dt.second), (12, 34, 56));
|
||||
assert_eq!(dt.offset_seconds, 2 * 3600);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_at_date_returns_error_for_invalid_input() {
|
||||
let base = "2000-01-01 00:00:00"
|
||||
.parse::<DateTime>()
|
||||
.unwrap()
|
||||
.to_zoned(TimeZone::UTC)
|
||||
.unwrap();
|
||||
let result = parse_at_date(base, "not-a-date");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "expected extended datetime")]
|
||||
fn assert_extended_datetime_panics_for_in_range_input() {
|
||||
let base = "2000-01-01 00:00:00"
|
||||
.parse::<DateTime>()
|
||||
.unwrap()
|
||||
.to_zoned(TimeZone::UTC)
|
||||
.unwrap();
|
||||
assert_extended_datetime("2001-01-01", base, "2001-01-01 00:00:00+00:00");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "ParsedDateTime is not representable as jiff::Zoned")]
|
||||
fn expect_in_range_panics_for_extended_input() {
|
||||
let base = "2000-01-01 00:00:00"
|
||||
.parse::<DateTime>()
|
||||
.unwrap()
|
||||
.to_zoned(TimeZone::UTC)
|
||||
.unwrap();
|
||||
let parsed = parse_at_date(base, "10000-01-01").unwrap();
|
||||
let _ = parsed.expect_in_range();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn relative_weekday() {
|
||||
// Jan 1 2025 is a Wed
|
||||
@@ -563,11 +765,15 @@ mod tests {
|
||||
for (input, expected) in [
|
||||
(
|
||||
r#"TZ="Europe/Paris" 2025-01-02"#,
|
||||
"2025-01-02 00:00:00[Europe/Paris]".parse().unwrap(),
|
||||
"2025-01-02 00:00:00[Europe/Paris]"
|
||||
.parse::<Zoned>()
|
||||
.unwrap(),
|
||||
),
|
||||
(
|
||||
r#"TZ="Europe/Paris" 2025-01-02 03:04:05"#,
|
||||
"2025-01-02 03:04:05[Europe/Paris]".parse().unwrap(),
|
||||
"2025-01-02 03:04:05[Europe/Paris]"
|
||||
.parse::<Zoned>()
|
||||
.unwrap(),
|
||||
),
|
||||
] {
|
||||
assert_eq!(parse_build(input), expected, "{input}");
|
||||
|
||||
@@ -108,6 +108,15 @@ impl Offset {
|
||||
hour_adjustment,
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn total_seconds(&self) -> i32 {
|
||||
let secs = (self.hours as i32) * 3600 + (self.minutes as i32) * 60;
|
||||
if self.negative {
|
||||
-secs
|
||||
} else {
|
||||
secs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<(bool, u8, u8)> for Offset {
|
||||
@@ -272,6 +281,7 @@ fn timezone_name_to_offset(input: &str) -> ModalResult<Offset> {
|
||||
"w" => Ok("-10"),
|
||||
"v" => Ok("-9"),
|
||||
"utc" => Ok("+0"),
|
||||
"ut" => Ok("+0"),
|
||||
"u" => Ok("-8"),
|
||||
"t" => Ok("-7"),
|
||||
"sst" => Ok("-11"),
|
||||
@@ -414,6 +424,7 @@ mod tests {
|
||||
fn timezone_name_without_offset() {
|
||||
for (input, expected) in [
|
||||
("utc", off(false, 0, 0)), // UTC
|
||||
("ut", off(false, 0, 0)), // Universal Time = UTC (issue #280)
|
||||
("gmt", off(false, 0, 0)), // UTC
|
||||
("z", off(false, 0, 0)), // UTC
|
||||
("west", off(false, 1, 0)), // positive offset
|
||||
@@ -465,4 +476,12 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn total_seconds() {
|
||||
assert_eq!(off(false, 0, 0).total_seconds(), 0);
|
||||
assert_eq!(off(false, 5, 30).total_seconds(), 19_800);
|
||||
assert_eq!(off(true, 5, 30).total_seconds(), -19_800);
|
||||
assert_eq!(off(false, 24, 0).total_seconds(), 86_400);
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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)),
|
||||
|
||||
@@ -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,
|
||||
|
||||
Loaded 20 of 26 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user