You've already forked parse_datetime
mirror of
https://github.com/uutils/parse_datetime.git
synced 2026-06-10 16:13:15 -07:00
Compare commits
297 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 46a5ef89c9 | |||
| 226d6a0588 | |||
| 28b9f11f2e | |||
| b68c325abd | |||
| 27a626d3d5 | |||
| 1630fd025e | |||
| cf5770838c | |||
| db9ff29c02 | |||
| 4cfe8db9be | |||
| 1e7a541528 | |||
| b2bb6dfe91 | |||
| d71c6ea41c | |||
| 3b6362e7c8 | |||
| 897a50f4d5 | |||
| 96f477121f | |||
| c47ce6870a | |||
| 77e1ab2d00 | |||
| a48ad6aec3 | |||
| 0ddeec70b4 | |||
| 2bb8ac8203 | |||
| 5b52929c91 | |||
| 3d6f79d826 | |||
| 324d0bca34 | |||
| 9f75f895e0 | |||
| 5ba4b18655 | |||
| 5d869b5264 | |||
| 793d18dce7 | |||
| 7182760266 | |||
| e7a8a52a21 | |||
| 0674254bc6 | |||
| 8bebfab576 | |||
| 6feeddfea1 | |||
| 6499842e15 | |||
| 949e0613d6 | |||
| 6e6ffc2dff | |||
| 67364fc6ba | |||
| d92f5eeb27 | |||
| 5c4a60fadf | |||
| 45bda33a6b | |||
| 64069cdbf2 | |||
| 4d6568b326 | |||
| f5c1a5e399 | |||
| 706ab45937 | |||
| 4d9afba2d4 | |||
| 05677a803d | |||
| 98d4931b53 | |||
| 0b5d3ffdf3 | |||
| 24fa78a927 | |||
| c0ada32ce1 | |||
| b5b1fd2b5e | |||
| 8d78de85a5 | |||
| 33ac3b75cc | |||
| 832942c019 | |||
| e031c8ce49 | |||
| 856a13ddb2 | |||
| f1f2c47120 | |||
| 2c0fa5a264 | |||
| 937572d557 | |||
| a7d9d1ab59 | |||
| 3be37c4d82 | |||
| 95897465b2 | |||
| 7260b0ae73 | |||
| 92daee09de | |||
| 73f69b1da0 | |||
| 30cb26d75c | |||
| 6e64f21967 | |||
| 9b67307512 | |||
| 76d8f17350 | |||
| c05b8217e0 | |||
| 41a53a2610 | |||
| 03cff2ce1e | |||
| 12f73c3d57 | |||
| c694587fdd | |||
| 3fe06c0b94 | |||
| 1a6a6f688a | |||
| 294bd5eeb2 | |||
| 60745300ad | |||
| 1552608bce | |||
| ee9beb07d3 | |||
| 6f073addf5 | |||
| a9676626be | |||
| da0af495e8 | |||
| e00555cd96 | |||
| ab91b2b6d7 | |||
| cc7c143b21 | |||
| 7721ae2cd8 | |||
| 557c3a3ae3 | |||
| bea8ec2641 | |||
| 852e49e224 | |||
| 2390ef0b25 | |||
| 77156f46d2 | |||
| bc6b70bc9e | |||
| 2479c61dd5 | |||
| 4c3d2221da | |||
| 07d4b80ea5 | |||
| b481a2bdec | |||
| a387463373 | |||
| b6795d9b2e | |||
| 3bdafeb2cf | |||
| b9630d4bd3 | |||
| 65fb9140d2 | |||
| 9b22a18bce | |||
| 28b788a300 | |||
| f6b6b3e4ea | |||
| 1a93bb4e3e | |||
| 9fc966308e | |||
| 7119769488 | |||
| 46a82b70b3 | |||
| e7ed14e8a4 | |||
| 9184428407 | |||
| 25825df8b7 | |||
| 0dbce58014 | |||
| 0c0a054f70 | |||
| 52dca03c56 | |||
| d4384ceec9 | |||
| 46cc4147c7 | |||
| 93e1c9bfe7 | |||
| e7a7a0b07f | |||
| d4353d0888 | |||
| 8aee979de5 | |||
| f1fada62a6 | |||
| ca6a39c0a7 | |||
| db18db4e1d | |||
| c337aba9cb | |||
| 63cfc01e80 | |||
| 1daaeb42a2 | |||
| 3fcbac6fae | |||
| 65cd15dba9 | |||
| 65c88f3586 | |||
| 2c55db21fa | |||
| badc887606 | |||
| f8adcb6c88 | |||
| d0dceb6eaf | |||
| 45b9d9f09a | |||
| 0965130c1b | |||
| 51ddcb793f | |||
| 7985aa04ba | |||
| 5c0c9c4405 | |||
| 8dd8051eb7 | |||
| 073b9f7b70 | |||
| 0318dabe3a | |||
| 57fe123e21 | |||
| 2fe83cabf1 | |||
| 94d4112707 | |||
| eda2422bb3 | |||
| c948274c42 | |||
| b209341259 | |||
| 9cd22afa29 | |||
| a7061bce12 | |||
| ea49dc94f0 | |||
| 938aa9694b | |||
| 51e294724c | |||
| fa53ec79bd | |||
| 90d22a2c37 | |||
| 06aac3f56f | |||
| 240abf791e | |||
| 34f226fd98 | |||
| 00dc90dd10 | |||
| 3c67f4b5c9 | |||
| faf363c2e6 | |||
| 56410cebcf | |||
| d777be1bb6 | |||
| 6429ef26c5 | |||
| fa311c0dd4 | |||
| 7ab9a02761 | |||
| 78877a1b98 | |||
| 623c626b3a | |||
| a9bccfa20a | |||
| dba1374f8e | |||
| 9b266bcb17 | |||
| 5e3fc539a3 | |||
| c26d83fd6e | |||
| a841ad0fc4 | |||
| 8ca89bdb55 | |||
| 48852068de | |||
| 3813a3bae9 | |||
| 313de7ba7c | |||
| 7913ecd888 | |||
| 8a528468ae | |||
| 212c6f7c24 | |||
| cd7020f038 | |||
| e543ee64a7 | |||
| 19d49cfe73 | |||
| 86d72fc867 | |||
| bbcafc42f9 | |||
| 1570d92fc5 | |||
| e41e26fef1 | |||
| d5015a641e | |||
| 5e6bddb6f5 | |||
| ced645a4f4 | |||
| d75338a51b | |||
| d4f67bbde0 | |||
| ba19b94acd | |||
| f7f789913a | |||
| 7e84a62b9e | |||
| db8c1d364f | |||
| e1aa18d263 | |||
| a05bd1d06e | |||
| a75107c69d | |||
| 0f5b6bf16e | |||
| 1bb93d7fe9 | |||
| 138999131b | |||
| fc14854394 | |||
| be45c2734d | |||
| acf9b9c76e | |||
| 04494f6ddb | |||
| 20a39748f0 | |||
| 113100e669 | |||
| ff2113eabe | |||
| ad9a056826 | |||
| d1263e0e7e | |||
| 5b81981a8a | |||
| 85957480b3 | |||
| 0d04eeb08d | |||
| 0dddb4b726 | |||
| deffff9803 | |||
| 96d8c7c294 | |||
| 249c079790 | |||
| 261b28db22 | |||
| cdc1b64fcf | |||
| 9e27be1e1a | |||
| 9bb047eeab | |||
| 040561c235 | |||
| 9009bfc4ed | |||
| c39e01748c | |||
| 325ae3a408 | |||
| 1ded0a062c | |||
| 0047868a35 | |||
| 5d11c142b5 | |||
| 8ef448acb5 | |||
| 0ad9ef3e6b | |||
| 4dfa20c7b0 | |||
| e8ab1a46e2 | |||
| 57d3527d81 | |||
| 48255280aa | |||
| bddcc6067f | |||
| be7fe1dc12 | |||
| 70fde9fa18 | |||
| 2d05b14a2d | |||
| 96db1a281e | |||
| 93c02dc0b7 | |||
| 29d641389d | |||
| adb5fabfa7 | |||
| 02e5c22985 | |||
| c6feb9a251 | |||
| 852638fef0 | |||
| 5c34517469 | |||
| 47c5f3620b | |||
| 1e43cc945f | |||
| f9ce5047a5 | |||
| 4c3fd2b1ed | |||
| 9a7f87ac09 | |||
| 0b0aa70193 | |||
| 0c44becec8 | |||
| 64dfea4b47 | |||
| 0bdabd5303 | |||
| 2112830d8b | |||
| 2737b4a0f2 | |||
| f10749eade | |||
| a7380508bc | |||
| 7f0a909752 | |||
| ff0f99bea5 | |||
| dc0d8a3aa0 | |||
| b615effc4e | |||
| cebf55cf82 | |||
| efee20bf01 | |||
| c084001056 | |||
| a35530ed4a | |||
| 6cfcc985a8 | |||
| 497bebb9e7 | |||
| dfd43a6d65 | |||
| 51dd843ded | |||
| c24d593821 | |||
| bed2d16e67 | |||
| 7ab1ad2f0e | |||
| c1a2bef4a6 | |||
| 673d5b600e | |||
| 83e1431db0 | |||
| 80f8fdf883 | |||
| 22bdd8f49c | |||
| dc508c469b | |||
| e503de7559 | |||
| 7ee33d1fb1 | |||
| bc33770075 | |||
| 70d3e53604 | |||
| 1ba787d1c3 | |||
| 33cefd7db2 | |||
| d0d913e161 | |||
| c784a721ee | |||
| 29e7a5c335 | |||
| 8d797fcbcb | |||
| c177117428 | |||
| 03c8e81a10 | |||
| 7547f31b76 | |||
| 1c32733f38 | |||
| 5278394b7c | |||
| e53aabd914 |
+54
-55
@@ -4,6 +4,7 @@ name: Basic CI
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUST_MIN_SRV: "1.71.1"
|
||||
|
||||
jobs:
|
||||
check:
|
||||
@@ -13,15 +14,9 @@ jobs:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- run: cargo check
|
||||
|
||||
test:
|
||||
name: cargo test
|
||||
@@ -30,31 +25,18 @@ jobs:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- run: cargo test
|
||||
|
||||
fmt:
|
||||
name: cargo fmt --all -- --check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- run: rustup component add rustfmt
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
- run: cargo fmt --all -- --check
|
||||
|
||||
clippy:
|
||||
name: cargo clippy -- -D warnings
|
||||
@@ -63,17 +45,23 @@ jobs:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
profile: minimal
|
||||
toolchain: stable
|
||||
override: true
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
- run: rustup component add clippy
|
||||
- uses: actions-rs/cargo@v1
|
||||
- run: cargo clippy --all-targets -- -D warnings
|
||||
|
||||
min_version:
|
||||
name: Minimum Supported Rust Version
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macOS-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
command: clippy
|
||||
args: -- -D warnings
|
||||
toolchain: ${{ env.RUST_MIN_SRV }}
|
||||
- run: cargo test
|
||||
|
||||
coverage:
|
||||
name: Code Coverage
|
||||
@@ -86,7 +74,7 @@ jobs:
|
||||
- { os: macos-latest , features: macos }
|
||||
- { os: windows-latest , features: windows }
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/checkout@v4
|
||||
- name: Initialize workflow variables
|
||||
id: vars
|
||||
shell: bash
|
||||
@@ -101,28 +89,22 @@ jobs:
|
||||
if [ -n "${{ matrix.job.toolchain }}" ]; then TOOLCHAIN="${{ matrix.job.toolchain }}" ; fi
|
||||
outputs TOOLCHAIN
|
||||
# target-specific options
|
||||
# * CARGO_FEATURES_OPTION
|
||||
CARGO_FEATURES_OPTION='--all -- --check' ; ## default to '--all-features' for code coverage
|
||||
# * CODECOV_FLAGS
|
||||
CODECOV_FLAGS=$( echo "${{ matrix.job.os }}" | sed 's/[^[:alnum:]]/_/g' )
|
||||
outputs CODECOV_FLAGS
|
||||
|
||||
- name: rust toolchain ~ install
|
||||
uses: actions-rs/toolchain@v1
|
||||
uses: dtolnay/rust-toolchain@nightly
|
||||
with:
|
||||
toolchain: ${{ steps.vars.outputs.TOOLCHAIN }}
|
||||
default: true
|
||||
profile: minimal # minimal component installation (ie, no documentation)
|
||||
components: llvm-tools-preview
|
||||
- name: Test
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: ${{ steps.vars.outputs.CARGO_FEATURES_OPTION }} --no-fail-fast
|
||||
run: cargo test --no-fail-fast
|
||||
env:
|
||||
CARGO_INCREMENTAL: "0"
|
||||
RUSTC_WRAPPER: ""
|
||||
RUSTFLAGS: "-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort"
|
||||
RUSTFLAGS: "-Cinstrument-coverage -Zcoverage-options=branch -Ccodegen-units=1 -Copt-level=0 -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort"
|
||||
RUSTDOCFLAGS: "-Cpanic=abort"
|
||||
LLVM_PROFILE_FILE: "parse_datetime-%p-%m.profraw"
|
||||
- name: "`grcov` ~ install"
|
||||
id: build_grcov
|
||||
shell: bash
|
||||
@@ -150,17 +132,34 @@ jobs:
|
||||
COVERAGE_REPORT_FILE="${COVERAGE_REPORT_DIR}/lcov.info"
|
||||
mkdir -p "${COVERAGE_REPORT_DIR}"
|
||||
# display coverage files
|
||||
grcov . --output-type files --ignore build.rs --ignore "vendor/*" --ignore "/*" --ignore "[a-zA-Z]:/*" --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()" | sort --unique
|
||||
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 . --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\()"
|
||||
echo "report=${COVERAGE_REPORT_FILE}" >> $GITHUB_OUTPUT
|
||||
- name: Upload coverage results (to Codecov.io)
|
||||
uses: codecov/codecov-action@v3
|
||||
# if: steps.vars.outputs.HAS_CODECOV_TOKEN
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
# token: ${{ secrets.CODECOV_TOKEN }}
|
||||
file: ${{ steps.coverage.outputs.report }}
|
||||
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
|
||||
|
||||
fuzz:
|
||||
name: Run the fuzzers
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUN_FOR: 60
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
- name: Install `cargo-fuzz`
|
||||
run: cargo install cargo-fuzz
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
- name: Run from_str 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
|
||||
|
||||
Generated
+308
-147
File diff suppressed because it is too large
Load Diff
+12
-6
@@ -1,12 +1,18 @@
|
||||
[package]
|
||||
name = "humantime_to_duration"
|
||||
description = " parsing human-readable relative time strings and converting them to a Duration"
|
||||
version = "0.3.0"
|
||||
name = "parse_datetime"
|
||||
description = "parsing human-readable time strings and converting them to a DateTime"
|
||||
version = "0.11.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
repository = "https://github.com/uutils/humantime_to_duration"
|
||||
repository = "https://github.com/uutils/parse_datetime"
|
||||
readme = "README.md"
|
||||
rust-version = "1.71.1"
|
||||
|
||||
[dependencies]
|
||||
regex = "1.8"
|
||||
chrono = "0.4"
|
||||
regex = "1.10.4"
|
||||
chrono = { version="0.4.38", default-features=false, features=["std", "alloc", "clock"] }
|
||||
winnow = "0.7.10"
|
||||
num-traits = "0.2.19"
|
||||
|
||||
[dev-dependencies]
|
||||
rstest = "0.26"
|
||||
|
||||
@@ -1,47 +1,55 @@
|
||||
# humantime_to_duration
|
||||
# parse_datetime
|
||||
|
||||
[](https://crates.io/crates/humantime_to_duration)
|
||||
[](https://github.com/uutils/humantime_to_duration/blob/main/LICENSE)
|
||||
[](https://codecov.io/gh/uutils/humantime_to_duration)
|
||||
[](https://crates.io/crates/parse_datetime)
|
||||
[](https://github.com/uutils/parse_datetime/blob/main/LICENSE)
|
||||
[](https://codecov.io/gh/uutils/parse_datetime)
|
||||
|
||||
A Rust crate for parsing human-readable relative time strings and converting them to a `Duration`.
|
||||
A Rust crate for parsing human-readable relative time strings and human-readable datetime strings and converting them to a `DateTime`.
|
||||
|
||||
## Features
|
||||
|
||||
- Parses a variety of human-readable time formats.
|
||||
- Parses a variety of human-readable and standard time formats.
|
||||
- Supports positive and negative durations.
|
||||
- Allows for chaining time units (e.g., "1 hour 2 minutes" or "2 days and 2 hours").
|
||||
- Allows for chaining time units (e.g., "1 hour 2 minutes" or "2 days 2 hours ago").
|
||||
- Calculate durations relative to a specified date.
|
||||
- Relies on Chrono
|
||||
|
||||
## Usage
|
||||
|
||||
Add this to your `Cargo.toml`:
|
||||
Add `parse_datetime` to your `Cargo.toml` with:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
humantime_to_duration = "0.3.0"
|
||||
```
|
||||
cargo add parse_datetime
|
||||
```
|
||||
|
||||
Then, import the crate and use the `from_str` and `from_str_at_date` functions:
|
||||
```
|
||||
use humantime_to_duration::{from_str, from_str_at_date};
|
||||
use chrono::Duration;
|
||||
Then, import the crate and use the `parse_datetime_at_date` function:
|
||||
|
||||
let duration = from_str("+3 days");
|
||||
assert_eq!(duration.unwrap(), Duration::days(3));
|
||||
```rs
|
||||
use chrono::{Duration, Local};
|
||||
use parse_datetime::parse_datetime_at_date;
|
||||
|
||||
let now = Local::now();
|
||||
let after = parse_datetime_at_date(now, "+3 days");
|
||||
|
||||
let today = Utc::today().naive_utc();
|
||||
let yesterday = today - Duration::days(1);
|
||||
assert_eq!(
|
||||
from_str_at_date(yesterday, "2 days").unwrap(),
|
||||
Duration::days(1)
|
||||
(now + Duration::days(3)).naive_utc(),
|
||||
after.unwrap().naive_utc()
|
||||
);
|
||||
```
|
||||
|
||||
For DateTime parsing, import the `parse_datetime` function:
|
||||
|
||||
```rs
|
||||
use parse_datetime::parse_datetime;
|
||||
use chrono::{Local, TimeZone};
|
||||
|
||||
let dt = parse_datetime("2021-02-14 06:37:47");
|
||||
assert_eq!(dt.unwrap(), Local.with_ymd_and_hms(2021, 2, 14, 6, 37, 47).unwrap());
|
||||
```
|
||||
|
||||
### Supported Formats
|
||||
|
||||
The `from_str` and `from_str_at_date` functions support the following formats for relative time:
|
||||
The `parse_datetime` and `parse_datetime_at_date` functions support absolute datetime and the following relative times:
|
||||
|
||||
- `num` `unit` (e.g., "-1 hour", "+3 days")
|
||||
- `unit` (e.g., "hour", "day")
|
||||
@@ -49,28 +57,36 @@ The `from_str` and `from_str_at_date` functions support the following formats fo
|
||||
- "yesterday"
|
||||
- "tomorrow"
|
||||
- use "ago" for the past
|
||||
- combined units with "and" or "," (e.g., "2 years and 1 month", "1 day, 2 hours" or "2 weeks 1 second")
|
||||
- use "next" or "last" with `unit` (e.g., "next week", "last year")
|
||||
- unix timestamps (for example "@0" "@1344000")
|
||||
|
||||
`num` can be a positive or negative integer.
|
||||
`unit` can be one of the following: "fortnight", "week", "day", "hour", "minute", "min", "second", "sec" and their plural forms.
|
||||
|
||||
## Return Values
|
||||
|
||||
The `from_str` and `from_str_at_date` functions return:
|
||||
### parse_datetime and parse_datetime_at_date
|
||||
|
||||
- `Ok(Duration)` - If the input string can be parsed as a relative time
|
||||
- `Err(ParseDurationError)` - If the input string cannot be parsed as a relative time
|
||||
The `parse_datetime` and `parse_datetime_at_date` function return:
|
||||
|
||||
This function will return `Err(ParseDurationError::InvalidInput)` if the input string
|
||||
cannot be parsed as a relative time.
|
||||
- `Ok(DateTime<FixedOffset>)` - If the input string can be parsed as a datetime
|
||||
- `Err(ParseDateTimeError::InvalidInput)` - If the input string cannot be parsed
|
||||
|
||||
## Fuzzer
|
||||
|
||||
To run the fuzzer:
|
||||
|
||||
```
|
||||
$ cargo fuzz run fuzz_from_str
|
||||
$ cd fuzz
|
||||
$ cargo install cargo-fuzz
|
||||
$ cargo +nightly fuzz run fuzz_parse_datetime
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the [MIT License](LICENSE).
|
||||
|
||||
## Note
|
||||
|
||||
At some point, this crate was called humantime_to_duration.
|
||||
It has been renamed to cover more cases.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
corpus
|
||||
Generated
+107
-159
@@ -1,6 +1,6 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
@@ -46,11 +46,13 @@ checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.79"
|
||||
version = "1.2.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
|
||||
checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
"libc",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -61,17 +63,14 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.26"
|
||||
version = "0.4.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5"
|
||||
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
"js-sys",
|
||||
"num-traits",
|
||||
"time",
|
||||
"wasm-bindgen",
|
||||
"winapi",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -81,47 +80,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
|
||||
|
||||
[[package]]
|
||||
name = "fuzz_from_str"
|
||||
version = "0.1.0"
|
||||
name = "fuzz_parse_datetime"
|
||||
version = "0.2.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"humantime_to_duration",
|
||||
"libfuzzer-sys",
|
||||
"rand",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c85e1d9ab2eadba7e5040d4e09cbd6d072b76a557ad64e797c2cb9d4da21d7e4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "humantime_to_duration"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"parse_datetime",
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
version = "0.1.56"
|
||||
version = "0.1.61"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c"
|
||||
checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
|
||||
dependencies = [
|
||||
"android_system_properties",
|
||||
"core-foundation-sys",
|
||||
"iana-time-zone-haiku",
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
"windows",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -135,18 +114,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.26"
|
||||
version = "0.1.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2"
|
||||
checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.63"
|
||||
version = "0.3.70"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2f37a4a5928311ac501dee68b3c7613a1037d0edb30c8e5427bd832d55d1b790"
|
||||
checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a"
|
||||
dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
@@ -159,13 +138,12 @@ checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317"
|
||||
|
||||
[[package]]
|
||||
name = "libfuzzer-sys"
|
||||
version = "0.4.6"
|
||||
version = "0.4.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "beb09950ae85a0a94b27676cccf37da5ff13f27076aa1adbc6545dd0d0e1bd4e"
|
||||
checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404"
|
||||
dependencies = [
|
||||
"arbitrary",
|
||||
"cc",
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -176,15 +154,15 @@ checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.5.0"
|
||||
version = "2.6.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
|
||||
checksum = "5486aed0026218e61b8a01d5fbd5a0a134649abb71a0e53b7bc088529dced86e"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.15"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
@@ -196,10 +174,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
|
||||
name = "parse_datetime"
|
||||
version = "0.10.0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"num-traits",
|
||||
"regex",
|
||||
"winnow",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
@@ -219,41 +201,23 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"rand_chacha",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_chacha"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
|
||||
dependencies = [
|
||||
"ppv-lite86",
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rand_core"
|
||||
version = "0.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.8.3"
|
||||
version = "1.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81ca098a9821bd52d6b24fd8b10bd081f47d39c22778cafaa75a2857a62c6390"
|
||||
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -262,9 +226,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.7.2"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78"
|
||||
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "1.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
@@ -277,50 +247,28 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.1.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi 0.10.0+wasi-snapshot-preview1",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.10.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.11.0+wasi-snapshot-preview1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.86"
|
||||
version = "0.2.93"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73"
|
||||
checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
"wasm-bindgen-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-backend"
|
||||
version = "0.2.86"
|
||||
version = "0.2.93"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19b04bc93f9d6bdee709f6bd2118f57dd6679cf1176a1af464fca3ab0d66d8fb"
|
||||
checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
@@ -333,9 +281,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.86"
|
||||
version = "0.2.93"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "14d6b024f1a526bb0234f52840389927257beb670610081360e5a03c5df9c258"
|
||||
checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
@@ -343,9 +291,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.86"
|
||||
version = "0.2.93"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8"
|
||||
checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -356,50 +304,35 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.86"
|
||||
version = "0.2.93"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93"
|
||||
checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
name = "windows-core"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.48.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
|
||||
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.48.0"
|
||||
name = "windows-link"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5"
|
||||
checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3"
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_gnullvm",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
@@ -408,42 +341,57 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.48.0"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
|
||||
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.48.0"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
|
||||
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.48.0"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
|
||||
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnullvm"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.48.0"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
|
||||
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.48.0"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
|
||||
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.48.0"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
|
||||
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.48.0"
|
||||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
+9
-10
@@ -1,22 +1,21 @@
|
||||
[package]
|
||||
name = "fuzz_from_str"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
name = "fuzz_parse_datetime"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
|
||||
[package.metadata]
|
||||
cargo-fuzz = true
|
||||
|
||||
[dependencies]
|
||||
rand = "0.8.5"
|
||||
libfuzzer-sys = "0.4"
|
||||
regex = "1.8.3"
|
||||
chrono = "0.4"
|
||||
libfuzzer-sys = "0.4.7"
|
||||
regex = "1.10.4"
|
||||
chrono = { version="0.4", default-features=false, features=["std", "alloc", "clock"] }
|
||||
|
||||
[dependencies.humantime_to_duration]
|
||||
[dependencies.parse_datetime]
|
||||
path = "../"
|
||||
|
||||
[[bin]]
|
||||
name = "fuzz_from_str"
|
||||
path = "fuzz_targets/from_str.rs"
|
||||
name = "fuzz_parse_datetime"
|
||||
path = "fuzz_targets/parse_datetime.rs"
|
||||
test = false
|
||||
doc = false
|
||||
|
||||
@@ -4,5 +4,5 @@ use libfuzzer_sys::fuzz_target;
|
||||
|
||||
fuzz_target!(|data: &[u8]| {
|
||||
let s = std::str::from_utf8(data).unwrap_or("");
|
||||
let _ = humantime_to_duration::from_str(s);
|
||||
let _ = parse_datetime::parse_datetime(s);
|
||||
});
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:base"
|
||||
"config:recommended"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,320 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
use chrono::{DateTime, Datelike, FixedOffset, NaiveDate, TimeZone, Timelike};
|
||||
|
||||
use super::{date, relative, time, weekday};
|
||||
|
||||
/// The builder is used to construct a DateTime object from various components.
|
||||
/// The parser creates a `DateTimeBuilder` object with the parsed components,
|
||||
/// but without the baseline date and time. So you normally need to set the base
|
||||
/// date and time using the `set_base()` method before calling `build()`, or
|
||||
/// leave it unset to use the current date and time as the base.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DateTimeBuilder {
|
||||
base: Option<DateTime<FixedOffset>>,
|
||||
timestamp: Option<f64>,
|
||||
date: Option<date::Date>,
|
||||
time: Option<time::Time>,
|
||||
weekday: Option<weekday::Weekday>,
|
||||
timezone: Option<time::Offset>,
|
||||
relative: Vec<relative::Relative>,
|
||||
}
|
||||
|
||||
impl DateTimeBuilder {
|
||||
pub(super) fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
/// Sets the base date and time for the builder. If not set, the current
|
||||
/// date and time will be used.
|
||||
pub(super) fn set_base(mut self, base: DateTime<FixedOffset>) -> Self {
|
||||
self.base = Some(base);
|
||||
self
|
||||
}
|
||||
|
||||
/// Timestamp value is exclusive to other date/time components. Caller of
|
||||
/// the builder must ensure that it is not combined with other items.
|
||||
pub(super) fn set_timestamp(mut self, ts: f64) -> Result<Self, &'static str> {
|
||||
self.timestamp = Some(ts);
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub(super) fn set_year(mut self, year: u32) -> Result<Self, &'static str> {
|
||||
if let Some(date) = self.date.as_mut() {
|
||||
if date.year.is_some() {
|
||||
Err("year cannot appear more than once")
|
||||
} else {
|
||||
date.year = Some(year);
|
||||
Ok(self)
|
||||
}
|
||||
} else {
|
||||
self.date = Some(date::Date {
|
||||
day: 1,
|
||||
month: 1,
|
||||
year: Some(year),
|
||||
});
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn set_date(mut self, date: date::Date) -> Result<Self, &'static str> {
|
||||
if self.date.is_some() || self.timestamp.is_some() {
|
||||
Err("date cannot appear more than once")
|
||||
} else {
|
||||
self.date = Some(date);
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn set_time(mut self, time: time::Time) -> Result<Self, &'static str> {
|
||||
if self.time.is_some() || self.timestamp.is_some() {
|
||||
Err("time cannot appear more than once")
|
||||
} else if self.timezone.is_some() && time.offset.is_some() {
|
||||
Err("time offset and timezone are mutually exclusive")
|
||||
} else {
|
||||
self.time = Some(time);
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn set_weekday(mut self, weekday: weekday::Weekday) -> Result<Self, &'static str> {
|
||||
if self.weekday.is_some() {
|
||||
Err("weekday cannot appear more than once")
|
||||
} else {
|
||||
self.weekday = Some(weekday);
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn set_timezone(mut self, timezone: time::Offset) -> Result<Self, &'static str> {
|
||||
if self.timezone.is_some() {
|
||||
Err("timezone cannot appear more than once")
|
||||
} else if self.time.as_ref().and_then(|t| t.offset.as_ref()).is_some() {
|
||||
Err("time offset and timezone are mutually exclusive")
|
||||
} else {
|
||||
self.timezone = Some(timezone);
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn push_relative(mut self, relative: relative::Relative) -> Self {
|
||||
self.relative.push(relative);
|
||||
self
|
||||
}
|
||||
|
||||
pub(super) fn build(self) -> Option<DateTime<FixedOffset>> {
|
||||
let base = self.base.unwrap_or_else(|| chrono::Local::now().into());
|
||||
|
||||
// If any of the following items are set, we truncate the time portion
|
||||
// of the base date to zero; otherwise, we use the base date as is.
|
||||
let mut dt = if self.timestamp.is_none()
|
||||
&& self.date.is_none()
|
||||
&& self.time.is_none()
|
||||
&& self.weekday.is_none()
|
||||
&& self.timezone.is_none()
|
||||
{
|
||||
base
|
||||
} else {
|
||||
new_date(
|
||||
base.year(),
|
||||
base.month(),
|
||||
base.day(),
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
*base.offset(),
|
||||
)?
|
||||
};
|
||||
|
||||
if let Some(ts) = self.timestamp {
|
||||
// TODO: How to make the fract -> nanosecond conversion more precise?
|
||||
// Maybe considering using the
|
||||
// [rust_decimal](https://crates.io/crates/rust_decimal) crate?
|
||||
match chrono::Utc.timestamp_opt(ts as i64, (ts.fract() * 10f64.powi(9)).round() as u32)
|
||||
{
|
||||
chrono::MappedLocalTime::Single(t) => {
|
||||
// If the timestamp is valid, we can use it directly.
|
||||
dt = t.with_timezone(&dt.timezone());
|
||||
}
|
||||
chrono::MappedLocalTime::Ambiguous(earliest, _latest) => {
|
||||
// TODO: When there is a fold in the local time, which value
|
||||
// do we choose? For now, we use the earliest one.
|
||||
dt = earliest.with_timezone(&dt.timezone());
|
||||
}
|
||||
chrono::MappedLocalTime::None => {
|
||||
return None; // Invalid timestamp
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(date::Date { year, month, day }) = self.date {
|
||||
dt = new_date(
|
||||
year.map(|x| x as i32).unwrap_or(dt.year()),
|
||||
month,
|
||||
day,
|
||||
dt.hour(),
|
||||
dt.minute(),
|
||||
dt.second(),
|
||||
dt.nanosecond(),
|
||||
*dt.offset(),
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(time::Time {
|
||||
hour,
|
||||
minute,
|
||||
second,
|
||||
ref offset,
|
||||
}) = self.time
|
||||
{
|
||||
let offset = offset
|
||||
.clone()
|
||||
.and_then(|o| chrono::FixedOffset::try_from(o).ok())
|
||||
.unwrap_or(*dt.offset());
|
||||
|
||||
dt = new_date(
|
||||
dt.year(),
|
||||
dt.month(),
|
||||
dt.day(),
|
||||
hour,
|
||||
minute,
|
||||
second as u32,
|
||||
(second.fract() * 10f64.powi(9)).round() as u32,
|
||||
offset,
|
||||
)?;
|
||||
}
|
||||
|
||||
if let Some(weekday::Weekday { offset, day }) = self.weekday {
|
||||
if self.time.is_none() {
|
||||
dt = new_date(dt.year(), dt.month(), dt.day(), 0, 0, 0, 0, *dt.offset())?;
|
||||
}
|
||||
|
||||
let mut offset = offset;
|
||||
let day = day.into();
|
||||
|
||||
// If the current day is not the target day, we need to adjust
|
||||
// the x value to ensure we find the correct day.
|
||||
//
|
||||
// Consider this:
|
||||
// Assuming today is Monday, next Friday is actually THIS Friday;
|
||||
// but next Monday is indeed NEXT Monday.
|
||||
if dt.weekday() != day && offset > 0 {
|
||||
offset -= 1;
|
||||
}
|
||||
|
||||
// Calculate the delta to the target day.
|
||||
//
|
||||
// Assuming today is Thursday, here are some examples:
|
||||
//
|
||||
// Example 1: last Thursday (x = -1, day = Thursday)
|
||||
// delta = (3 - 3) % 7 + (-1) * 7 = -7
|
||||
//
|
||||
// Example 2: last Monday (x = -1, day = Monday)
|
||||
// delta = (0 - 3) % 7 + (-1) * 7 = -3
|
||||
//
|
||||
// Example 3: next Monday (x = 1, day = Monday)
|
||||
// delta = (0 - 3) % 7 + (0) * 7 = 4
|
||||
// (Note that we have adjusted the x value above)
|
||||
//
|
||||
// Example 4: next Thursday (x = 1, day = Thursday)
|
||||
// delta = (3 - 3) % 7 + (1) * 7 = 7
|
||||
let delta = (day.num_days_from_monday() as i32
|
||||
- dt.weekday().num_days_from_monday() as i32)
|
||||
.rem_euclid(7)
|
||||
+ offset.checked_mul(7)?;
|
||||
|
||||
dt = if delta < 0 {
|
||||
dt.checked_sub_days(chrono::Days::new((-delta) as u64))?
|
||||
} else {
|
||||
dt.checked_add_days(chrono::Days::new(delta as u64))?
|
||||
}
|
||||
}
|
||||
|
||||
for rel in self.relative {
|
||||
// TODO: Handle potential overflows in the addition operations.
|
||||
match rel {
|
||||
relative::Relative::Years(x) => {
|
||||
dt = dt.with_year(dt.year() + x)?;
|
||||
}
|
||||
relative::Relative::Months(x) => {
|
||||
// *NOTE* This is done in this way to conform to
|
||||
// GNU behavior.
|
||||
let days = last_day_of_month(dt.year(), dt.month());
|
||||
if x >= 0 {
|
||||
dt += dt
|
||||
.date_naive()
|
||||
.checked_add_days(chrono::Days::new((days * x as u32) as u64))?
|
||||
.signed_duration_since(dt.date_naive());
|
||||
} else {
|
||||
dt += dt
|
||||
.date_naive()
|
||||
.checked_sub_days(chrono::Days::new((days * -x as u32) as u64))?
|
||||
.signed_duration_since(dt.date_naive());
|
||||
}
|
||||
}
|
||||
relative::Relative::Days(x) => dt += chrono::Duration::days(x.into()),
|
||||
relative::Relative::Hours(x) => dt += chrono::Duration::hours(x.into()),
|
||||
relative::Relative::Minutes(x) => {
|
||||
dt += chrono::Duration::try_minutes(x.into())?;
|
||||
}
|
||||
// Seconds are special because they can be given as a float.
|
||||
relative::Relative::Seconds(x) => {
|
||||
dt += chrono::Duration::try_seconds(x as i64)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(offset) = self.timezone {
|
||||
dt = with_timezone_restore(offset, dt)?;
|
||||
}
|
||||
|
||||
Some(dt)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn new_date(
|
||||
year: i32,
|
||||
month: u32,
|
||||
day: u32,
|
||||
hour: u32,
|
||||
minute: u32,
|
||||
second: u32,
|
||||
nano: u32,
|
||||
offset: FixedOffset,
|
||||
) -> Option<DateTime<FixedOffset>> {
|
||||
let newdate = NaiveDate::from_ymd_opt(year, month, day)
|
||||
.and_then(|naive| naive.and_hms_nano_opt(hour, minute, second, nano))?;
|
||||
|
||||
Some(DateTime::<FixedOffset>::from_local(newdate, offset))
|
||||
}
|
||||
|
||||
/// Restores year, month, day, etc after applying the timezone
|
||||
/// returns None if timezone overflows the date
|
||||
fn with_timezone_restore(
|
||||
offset: time::Offset,
|
||||
at: DateTime<FixedOffset>,
|
||||
) -> Option<DateTime<FixedOffset>> {
|
||||
let offset: FixedOffset = chrono::FixedOffset::try_from(offset).ok()?;
|
||||
let copy = at;
|
||||
let x = at
|
||||
.with_timezone(&offset)
|
||||
.with_day(copy.day())?
|
||||
.with_month(copy.month())?
|
||||
.with_year(copy.year())?
|
||||
.with_hour(copy.hour())?
|
||||
.with_minute(copy.minute())?
|
||||
.with_second(copy.second())?
|
||||
.with_nanosecond(copy.nanosecond())?;
|
||||
Some(x)
|
||||
}
|
||||
|
||||
fn last_day_of_month(year: i32, month: u32) -> u32 {
|
||||
NaiveDate::from_ymd_opt(year, month + 1, 1)
|
||||
.unwrap_or(NaiveDate::from_ymd_opt(year + 1, 1, 1).unwrap())
|
||||
.pred_opt()
|
||||
.unwrap()
|
||||
.day()
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
//! Parse an ISO 8601 date and time item
|
||||
//!
|
||||
//! The GNU docs state:
|
||||
//!
|
||||
//! > The ISO 8601 date and time of day extended format consists of an ISO 8601
|
||||
//! > date, a ‘T’ character separator, and an ISO 8601 time of day. This format
|
||||
//! > is also recognized if the ‘T’ is replaced by a space.
|
||||
//! >
|
||||
//! > In this format, the time of day should use 24-hour notation. Fractional
|
||||
//! > seconds are allowed, with either comma or period preceding the fraction.
|
||||
//! > ISO 8601 fractional minutes and hours are not supported. Typically, hosts
|
||||
//! > support nanosecond timestamp resolution; excess precision is silently
|
||||
//! > discarded.
|
||||
use winnow::{
|
||||
combinator::{alt, trace},
|
||||
seq, ModalResult, Parser,
|
||||
};
|
||||
|
||||
use crate::items::space;
|
||||
|
||||
use super::{
|
||||
date::{self, Date},
|
||||
primitive::s,
|
||||
time::{self, Time},
|
||||
};
|
||||
|
||||
#[derive(PartialEq, Debug, Clone, Default)]
|
||||
pub(crate) struct DateTime {
|
||||
pub(crate) date: Date,
|
||||
pub(crate) 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)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{parse, DateTime};
|
||||
use crate::items::{date::Date, time::Time};
|
||||
|
||||
#[test]
|
||||
fn some_date() {
|
||||
let reference = Some(DateTime {
|
||||
date: Date {
|
||||
day: 10,
|
||||
month: 10,
|
||||
year: Some(2022),
|
||||
},
|
||||
time: Time {
|
||||
hour: 10,
|
||||
minute: 10,
|
||||
second: 55.0,
|
||||
offset: None,
|
||||
},
|
||||
});
|
||||
|
||||
for mut s in [
|
||||
"2022-10-10t10:10:55",
|
||||
"2022-10-10 10:10:55",
|
||||
"2022-10-10 t 10:10:55",
|
||||
"2022-10-10 10:10:55",
|
||||
"2022-10-10 (A comment!) t 10:10:55",
|
||||
"2022-10-10 (A comment!) 10:10:55",
|
||||
] {
|
||||
let old_s = s.to_owned();
|
||||
assert_eq!(parse(&mut s).ok(), reference, "Failed string: {old_s}")
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
use winnow::{combinator::preceded, ModalResult, Parser};
|
||||
|
||||
use super::primitive::{float, s};
|
||||
|
||||
/// Parse a timestamp in the form of `@1234567890`.
|
||||
pub fn parse(input: &mut &str) -> ModalResult<f64> {
|
||||
s(preceded("@", float)).parse_next(input)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::parse;
|
||||
|
||||
fn float_eq(a: f64, b: f64) -> bool {
|
||||
(a - b).abs() < f64::EPSILON
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn float() {
|
||||
let mut input = "@1234567890";
|
||||
assert!(float_eq(parse(&mut input).unwrap(), 1234567890.0));
|
||||
|
||||
let mut input = "@1234567890.12345";
|
||||
assert!(float_eq(parse(&mut input).unwrap(), 1234567890.12345));
|
||||
|
||||
let mut input = "@1234567890,12345";
|
||||
assert!(float_eq(parse(&mut input).unwrap(), 1234567890.12345));
|
||||
|
||||
let mut input = "@-1234567890.12345";
|
||||
assert_eq!(parse(&mut input).unwrap(), -1234567890.12345);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,47 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
use winnow::{
|
||||
ascii::alpha1,
|
||||
combinator::{alt, opt},
|
||||
ModalResult, Parser,
|
||||
};
|
||||
|
||||
use super::primitive::{dec_uint, s};
|
||||
|
||||
pub(super) fn ordinal(input: &mut &str) -> ModalResult<i32> {
|
||||
alt((text_ordinal, number_ordinal)).parse_next(input)
|
||||
}
|
||||
|
||||
fn number_ordinal(input: &mut &str) -> ModalResult<i32> {
|
||||
let sign = opt(alt(('+'.value(1), '-'.value(-1)))).map(|s| s.unwrap_or(1));
|
||||
(s(sign), s(dec_uint))
|
||||
.verify_map(|(s, u): (i32, u32)| {
|
||||
let i: i32 = u.try_into().ok()?;
|
||||
Some(s * i)
|
||||
})
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
fn text_ordinal(input: &mut &str) -> ModalResult<i32> {
|
||||
s(alpha1)
|
||||
.verify_map(|s: &str| {
|
||||
Some(match s {
|
||||
"last" => -1,
|
||||
"this" => 0,
|
||||
"next" | "first" => 1,
|
||||
"third" => 3,
|
||||
"fourth" => 4,
|
||||
"fifth" => 5,
|
||||
"sixth" => 6,
|
||||
"seventh" => 7,
|
||||
"eight" => 8,
|
||||
"ninth" => 9,
|
||||
"tenth" => 10,
|
||||
"eleventh" => 11,
|
||||
"twelfth" => 12,
|
||||
_ => return None,
|
||||
})
|
||||
})
|
||||
.parse_next(input)
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
//! Primitive combinators.
|
||||
|
||||
use winnow::{
|
||||
ascii::{digit1, multispace0},
|
||||
combinator::{alt, delimited, not, opt, peek, preceded, repeat, separated},
|
||||
error::{ContextError, ParserError, StrContext, StrContextValue},
|
||||
stream::AsChar,
|
||||
token::{none_of, one_of, take_while},
|
||||
Parser,
|
||||
};
|
||||
|
||||
/// Allow spaces and comments before a parser
|
||||
///
|
||||
/// Every token parser should be wrapped in this to allow spaces and comments.
|
||||
/// It is only preceding, because that allows us to check mandatory whitespace
|
||||
/// after running the parser.
|
||||
pub(super) fn s<'a, O, E>(p: impl Parser<&'a str, O, E>) -> impl Parser<&'a str, O, E>
|
||||
where
|
||||
E: ParserError<&'a str>,
|
||||
{
|
||||
preceded(space, p)
|
||||
}
|
||||
|
||||
/// Parse the space in-between tokens
|
||||
///
|
||||
/// You probably want to use the [`s`] combinator instead.
|
||||
pub(super) fn space<'a, E>(input: &mut &'a str) -> winnow::Result<(), E>
|
||||
where
|
||||
E: ParserError<&'a str>,
|
||||
{
|
||||
separated(0.., multispace0, alt((comment, ignored_hyphen_or_plus))).parse_next(input)
|
||||
}
|
||||
|
||||
/// A hyphen or plus is ignored when it is not followed by a digit
|
||||
///
|
||||
/// This includes being followed by a comment! Compare these inputs:
|
||||
/// ```txt
|
||||
/// - 12 weeks
|
||||
/// - (comment) 12 weeks
|
||||
/// ```
|
||||
/// The last comment should be ignored.
|
||||
///
|
||||
/// The plus is undocumented, but it seems to be ignored.
|
||||
fn ignored_hyphen_or_plus<'a, E>(input: &mut &'a str) -> winnow::Result<(), E>
|
||||
where
|
||||
E: ParserError<&'a str>,
|
||||
{
|
||||
(
|
||||
alt(('-', '+')),
|
||||
multispace0,
|
||||
peek(not(take_while(1, AsChar::is_dec_digit))),
|
||||
)
|
||||
.void()
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse a comment
|
||||
///
|
||||
/// A comment is given between parentheses, which must be balanced. Any other
|
||||
/// tokens can be within the comment.
|
||||
fn comment<'a, E>(input: &mut &'a str) -> winnow::Result<(), E>
|
||||
where
|
||||
E: ParserError<&'a str>,
|
||||
{
|
||||
delimited(
|
||||
'(',
|
||||
repeat(0.., alt((none_of(['(', ')']).void(), comment))),
|
||||
')',
|
||||
)
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse a signed decimal integer.
|
||||
///
|
||||
/// Rationale for not using `winnow::ascii::dec_int`: When upgrading winnow from
|
||||
/// 0.5 to 0.7, we discovered that `winnow::ascii::dec_int` now accepts only the
|
||||
/// following two forms:
|
||||
///
|
||||
/// - 0
|
||||
/// - [+-]?[1-9][0-9]*
|
||||
///
|
||||
/// Inputs like [+-]?0[0-9]* (e.g., `+012`) are therefore rejected. We provide a
|
||||
/// custom implementation to support such zero-prefixed integers.
|
||||
#[allow(unused)]
|
||||
pub(super) fn dec_int<'a, E>(input: &mut &'a str) -> winnow::Result<i32, E>
|
||||
where
|
||||
E: ParserError<&'a str>,
|
||||
{
|
||||
(opt(one_of(['+', '-'])), digit1)
|
||||
.void()
|
||||
.take()
|
||||
.verify_map(|s: &str| s.parse().ok())
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse an unsigned decimal integer.
|
||||
///
|
||||
/// See the rationale for `dec_int` for why we don't use
|
||||
/// `winnow::ascii::dec_uint`.
|
||||
pub(super) fn dec_uint<'a, E>(input: &mut &'a str) -> winnow::Result<u32, E>
|
||||
where
|
||||
E: ParserError<&'a str>,
|
||||
{
|
||||
digit1
|
||||
.void()
|
||||
.take()
|
||||
.verify_map(|s: &str| s.parse().ok())
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
/// Parse a float number.
|
||||
///
|
||||
/// Rationale for not using `winnow::ascii::float`: the `float` parser provided
|
||||
/// by winnow accepts E-notation numbers (e.g., `1.23e4`), whereas GNU date
|
||||
/// rejects such numbers. To remain compatible with GNU date, we provide a
|
||||
/// custom implementation that only accepts inputs like [+-]?[0-9]+(\.[0-9]+)?.
|
||||
pub(super) fn float<'a, E>(input: &mut &'a str) -> winnow::Result<f64, E>
|
||||
where
|
||||
E: ParserError<&'a str>,
|
||||
{
|
||||
(
|
||||
opt(one_of(['+', '-'])),
|
||||
digit1,
|
||||
opt(preceded(one_of(['.', ',']), digit1)),
|
||||
)
|
||||
.void()
|
||||
.take()
|
||||
.verify_map(|s: &str| s.replace(",", ".").parse().ok())
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
/// Create a context error with a reason.
|
||||
pub(super) fn ctx_err(reason: &'static str) -> ContextError {
|
||||
let mut err = ContextError::new();
|
||||
err.push(StrContext::Expected(StrContextValue::Description(reason)));
|
||||
err
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
//! Parse a relative datetime item
|
||||
//!
|
||||
//! The GNU docs state:
|
||||
//!
|
||||
//! > The unit of time displacement may be selected by the string ‘year’ or
|
||||
//! > ‘month’ for moving by whole years or months. These are fuzzy units, as
|
||||
//! > years and months are not all of equal duration. More precise units are
|
||||
//! > ‘fortnight’ which is worth 14 days, ‘week’ worth 7 days, ‘day’ worth 24
|
||||
//! > hours, ‘hour’ worth 60 minutes, ‘minute’ or ‘min’ worth 60 seconds, and
|
||||
//! > ‘second’ or ‘sec’ worth one second. An ‘s’ suffix on these units is
|
||||
//! > accepted and ignored.
|
||||
//! >
|
||||
//! > The unit of time may be preceded by a multiplier, given as an optionally
|
||||
//! > signed number. Unsigned numbers are taken as positively signed. No number
|
||||
//! > at all implies 1 for a multiplier. Following a relative item by the
|
||||
//! > string ‘ago’ is equivalent to preceding the unit by a multiplier with
|
||||
//! > value -1.
|
||||
//! >
|
||||
//! > The string ‘tomorrow’ is worth one day in the future (equivalent to
|
||||
//! > ‘day’), the string ‘yesterday’ is worth one day in the past (equivalent
|
||||
//! > to ‘day ago’).
|
||||
//! >
|
||||
//! > The strings ‘now’ or ‘today’ are relative items corresponding to
|
||||
//! > zero-valued time displacement, these strings come from the fact a
|
||||
//! > zero-valued time displacement represents the current time when not
|
||||
//! > otherwise changed by previous items. They may be used to stress other
|
||||
//! > items, like in ‘12:00 today’. The string ‘this’ also has the meaning of a
|
||||
//! > zero-valued time displacement, but is preferred in date strings like
|
||||
//! > ‘this thursday’.
|
||||
|
||||
use winnow::{
|
||||
ascii::alpha1,
|
||||
combinator::{alt, opt},
|
||||
ModalResult, Parser,
|
||||
};
|
||||
|
||||
use super::{
|
||||
ordinal::ordinal,
|
||||
primitive::{float, s},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub enum Relative {
|
||||
Years(i32),
|
||||
Months(i32),
|
||||
Days(i32),
|
||||
Hours(i32),
|
||||
Minutes(i32),
|
||||
// Seconds are special because they can be given as a float
|
||||
Seconds(f64),
|
||||
}
|
||||
|
||||
impl Relative {
|
||||
// TODO: determine how to handle multiplication overflows,
|
||||
// using saturating_mul for now.
|
||||
fn mul(self, n: i32) -> Self {
|
||||
match self {
|
||||
Self::Years(x) => Self::Years(n.saturating_mul(x)),
|
||||
Self::Months(x) => Self::Months(n.saturating_mul(x)),
|
||||
Self::Days(x) => Self::Days(n.saturating_mul(x)),
|
||||
Self::Hours(x) => Self::Hours(n.saturating_mul(x)),
|
||||
Self::Minutes(x) => Self::Minutes(n.saturating_mul(x)),
|
||||
Self::Seconds(x) => Self::Seconds(f64::from(n) * x),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(input: &mut &str) -> ModalResult<Relative> {
|
||||
alt((
|
||||
s("tomorrow").value(Relative::Days(1)),
|
||||
s("yesterday").value(Relative::Days(-1)),
|
||||
// For "today" and "now", the unit is arbitrary
|
||||
s("today").value(Relative::Days(0)),
|
||||
s("now").value(Relative::Days(0)),
|
||||
seconds,
|
||||
other,
|
||||
))
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
fn seconds(input: &mut &str) -> ModalResult<Relative> {
|
||||
(
|
||||
opt(alt((s(float), ordinal.map(|x| x as f64)))),
|
||||
s(alpha1).verify(|s: &str| matches!(s, "seconds" | "second" | "sec" | "secs")),
|
||||
ago,
|
||||
)
|
||||
.map(|(n, _, ago)| Relative::Seconds(n.unwrap_or(1.0) * if ago { -1.0 } else { 1.0 }))
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
fn other(input: &mut &str) -> ModalResult<Relative> {
|
||||
(opt(ordinal), integer_unit, ago)
|
||||
.map(|(n, unit, ago)| unit.mul(n.unwrap_or(1) * if ago { -1 } else { 1 }))
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
fn ago(input: &mut &str) -> ModalResult<bool> {
|
||||
opt(s("ago")).map(|o| o.is_some()).parse_next(input)
|
||||
}
|
||||
|
||||
fn integer_unit(input: &mut &str) -> ModalResult<Relative> {
|
||||
s(alpha1)
|
||||
.verify_map(|s: &str| {
|
||||
Some(match s.strip_suffix('s').unwrap_or(s) {
|
||||
"year" => Relative::Years(1),
|
||||
"month" => Relative::Months(1),
|
||||
"fortnight" => Relative::Days(14),
|
||||
"week" => Relative::Days(7),
|
||||
"day" => Relative::Days(1),
|
||||
"hour" => Relative::Hours(1),
|
||||
"minute" | "min" => Relative::Minutes(1),
|
||||
_ => return None,
|
||||
})
|
||||
})
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{parse, Relative};
|
||||
|
||||
#[test]
|
||||
fn all() {
|
||||
for (s, rel) in [
|
||||
// Seconds
|
||||
("second", Relative::Seconds(1.0)),
|
||||
("sec", Relative::Seconds(1.0)),
|
||||
("seconds", Relative::Seconds(1.0)),
|
||||
("secs", Relative::Seconds(1.0)),
|
||||
("second ago", Relative::Seconds(-1.0)),
|
||||
("3 seconds", Relative::Seconds(3.0)),
|
||||
("3.5 seconds", Relative::Seconds(3.5)),
|
||||
// ("+3.5 seconds", Relative::Seconds(3.5)),
|
||||
("3.5 seconds ago", Relative::Seconds(-3.5)),
|
||||
("-3.5 seconds ago", Relative::Seconds(3.5)),
|
||||
// Minutes
|
||||
("minute", Relative::Minutes(1)),
|
||||
("minutes", Relative::Minutes(1)),
|
||||
("min", Relative::Minutes(1)),
|
||||
("mins", Relative::Minutes(1)),
|
||||
("10 minutes", Relative::Minutes(10)),
|
||||
("-10 minutes", 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 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 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)),
|
||||
("week", Relative::Days(7)),
|
||||
("weeks", Relative::Days(7)),
|
||||
("2 weeks ago", Relative::Days(-14)),
|
||||
// Other
|
||||
("year", Relative::Years(1)),
|
||||
("years", Relative::Years(1)),
|
||||
("month", Relative::Months(1)),
|
||||
("months", Relative::Months(1)),
|
||||
// Special
|
||||
("yesterday", Relative::Days(-1)),
|
||||
("tomorrow", Relative::Days(1)),
|
||||
("today", Relative::Days(0)),
|
||||
("now", Relative::Days(0)),
|
||||
// This something
|
||||
("this day", Relative::Days(0)),
|
||||
("this second", Relative::Seconds(0.0)),
|
||||
("this year", Relative::Years(0)),
|
||||
// Weird stuff
|
||||
("next week ago", Relative::Days(-7)),
|
||||
("last week ago", Relative::Days(7)),
|
||||
("this week ago", Relative::Days(0)),
|
||||
] {
|
||||
let mut t = s;
|
||||
assert_eq!(parse(&mut t).ok(), Some(rel), "Failed string: {s}")
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,10 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
use winnow::ModalResult;
|
||||
|
||||
use super::time;
|
||||
|
||||
pub(crate) fn parse(input: &mut &str) -> ModalResult<time::Offset> {
|
||||
time::timezone(input)
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
// spell-checker:ignore wednes
|
||||
|
||||
//! The GNU docs state:
|
||||
//!
|
||||
//! > The explicit mention of a day of the week will forward the date (only if
|
||||
//! > necessary) to reach that day of the week in the future.
|
||||
//! >
|
||||
//! > Days of the week may be spelled out in full: ‘Sunday’, ‘Monday’,
|
||||
//! > ‘Tuesday’, ‘Wednesday’, ‘Thursday’, ‘Friday’ or ‘Saturday’. Days may be
|
||||
//! > abbreviated to their first three letters, optionally followed by a
|
||||
//! > period. The special abbreviations ‘Tues’ for ‘Tuesday’, ‘Wednes’ for
|
||||
//! > ‘Wednesday’ and ‘Thur’ or ‘Thurs’ for ‘Thursday’ are also allowed.
|
||||
//! >
|
||||
//! > A number may precede a day of the week item to move forward supplementary
|
||||
//! > weeks. It is best used in expression like ‘third monday’. In this
|
||||
//! > context, ‘last day’ or ‘next day’ is also acceptable; they move one week
|
||||
//! > before or after the day that day by itself would represent.
|
||||
//! >
|
||||
//! > A comma following a day of the week item is ignored.
|
||||
|
||||
use winnow::{
|
||||
ascii::alpha1,
|
||||
combinator::{opt, terminated},
|
||||
seq, ModalResult, Parser,
|
||||
};
|
||||
|
||||
use super::{ordinal::ordinal, primitive::s};
|
||||
|
||||
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
|
||||
pub(crate) enum Day {
|
||||
Monday,
|
||||
Tuesday,
|
||||
Wednesday,
|
||||
Thursday,
|
||||
Friday,
|
||||
Saturday,
|
||||
Sunday,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Debug)]
|
||||
pub struct Weekday {
|
||||
pub(crate) offset: i32,
|
||||
pub(crate) day: Day,
|
||||
}
|
||||
|
||||
impl From<Day> for chrono::Weekday {
|
||||
fn from(value: Day) -> Self {
|
||||
match value {
|
||||
Day::Monday => chrono::Weekday::Mon,
|
||||
Day::Tuesday => chrono::Weekday::Tue,
|
||||
Day::Wednesday => chrono::Weekday::Wed,
|
||||
Day::Thursday => chrono::Weekday::Thu,
|
||||
Day::Friday => chrono::Weekday::Fri,
|
||||
Day::Saturday => chrono::Weekday::Sat,
|
||||
Day::Sunday => chrono::Weekday::Sun,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a weekday item.
|
||||
pub(super) fn parse(input: &mut &str) -> ModalResult<Weekday> {
|
||||
seq!(Weekday {
|
||||
offset: opt(ordinal).map(|o| o.unwrap_or_default()),
|
||||
day: terminated(day, opt(s(","))),
|
||||
})
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
fn day(input: &mut &str) -> ModalResult<Day> {
|
||||
s(alpha1)
|
||||
.verify_map(|s: &str| {
|
||||
Some(match s {
|
||||
"monday" | "mon" | "mon." => Day::Monday,
|
||||
"tuesday" | "tue" | "tue." | "tues" => Day::Tuesday,
|
||||
"wednesday" | "wed" | "wed." | "wednes" => Day::Wednesday,
|
||||
"thursday" | "thu" | "thu." | "thur" | "thurs" => Day::Thursday,
|
||||
"friday" | "fri" | "fri." => Day::Friday,
|
||||
"saturday" | "sat" | "sat." => Day::Saturday,
|
||||
"sunday" | "sun" | "sun." => Day::Sunday,
|
||||
_ => return None,
|
||||
})
|
||||
})
|
||||
.parse_next(input)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{parse, Day, Weekday};
|
||||
|
||||
#[test]
|
||||
fn this_monday() {
|
||||
for mut s in [
|
||||
"monday",
|
||||
"mon",
|
||||
"mon.",
|
||||
"this monday",
|
||||
"this mon",
|
||||
"this mon.",
|
||||
"this monday",
|
||||
"this - monday",
|
||||
"0 monday",
|
||||
] {
|
||||
assert_eq!(
|
||||
parse(&mut s).unwrap(),
|
||||
Weekday {
|
||||
offset: 0,
|
||||
day: Day::Monday,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn next_tuesday() {
|
||||
for s in ["tuesday", "tue", "tue.", "tues"] {
|
||||
let s = format!("next {s}");
|
||||
assert_eq!(
|
||||
parse(&mut s.as_ref()).unwrap(),
|
||||
Weekday {
|
||||
offset: 1,
|
||||
day: Day::Tuesday,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn last_wednesday() {
|
||||
for s in ["wednesday", "wed", "wed.", "wednes"] {
|
||||
let s = format!("last {s}");
|
||||
assert_eq!(
|
||||
parse(&mut s.as_ref()).unwrap(),
|
||||
Weekday {
|
||||
offset: -1,
|
||||
day: Day::Wednesday,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optional_comma() {
|
||||
for mut s in ["monday,", "mon,", "mon.,", "mon. ,"] {
|
||||
assert_eq!(
|
||||
parse(&mut s).unwrap(),
|
||||
Weekday {
|
||||
offset: 0,
|
||||
day: Day::Monday,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user