Compare commits

...
278 Commits
Author SHA1 Message Date
Sylvestre Ledru eb444cc124 prepare version 0.17.0 2026-10-11 16:23:43 +02:00
Ion f75711a932 fix(relative): add the fraction of a negative relative second count (#327)
* fix(relative): add the fraction of a negative relative second count

`Relative::Seconds` holds a floor decomposition, so -0.25 seconds is
`(-1, 750_000_000)`. A `jiff::Span` is sign-uniform, so combining those two
fields directly subtracted the fraction instead of adding it and the result
was off by `2 * (1 - fraction)` seconds. Rebalance the fraction onto the sign
of the whole seconds before building the span.

* fix(relative): shorten the fraction comment, align the GNU version to 9.11
2026-10-11 16:10:53 +02:00
Anas Awadalla de62667844 fix(weekday): accept a period after abbreviated weekday names (#334)
* fix(weekday): accept a period after abbreviated weekday names

* test(weekday): parse abbreviated weekdays with a period through the full parser

* test(weekday): cover mismatched abbreviated weekday with period
2026-10-11 15:52:32 +02:00
CodSpeed Bot 701f5a0200 bench: pin parse_weekday to a fixed reference date
The weekday benchmark parsed "wednesday" relative to the current date, so
its cost depended on which day of the week CI ran (no date shift on a
Wednesday, a multi-day shift otherwise), producing spurious regressions.
2026-10-11 09:41:30 +02:00
Asifur Rahaman Meeru 2df367552d fix(offset): correct ADT, AST, BST, GST and SST to GNU's values
The table is meant to cover the same abbreviations GNU date supports, but
five of them resolved to a different offset than GNU date gives, so
"2026-06-15 12:00 BST" parsed as +06:00 (Bangladesh) rather than +01:00
(British Summer Time). Offsets were measured with `date --debug` on GNU
coreutils 9.10: ADT is -3, AST -4, BST +1, GST +10 and SST -12.
2026-10-11 09:41:30 +02:00
renovate[bot] 3fc6d60400 chore(deps): update rust crate arbitrary to v1.5.0 2026-10-07 06:52:23 +02:00
renovate[bot] 1c92c879ed chore(deps): update rust crate jiff to v0.2.38 2026-10-07 06:51:33 +02:00
sap1110 5bc4b0a902 fixup! builder: ignore weekday when an explicit date is given 2026-10-05 22:20:33 +02:00
sap1110 a066a83ca4 builder: ignore weekday when an explicit date is given
When the input carries both a weekday and a calendar date, the date is
now authoritative and the weekday is dropped, matching GNU date. This
also covers ordinal forms such as "next friday sep 25 2026". Relative
items are still applied on top of the date, and a weekday without a
date behaves as before.

Closes #317
2026-10-05 22:20:33 +02:00
oech3 ed95575362 ci.yml: use prebuilt cargo-fuzz 2026-10-04 10:19:34 +02:00
Anas Awadalla c6bfe2d15a test(primitive): cover comments and ignored signs between items 2026-10-03 13:17:40 -04:00
Anas Awadalla 59336d8db1 test(offset): cover offsets followed by a relative item 2026-10-03 13:17:40 -04:00
Anas Awadalla dabd1eab31 refactor(offset): shorten the relative lookahead comments 2026-10-03 13:17:40 -04:00
Anas Awadalla f1523281a0 refactor(items): address review cleanups 2026-10-03 13:17:40 -04:00
Anas Awadalla 5b0b578a4c perf(items): parse an ISO date once and lowercase the input only when needed 2026-10-03 13:17:40 -04:00
Anas Awadalla 2788bf4e3d perf(offset): map timezone names to offsets directly and defer the relative lookahead 2026-10-03 13:17:40 -04:00
Anas Awadalla 04fa426eff perf(primitive): skip the whitespace combinator when no comment or sign follows 2026-10-03 13:17:40 -04:00
Anas Awadalla fef8bf0cfc fix(ordinal): accept eighth instead of eight as an ordinal (#333)
* fix(ordinal): accept eighth instead of eight as an ordinal

* test(ordinal): cover text ordinals and parse eighth through the full parser
2026-10-03 19:16:06 +02:00
Natsu e25bf1530c relative: accept hence after a relative time (#331) 2026-09-21 19:13:23 +02:00
renovate[bot] fdd87ce6ad chore(deps): update rust crate codspeed-criterion-compat to v5.0.2 2026-09-18 07:18:08 +02:00
renovate[bot] f451f94912 chore(deps): update rust crate jiff to v0.2.37 2026-09-13 09:51:25 +02:00
renovate[bot] d4bace0d65 chore(deps): update rust crate rstest to 0.27 2026-09-07 07:12:23 +02:00
Sylvestre Ledru 618dda7263 fix(builder): keep the time of day when a zone item is combined with a relative item (#324) 2026-08-29 17:39:35 +02:00
Sylvestre Ledru 9f2a054284 prepare 0.16.0 (#323) 2026-08-29 17:35:28 +02:00
Tsolmondorj Natsagdorj 390c747003 fix(offset): support the military timezone letter j (local time) (#320)
GNU `date` accepts every letter `a`-`y` as a military time zone, and all
of them are already in our table except `j`. It is missing because it is
the one letter that does not denote a fixed offset: `j` means *local
time*, so it cannot be expressed as an `Offset` at all.

    $ TZ=America/New_York date -d '8j'            # 08:00 -0400
    $ TZ=America/New_York date -d '2026-01-01 j'  # 00:00 -0500
    $ TZ=America/New_York date -d '8z'            # 04:00 -0400

Because it follows the base zone's DST rules rather than pinning an
offset, it is represented as its own item and recorded on the builder as
a flag rather than as an offset.

- `j` parses as its own item, so `8j`, `8 j`, `j 8` and `8J` all work.
- It is still a time zone item, so `8j utc`, `8 utc j` and `8 j j` are
  rejected as a repeated zone, matching GNU.
- The whole alphabetic word is consumed before comparing, so `8jan` still
  parses as a date and `jj` is rejected rather than matching a `j` prefix.
- Unknown words remain errors: `8foo` still fails, as it does in GNU.

Verified against GNU coreutils 9.4: all 26 letters `a`-`z` now resolve to
the same instant as GNU, in both a DST and a non-DST base zone.
2026-08-29 17:19:00 +02:00
Daniel Hofstetter 1c7fdea358 Bump crossbeam-epoch from 0.9.18 to 0.9.20 (#312)
https://rustsec.org/advisories/RUSTSEC-2026-0204
2026-08-28 17:00:38 +02:00
wtc e42c4f0e2e fix: accept vertical tab and form feed as whitespace between tokens (#322) 2026-08-28 16:50:43 +02:00
Asifur Rahaman Meeru 4cf4db33f0 fix(offset): accept MEZ, MESZ, MEST and KST abbreviations (#321)
The table is meant to cover the same abbreviations GNU date supports, but
these four were missing, so `date -d "2024-01-15 12:00 MEZ"` failed here
while GNU accepts it. Offsets were measured against GNU coreutils: MEZ is
+1, MESZ and MEST are +2, and KST is +9.

Downstream this is what pushes uutils/coreutils to keep its own abbreviation
table in date.rs and parse the string in fragments, which is the root of
uutils/coreutils#13865.
2026-08-27 08:02:49 +02:00
renovate[bot] 46fb737541 chore(deps): update codspeedhq/action action to v5 2026-07-29 13:12:51 +02:00
renovate[bot] 0562eec95b chore(deps): update rust crate jiff to v0.2.35 2026-07-26 10:21:10 +02:00
renovate[bot] d435b3727a chore(deps): update rust crate jiff to v0.2.34 2026-07-20 08:04:40 +02:00
renovate[bot] 01efbda22a chore(deps): update rust crate jiff to v0.2.33 2026-07-19 10:20:46 +02:00
renovate[bot] 0ded151ae6 chore(deps): update rust crate winnow to v1.0.4 2026-07-14 08:04:40 +02:00
renovate[bot] 75aaf908c0 chore(deps): update rust crate jiff to v0.2.32 2026-07-09 07:54:44 +02:00
Tsolmondorj Natsagdorj cb1cb02a33 fix: add unix_epoch_second() with floor semantics for negative timestamps (#285)
* fix: add unix_epoch_second() with floor semantics for negative timestamps

jiff::Timestamp::as_second() uses truncation toward zero, so a timestamp
of -1.5s returns -1. GNU date's %s format uses floor (toward negative
infinity), returning -2 for the same value.

Add ParsedDateTime::unix_epoch_second() which corrects for this by
detecting a negative subsec_nanosecond and subtracting 1 from the
truncated second. Also add subsec_nanosecond() that re-normalizes to
the non-negative [0, 1_000_000_000) range matching the GNU timespec
convention.

The uutils date command can use these methods to produce correct %s
output for negative fractional epoch inputs like @-1.5.

Fixes #283

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test: cover extended epoch helper paths

* test: cover direct extended epoch accessors

* test: remove panic branch to land full patch coverage

The let-else with panic! left one line reachable only on test failure,
which showed up as uncovered on codecov/patch. Use matches! + assert!
to reach the same invariant with 100% patch coverage.
2026-07-05 16:23:40 +02:00
Tsolmondorj Natsagdorj 52554677dd fix: support HH:MM am/pm time formats in combined date-time parsing (#284)
* fix: support HH:MM am/pm time formats in combined date-time parsing

The combined date-time parser (e.g., "2024-06-15 12:00 PM") used
time::iso which only accepts 24-hour notation. When an am/pm suffix
followed, iso would greedily consume the time portion, leaving the
meridiem orphaned and causing a parse error.

Switch to time::parse which tries am_pm_time first (with fallback to
iso), so inputs like "2024-06-15 12:00 PM", "2024-06-15 11:30am", and
"2024-06-15 3:00 PM" are now accepted. Case insensitivity is already
handled by parse_items lowercasing the input before parsing.

Fixes #282



* perf: keep ISO fast path for combined datetime parsing
2026-07-05 16:22:03 +02:00
Tsolmondorj Natsagdorj 516a3a934d fix(offset): accept "ut" / "UT" as a bare UTC timezone abbreviation (#287)
* fix(offset): accept "ut" / "UT" as a bare UTC timezone abbreviation

GNU date -d accepts "ut" and "UT" (Universal Time) as synonyms for UTC.
parse_datetime rejected them because the timezone_name_to_offset table
contained "utc" and "gmt" but was missing "ut".

Add "ut" => "+0" to the lookup table. "gmt" was already present and
continues to work unchanged.

Add regression tests:
  - offset-level: timezone_name_without_offset now includes "ut"
  - lib-level: test_bare_utc_timezone_abbreviations covers all four
    forms (ut, UT, gmt, GMT) end-to-end

Fixes #280

* style: run rustfmt on bare UT timezone test
2026-07-05 14:08:51 +02:00
Daniel Hofstetter 1b014a15a3 Bump defmt from 1.1.0 to 1.1.1 (#310) 2026-07-05 14:08:15 +02:00
Daniel Hofstetter bf5cf43375 Bump anyhow from 1.0.102 to 1.0.103 (#309) 2026-06-30 11:08:19 +02:00
renovate[bot] edb83cceba chore(deps): update rust crate jiff to v0.2.31 2026-06-30 08:14:42 +02:00
renovate[bot] a3b8d128fa chore(deps): update rust crate codspeed-criterion-compat to v5.0.1 2026-06-27 10:58:09 +02:00
renovate[bot] e717803a5e chore(deps): update rust crate codspeed-criterion-compat to v5 2026-06-23 15:30:32 +02:00
renovate[bot] b6c2256daf chore(deps): update rust crate jiff to v0.2.29 2026-06-21 10:02:40 +02:00
renovate[bot] 3355829f3d chore(deps): update actions/checkout action to v7 2026-06-19 08:15:24 +02:00
xtqqczzeandSylvestre Ledru 80556965f3 chore(deps): update rust crates (#288)
Co-authored-by: Sylvestre Ledru <sylvestre@debian.org>
2026-06-15 21:37:34 +02:00
Sylvestre Ledru b83d7a953c builder: apply fixed offset before relative adjustments
A fixed offset (e.g. the "UTC" keyword) was applied after relative
items, so "1970/01/01 UTC N seconds" added N seconds to a local
wall-clock that could drift across a DST boundary before being
re-anchored at the offset, yielding an instant off by the DST gap.
Anchor the offset first, then apply relative items.

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

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

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

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

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

Closes: https://github.com/uutils/coreutils/issues/8618
2025-11-21 06:37:09 +01:00
renovate[bot] 6ce151fb58 chore(deps): update actions/checkout action to v6 2025-11-20 18:43:38 +00:00
Daniel Hofstetter 9f6605f20d Merge pull request #244 from cakebaker/bump_version_to_0_13_2
chore: bump version to `0.13.2`
2025-11-09 18:27:31 +01:00
Daniel Hofstetter a025a04d72 chore: bump version to 0.13.2 2025-11-09 17:49:27 +01:00
Sylvestre Ledru dbae631ff0 Merge pull request #241 from cakebaker/literal3
feat: support formats `Nov-14-2024` and `November-14-2024`
2025-11-09 15:03:15 +01:00
Daniel Hofstetter 83278f0ddc Merge pull request #243 from uutils/renovate/jiff-0.x-lockfile
chore(deps): update rust crate jiff to v0.2.16
2025-11-07 17:39:07 +01:00
renovate[bot] ae518cac37 chore(deps): update rust crate jiff to v0.2.16 2025-11-07 15:41:39 +00:00
Daniel Hofstetter bc5d201b68 Merge pull request #242 from cakebaker/update_dependencies
chore: update dependencies
2025-10-30 09:29:46 +01:00
Daniel Hofstetter 8c56c5cb13 chore: update dependencies 2025-10-29 11:13:04 +01:00
Daniel Hofstetter 9c95c9549b feat: support formats Nov-14-2024/November-14-2024 2025-10-29 10:40:22 +01:00
Daniel Hofstetter ba69fb8772 Merge pull request #239 from cakebaker/bump_version_to_0_13_1
chore: bump version to 0.13.1
2025-10-04 17:27:00 +02:00
Daniel Hofstetter 912bc4e706 chore: bump version to 0.13.1 2025-10-04 17:19:18 +02:00
Daniel Hofstetter c239ed0b36 Merge pull request #238 from stillbeingnick/bugfix/issue/coreutils-date-invalid
Changing ErrMode::Cut to ErrMode::Backtrack to allow alt parser flow (Fix for uutils/coreutils issue 8754)
2025-10-04 17:00:00 +02:00
Nicholas Still 3074fe3baa Change ErrMode::Cut to ErrMode::Backtrack to allow alt parser flow 2025-10-04 16:55:43 +02:00
Daniel Hofstetter 29e368662c Merge pull request #237 from yuankunzhang/add-comments-for-build-function
docs: add comments for the build function
2025-09-28 15:21:47 +02:00
yuankunzhang 1e757df39b docs: add comments for the build function 2025-09-27 21:34:09 +08:00
Daniel Hofstetter 7a32b1ee3d Merge pull request #236 from yuankunzhang/remove-regex-dependency
chore: remove regex crate as dependency
2025-09-27 14:29:14 +02:00
yuankunzhang c898cd84de chore: remove regex crate as dependency 2025-09-27 20:10:25 +08:00
Daniel Hofstetter eb610870e1 Merge pull request #235 from cakebaker/bump_version_to_0_13_0
chore: bump version to `0.13.0`
2025-09-26 14:20:54 +02:00
Daniel Hofstetter 9db7a54926 chore: bump version to 0.13.0 2025-09-26 09:18:35 +02:00
Daniel Hofstetter e43652c0ee Merge pull request #232 from yuankunzhang/tz-rule
add support for `TZ="timezone"` date spec
2025-09-25 16:03:41 +02:00
Daniel Hofstetter d9c2ed2b11 Merge pull request #234 from uutils/renovate/regex-1.x-lockfile
fix(deps): update rust crate regex to v1.11.3
2025-09-25 14:32:35 +02:00
renovate[bot] 6a16e61ab1 fix(deps): update rust crate regex to v1.11.3 2025-09-25 12:26:47 +00:00
yuankunzhang f0c499593e feat: add support for TZ="timezone" date spec 2025-09-25 00:09:54 +08:00
Daniel Hofstetter 5453d6777a Merge pull request #231 from yuankunzhang/rename-timezone-to-offset
refactor: rename the timezone module to offset
2025-09-23 17:51:28 +02:00
yuankunzhang 0c09d5bd17 refactor: rename the timezone module to offset 2025-09-23 22:57:59 +08:00
Daniel Hofstetter 5662a1b47f Merge pull request #228 from cakebaker/bump_version_to_0_12_0
Bump version to `0.12.0`
2025-09-22 14:29:38 +02:00
Daniel Hofstetter f75ec46d63 Merge pull request #230 from yuankunzhang/parser-combinators
refactor: replace imperative parsing loop with functional combinators
2025-09-15 16:09:46 +02:00
yuankunzhang 2dc4fa3ffb refactor: replace imperative parsing loop with functional combinators
- Add `TryFrom<Vec<Items>>` implementation for `DateTimeBuilder`
  to improve code separation and reusability.
- Replace manual parsing loop in `parse_items()` with
  `repeat_until` combinator for cleaner approach.
- Allow timestamp strings ending with whitespaces.
2025-09-15 21:49:24 +08:00
Daniel Hofstetter 69649bf6c6 chore: bump version to 0.12.0 2025-09-14 16:44:42 +02:00
Daniel Hofstetter 583eafe62c Merge pull request #229 from yuankunzhang/update-deps
chore: update dependencies
2025-09-14 16:38:36 +02:00
yuankunzhang 6815ba1406 chore: update dependencies 2025-09-14 22:29:31 +08:00
Sylvestre Ledru a4c29bafcb Merge pull request #227 from cakebaker/fuzz_remove_regex_and_chrono
fuzz/Cargo.toml: remove unused `regex` & `chrono`
2025-09-08 14:27:10 +02:00
Daniel Hofstetter e70d066e03 fuzz/Cargo.toml: remove unused regex & chrono 2025-09-08 13:58:12 +02:00
Daniel Hofstetter e5f9894b11 Merge pull request #225 from yuankunzhang/result-based-error-handling
refactor: replace Option-based error handling with Result-based error handling
2025-08-26 10:34:35 +02:00
yuankunzhang 1f4ebd93c6 refactor: replace Option-based error handling with Result-based error handling 2025-08-25 22:38:14 +08:00
Daniel Hofstetter 74dd77db53 Merge pull request #224 from uutils/renovate/regex-1.x-lockfile
fix(deps): update rust crate regex to v1.11.2
2025-08-25 07:58:18 +02:00
renovate[bot] 6f15b90e36 fix(deps): update rust crate regex to v1.11.2 2025-08-24 18:02:53 +00:00
Daniel Hofstetter 4db8bf4b22 Merge pull request #223 from yuankunzhang/support-timezone-offset-larger-than-24-hour
fix: support timezone offsets that exceed 24-hour range
2025-08-24 15:29:11 +02:00
yuankunzhang 84d27e6386 fix: support timezone offsets that exceed 24-hour range 2025-08-24 19:08:48 +08:00
Daniel Hofstetter a0f1376d2f Merge pull request #222 from yuankunzhang/improve-timezone
refactor: improve timezone offset parsing and validation
2025-08-23 17:56:53 +02:00
yuankunzhang 59eac72c44 refactor: improve timezone offset parsing and validation 2025-08-23 16:51:38 +02:00
Daniel Hofstetter d6bde134df Merge pull request #221 from yuankunzhang/misc-refactors
refactor: misc minor refactors
2025-08-23 16:47:26 +02:00
yuankunzhang 43842ebf8e refactor: misc minor refactors 2025-08-23 22:22:33 +08:00
Sylvestre Ledru c31711d351 Merge pull request #214 from cakebaker/refactor_use_better_names
refactor: rename a function name and a var name
2025-08-22 16:52:41 +02:00
Sylvestre Ledru ea5d61b3eb Merge pull request #216 from cakebaker/refactor_rename_var_to_fix_typo
refactor: rename var to fix typo
2025-08-22 16:52:24 +02:00
Daniel Hofstetter c55bea1d8b Merge pull request #220 from uutils/renovate/winnow-0.x-lockfile
fix(deps): update rust crate winnow to v0.7.13
2025-08-22 16:51:18 +02:00
Daniel Hofstetter 6baf7e301d refactor: rename var to fix typo 2025-08-22 16:48:19 +02:00
renovate[bot] 795d7e6c43 fix(deps): update rust crate winnow to v0.7.13 2025-08-22 14:43:38 +00:00
Sylvestre Ledru 3f4bd53eba Merge pull request #219 from cakebaker/fix_use_checked_mul
fix: use `checked_mul()` to avoid overflow with "week"
2025-08-22 16:43:00 +02:00
Daniel Hofstetter ec30e0e1f2 Merge pull request #218 from yuankunzhang/improve-epoch
refactor: improve timestamp handling and test organization
2025-08-22 15:28:10 +02:00
yuankunzhang 70055eaa21 refactor: improve timestamp handling and test organization 2025-08-22 20:47:48 +08:00
Daniel Hofstetter 871cae962e fix: use checked_mul() to avoid overflow with week 2025-08-22 14:46:54 +02:00
Sylvestre Ledru 91692a17e0 Merge pull request #217 from yuankunzhang/checked-mul
fix: use `checked_mul()` to avoid overflow panic
2025-08-22 14:25:52 +02:00
Daniel Hofstetter e225cf0557 Merge pull request #215 from yuankunzhang/improve-api-encapsulation
refactor: improve api encapsulation
2025-08-22 14:14:51 +02:00
yuankunzhang 8e2816d187 fix: use checked_mul() to avoid overflow panic 2025-08-22 20:10:50 +08:00
yuankunzhang 84d4e294df refactor: improve api encapsulation 2025-08-22 19:21:29 +08:00
Daniel Hofstetter c87e103745 refactor: rename a function name and a var name 2025-08-21 13:38:08 +02:00
Daniel Hofstetter 7d0c3e4500 Merge pull request #213 from yuankunzhang/replace-chrono-with-jiff-6
(jiff series #6) refactor: replace chrono with jiff
2025-08-20 17:18:32 +02:00
yuankunzhang ae25053473 refactor: replace chrono with jiff 2025-08-20 11:44:58 +08:00
Daniel Hofstetter 17847a8d9c Merge pull request #212 from yuankunzhang/replace-chrono-with-jiff-5
(jiff series #5) refactor: add conversion from `Day` to `jiff::civil::Weekday`
2025-08-16 16:39:24 +02:00
yuankunzhang 6fce02949d refactor: add conversion from Day to jiff::civil::Weekday 2025-08-16 22:31:12 +08:00
Daniel Hofstetter cc5029c177 Merge pull request #211 from yuankunzhang/replace-chrono-with-jiff-4
(jiff series #4) refactor: optimize offset type sizes and add conversion from `Offset` to `jifff::tz::Offset`
2025-08-16 16:29:12 +02:00
yuankunzhang 1b96dd5707 refactor: optimize offset type sizes and add conversion from Offset to jifff::tz::Offset 2025-08-16 16:10:18 +02:00
Daniel Hofstetter b5439d8cc2 Merge pull request #210 from yuankunzhang/replace-chrono-with-jiff-3
(jiff series #3) refactor: use `(i64, u32)` for fractional seconds in relative time parsing
2025-08-16 16:06:23 +02:00
yuankunzhang d2e6e7819d refactor: use (i64, u32) for fractional seconds in relative time parsing 2025-08-16 21:55:10 +08:00
Daniel Hofstetter a8e26ab539 Merge pull request #209 from yuankunzhang/replace-chrono-with-jiff-2
(jiff series #2) refactor: optimize time type sizes and add conversion from `Time` to `jiff::civil::Time`
2025-08-16 15:20:36 +02:00
yuankunzhang 920cfa5f1b refactor: optimize time type sizes and add convertion from Time to jiff::civil::Time 2025-08-16 15:05:17 +02:00
Daniel Hofstetter a79ce1fc31 Merge pull request #208 from yuankunzhang/replace-chrono-with-jiff-1
(jiff series #1) refactor: add jiff and optimize date type sizes
2025-08-16 15:01:53 +02:00
yuankunzhang c87b6f19eb refactor: add jiff and optimize date type sizes 2025-08-16 20:18:41 +08:00
Daniel Hofstetter 25f9e48e55 Merge pull request #205 from yuankunzhang/refactor-timestamp-parsing
refactor: replace floating-point seconds with precise integer representation
2025-08-13 11:03:37 +02:00
Daniel Hofstetter 14b8a9447c Merge pull request #206 from uutils/dependabot/cargo/slab-0.4.11
chore(deps): bump slab from 0.4.10 to 0.4.11
2025-08-13 08:05:42 +02:00
dependabot[bot] 692f17526d chore(deps): bump slab from 0.4.10 to 0.4.11
Bumps [slab](https://github.com/tokio-rs/slab) from 0.4.10 to 0.4.11.
- [Release notes](https://github.com/tokio-rs/slab/releases)
- [Changelog](https://github.com/tokio-rs/slab/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/slab/compare/v0.4.10...v0.4.11)

---
updated-dependencies:
- dependency-name: slab
  dependency-version: 0.4.11
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-08-13 04:39:23 +00:00
yuankunzhang 5c686bcfdf refactor: replace floating-point seconds with precise integer representation
Replace f64-based timestamp and second parsing with structured
types using separate second and nanosecond fields to eliminate
floating-point precision issues.
2025-08-12 20:24:38 +08:00
Daniel Hofstetter c86b137e6f Merge pull request #204 from uutils/renovate/actions-checkout-5.x
chore(deps): update actions/checkout action to v5
2025-08-11 17:29:32 +02:00
renovate[bot] f075ba8d8d chore(deps): update actions/checkout action to v5 2025-08-11 15:24:18 +00:00
Daniel Hofstetter 5ca5586523 Merge pull request #203 from yuankunzhang/refactor-timezone-related-code
refactor: extract timezone functionality from time module to timezone module
2025-08-10 16:38:54 +02:00
yuankunzhang 2bc18c52b7 refactor: extract timezone functionality from time module to timezone module.
Move timezone-related code from `time.rs` to `timezone.rs` to
improve separation of concerns. The `Offset` struct and all
timezone parsing functions are now properly separated from time
parsing logic.
2025-08-10 16:30:09 +02:00
Daniel Hofstetter 3eb58ab00a Merge pull request #202 from yuankunzhang/refactor-timestamp-handling
refactor: improve timestamp handling and validation in builder
2025-08-10 16:04:30 +02:00
yuankunzhang 628011ae97 refactor: improve timestamp handling and validation in builder 2025-08-10 20:40:16 +08:00
Daniel Hofstetter 42d09dadf7 Merge pull request #201 from yuankunzhang/minor-improvements
code and docs minor improvements
2025-08-06 16:36:19 +02:00
yuankunzhang 908356f3a3 code and docs minor improvements 2025-08-06 22:15:03 +08:00
Daniel Hofstetter 65965a85d1 Merge pull request #199 from yuankunzhang/implement-pure-number-parsing
feat: implement pure number parsing
2025-08-05 16:02:18 +02:00
yuankunzhang 551985ef91 feat: implement pure number parsing
Implement GNU-compatible pure number interpretation that treats
pure number as year or time based on the current parsing context.
2025-08-04 23:11:41 +08:00
Daniel Hofstetter 8fa8928a6a Merge pull request #198 from yuankunzhang/remove-unsupported-named-time-zones
fix: remove unsupported named time zones
2025-08-04 14:40:00 +02:00
yuankunzhang fa90303836 fix: remove unsupported named time zones 2025-08-04 19:53:13 +08:00
Sylvestre Ledru 46a5ef89c9 Merge pull request #195 from cakebaker/bump_version_to_0_11
Bump version to `0.11.0`
2025-08-01 09:05:34 +02:00
Daniel Hofstetter 226d6a0588 Bump version to 0.11.0 2025-08-01 08:38:55 +02:00
Sylvestre Ledru 28b9f11f2e Merge pull request #179 from cakebaker/remove_unnecessary_option
chore: remove unnecessary `Option` as return value
2025-08-01 08:25:34 +02:00
Sylvestre Ledru b68c325abd Merge pull request #162 from cakebaker/set_timezone_for_tests
tests: set timezone to `UTC0` in integration tests
2025-08-01 08:25:18 +02:00
Daniel Hofstetter 27a626d3d5 Merge pull request #194 from uutils/renovate/rstest-0.x-lockfile
chore(deps): update rust crate rstest to v0.26.1
2025-07-27 16:43:53 +02:00
renovate[bot] 1630fd025e chore(deps): update rust crate rstest to v0.26.1 2025-07-27 14:39:43 +00:00
Daniel Hofstetter cf5770838c Merge pull request #193 from uutils/renovate/rstest-0.x
chore(deps): update rust crate rstest to 0.26
2025-07-27 10:26:19 +02:00
renovate[bot] db9ff29c02 chore(deps): update rust crate rstest to 0.26 2025-07-26 21:25:06 +00:00
Daniel Hofstetter 4cfe8db9be Merge pull request #192 from yuankunzhang/update-readme
Update readme
2025-07-24 16:26:03 +02:00
yuankunzhang 1e7a541528 docs: update readme 2025-07-24 14:21:24 +00:00
Daniel Hofstetter b2bb6dfe91 Merge pull request #191 from yuankunzhang/update-comments
docs: update comments
2025-07-24 16:20:22 +02:00
yuankunzhang d71c6ea41c docs: update comments 2025-07-24 14:09:45 +00:00
Daniel Hofstetter 3b6362e7c8 Merge pull request #190 from yuankunzhang/fix-base-datetime-handling
fix base datetime handling in the builder
2025-07-24 10:21:28 +02:00
yuankunzhang 897a50f4d5 fix base datetime handling in the builder 2025-07-24 10:09:25 +02:00
Daniel Hofstetter 96f477121f Merge pull request #189 from yuankunzhang/support-float-timestamp
fix: support float timestamp values
2025-07-24 10:03:56 +02:00
yuankunzhang c47ce6870a fix: support float timestamp values 2025-07-24 09:34:02 +02:00
Daniel Hofstetter 77e1ab2d00 Merge pull request #188 from yuankunzhang/update-comments
Update comments
2025-07-23 17:35:43 +02:00
yuankunzhang a48ad6aec3 chore: update comments 2025-07-23 17:30:32 +02:00
Daniel Hofstetter 0ddeec70b4 Merge pull request #187 from yuankunzhang/year-module
feat: add a year module
2025-07-23 17:26:43 +02:00
yuankunzhang 2bb8ac8203 feat: add a year module 2025-07-23 13:53:46 +00:00
Daniel Hofstetter 5b52929c91 Merge pull request #186 from yuankunzhang/improve-literal-date-parser
feat: improve the literal date parsers
2025-07-23 14:42:54 +02:00
yuankunzhang 3d6f79d826 feat: improve the literal date parsers 2025-07-23 12:31:33 +00:00
Daniel Hofstetter 324d0bca34 Merge pull request #184 from yuankunzhang/improve-us-date-parser
Improve the `us()` date parser
2025-07-22 15:35:14 +02:00
yuankunzhang 9f75f895e0 feat: improve the us() date parser 2025-07-22 15:30:13 +02:00
Yuankun ZhangandDaniel Hofstetter 5ba4b18655 Improve the iso1() and iso2() date parsers (#183)
* feat: improve date parsing with GNU compatibility and validation

- Add `TryFrom<(&str, u32, u32)>` implementation for `Date` struct
  with validation.
- Add comprehensive date validation for months, days, taking leap
  year into account.
- Refactor `iso1()` and `iso2()` date parsers to use the new
  validation logic.

* fix typo

Co-authored-by: Daniel Hofstetter <daniel.hofstetter@42dh.com>

* remove unnecessary if condition

Co-authored-by: Daniel Hofstetter <daniel.hofstetter@42dh.com>

---------

Co-authored-by: Daniel Hofstetter <daniel.hofstetter@42dh.com>
2025-07-22 14:49:36 +02:00
Daniel Hofstetter 5d869b5264 Merge pull request #182 from yuankunzhang/add-ctx-err
feat: add a `ctx_err` util function
2025-07-20 14:07:21 +02:00
yuankunzhang 793d18dce7 feat: add a ctx_err util function 2025-07-20 03:28:52 +00:00
Daniel Hofstetter 7182760266 Merge pull request #181 from yuankunzhang/support-leading-zeros-in-timezone-colonless
feat: support leading zeros in `timezone_colonless()`
2025-07-19 17:30:07 +02:00
yuankunzhang e7a8a52a21 feat: support leading zeros in timezone_colonless() 2025-07-19 17:18:39 +02:00
Daniel Hofstetter 0674254bc6 Merge pull request #180 from yuankunzhang/fix-time-offset
fix: time offset can only appear after time or timezone
2025-07-19 16:16:17 +02:00
yuankunzhang 8bebfab576 fix: time offset can only appear after time or timezone 2025-07-19 13:57:46 +00:00
Daniel Hofstetter 6feeddfea1 chore: remove unnecessary Option as return value 2025-07-17 10:12:03 +02:00
Daniel Hofstetter 6499842e15 Merge pull request #178 from yuankunzhang/improve-items-mod
chore: re-organize items/mod.rs
2025-07-17 09:44:47 +02:00
yuankunzhang 949e0613d6 chore: re-organize items/mod.rs 2025-07-17 09:54:24 +08:00
Daniel Hofstetter 6e6ffc2dff tests: set timezone to UTC0 in integration tests 2025-07-16 10:13:20 +02:00
Daniel Hofstetter 67364fc6ba Merge pull request #174 from yuankunzhang/exclusive-timestamp
fix: timestamp cannot be combined with other items
2025-07-16 10:09:47 +02:00
Daniel Hofstetter d92f5eeb27 Merge pull request #176 from uutils/renovate/winnow-0.x-lockfile
fix(deps): update rust crate winnow to v0.7.12
2025-07-16 10:07:03 +02:00
renovate[bot] 5c4a60fadf fix(deps): update rust crate winnow to v0.7.12 2025-07-16 08:01:37 +00:00
Daniel Hofstetter 45bda33a6b Merge pull request #175 from uutils/renovate/libfuzzer-sys-0.x-lockfile
fix(deps): update rust crate libfuzzer-sys to v0.4.10
2025-07-04 08:06:44 +02:00
renovate[bot] 64069cdbf2 fix(deps): update rust crate libfuzzer-sys to v0.4.10 2025-07-03 23:25:47 +00:00
yuankunzhang 4d6568b326 fix: timestamp cannot be combined with other items 2025-07-01 13:40:04 +00:00
Daniel Hofstetter f5c1a5e399 Merge pull request #173 from yuankunzhang/date-time-builder
refactor: introduce a builder and simplify the date time composition
2025-07-01 14:52:49 +02:00
yuankunzhang 706ab45937 refactor: introduce a builder and simplify the date time composition 2025-06-30 13:51:25 +00:00
Daniel Hofstetter 4d9afba2d4 Merge pull request #172 from yuankunzhang/minor-refactor
refactor: put the epoch and timezone module into separate files
2025-06-30 15:10:34 +02:00
yuankunzhang 05677a803d refactor: put the epoch and timezone module into separate files 2025-06-30 13:05:10 +00:00
Daniel Hofstetter 98d4931b53 Merge pull request #169 from yuankunzhang/hour-with-meridiem
fix: hour must be greater than 0 when meridiem is specified
2025-06-28 17:03:48 +02:00
yuankunzhang 0b5d3ffdf3 fix: hour must be greater than 0 when meridiem is specified 2025-06-28 16:48:09 +02:00
Daniel Hofstetter 24fa78a927 Merge pull request #168 from yuankunzhang/nanoseconds
feat: support nanoseconds
2025-06-28 16:45:22 +02:00
yuankunzhang c0ada32ce1 feat: support nanoseconds 2025-06-28 16:30:24 +02:00
Daniel Hofstetter b5b1fd2b5e Merge pull request #171 from yuankunzhang/check-overflows
fix: check overflows during date time composition
2025-06-28 16:18:41 +02:00
yuankunzhang 8d78de85a5 fix: check overflows during date time composition 2025-06-27 15:24:00 +00:00
Sylvestre Ledru 33ac3b75cc Merge pull request #170 from cakebaker/clippy_fix_warning
clippy: fix warning from `uninlined_format_args`
2025-06-27 09:54:14 +02:00
Daniel Hofstetter 832942c019 clippy: fix warning from uninlined_format_args 2025-06-27 09:45:11 +02:00
Daniel Hofstetter e031c8ce49 Merge pull request #167 from Maximkaaa/integration_tests
test: Integration tests for time parsing
2025-06-26 16:59:28 +02:00
Maxim 856a13ddb2 test: Integration tests for date parsing 2025-06-25 05:15:41 +00:00
Sylvestre Ledru f1f2c47120 Merge pull request #163 from cakebaker/tests_fix_typos
tests: fix typos
2025-06-24 14:47:21 +02:00
Sylvestre Ledru 2c0fa5a264 Merge pull request #164 from cakebaker/tests_add_license_header
tests: add license header to integration tests
2025-06-24 14:47:08 +02:00
Daniel Hofstetter 937572d557 tests: add license header to integration tests 2025-06-24 14:43:56 +02:00
Daniel Hofstetter a7d9d1ab59 tests: fix typos
aphabetical -> alphabetical, ommititing -> omitting
2025-06-24 13:55:37 +02:00
Sylvestre Ledru 3be37c4d82 Merge pull request #161 from Maximkaaa/integration_tests
Integration tests for date parsing
2025-06-24 12:36:55 +02:00
Daniel Hofstetter 95897465b2 Merge pull request #159 from yuankunzhang/weekday-optional-comma
fix: weekday may be followed by an optional comma
2025-06-21 07:42:03 +02:00
Maxim 7260b0ae73 test: Integration tests for date parsing 2025-06-20 09:16:48 +00:00
Maxim 92daee09de chore: Remove empty source file 2025-06-20 15:31:40 +09:00
yuankunzhang 73f69b1da0 fix: weekday may be followed by an optional comma 2025-06-17 22:08:35 +08:00
Daniel Hofstetter 30cb26d75c Merge pull request #157 from yuankunzhang/simplify-mod-rs
Simplify items/mod.rs by moving the fundamental combinators to their own module
2025-06-15 17:04:49 +02:00
yuankunzhang 6e64f21967 refactor: move fundamental combinators from mod.rs to the primitive module 2025-06-15 21:15:28 +08:00
yuankunzhang 9b67307512 fix: relative date time handling 2025-06-15 21:13:31 +08:00
Sylvestre Ledru 76d8f17350 Merge pull request #155 from yuankunzhang/main
fix: use custom dec_uint in ordinal parsing
2025-06-15 11:05:09 +02:00
Yuankun Zhang c05b8217e0 Merge branch 'main' into main 2025-06-15 12:22:16 +08:00
yuankunzhang 41a53a2610 fix: use custom dec_uint in ordinal parsing 2025-06-15 12:21:07 +08:00
Daniel Hofstetter 03cff2ce1e Merge pull request #150 from yuankunzhang/minor-fix
fix: check whether the parsed time contains timezone
2025-06-14 15:10:44 +02:00
Daniel Hofstetter 12f73c3d57 Merge pull request #153 from uutils/renovate/winnow-0.x-lockfile
fix(deps): update rust crate winnow to v0.7.11
2025-06-10 18:47:50 +02:00
renovate[bot] c694587fdd fix(deps): update rust crate winnow to v0.7.11 2025-06-10 16:41:00 +00:00
yuankunzhang 3fe06c0b94 fix: check whether the parsed time contains timezone 2025-06-04 16:42:32 +08:00
Sylvestre Ledru 1a6a6f688a Merge pull request #143 from yuankunzhang/main
use proper error mode in items::parse()
2025-06-04 09:11:30 +02:00
yuankunzhang 294bd5eeb2 use proper error mode in items::parse() 2025-05-28 23:21:41 +08:00
yuankunzhang 60745300ad use a custom float parser 2025-05-28 23:19:10 +08:00
32 changed files with 7868 additions and 1873 deletions

No files matched your search

+15
View File
@@ -0,0 +1,15 @@
name: Security audit
on:
schedule:
- cron: "0 0 * * *"
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
with:
persist-credentials: false
- uses: rustsec/audit-check@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
+33 -27
View File
@@ -3,6 +3,7 @@ on: [push, pull_request]
name: Basic CI
env:
CARGO_INCREMENTAL: "0"
CARGO_TERM_COLOR: always
RUST_MIN_SRV: "1.71.1"
@@ -14,8 +15,7 @@ jobs:
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- 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@v4
- 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@v4
- 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@v4
- 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@v4
- 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@v4
- uses: actions/checkout@v7
- name: emulate nightly toolchain
run: echo "RUSTC_BOOTSTRAP=1" >> "${GITHUB_ENV}"
- name: Initialize workflow variables
id: vars
shell: bash
run: |
## VARs setup
outputs() { step_id="vars"; for var in "$@" ; do echo steps.${step_id}.outputs.${var}="${!var}"; echo "${var}=${!var}" >> $GITHUB_OUTPUT; done; }
# toolchain
TOOLCHAIN="nightly" ## default to "nightly" toolchain (required for certain required unstable compiler flags) ## !maint: refactor when stable channel has needed support
# * specify gnu-type TOOLCHAIN for windows; `grcov` requires gnu-style code coverage data files
case ${{ matrix.job.os }} in windows-*) TOOLCHAIN="$TOOLCHAIN-x86_64-pc-windows-gnu" ;; esac;
# * use requested TOOLCHAIN if specified
@@ -94,13 +89,12 @@ jobs:
outputs CODECOV_FLAGS
- name: rust toolchain ~ install
uses: dtolnay/rust-toolchain@nightly
uses: dtolnay/rust-toolchain@nightly # this job is not reproducible...
with:
components: llvm-tools-preview
- name: Test
run: cargo test --no-fail-fast
env:
CARGO_INCREMENTAL: "0"
RUSTC_WRAPPER: ""
RUSTFLAGS: "-Cinstrument-coverage -Zcoverage-options=branch -Ccodegen-units=1 -Copt-level=0 -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort"
RUSTDOCFLAGS: "-Cpanic=abort"
@@ -109,12 +103,12 @@ jobs:
id: build_grcov
shell: bash
run: |
git clone https://github.com/mozilla/grcov.git ~/grcov/
git clone --depth=1 https://github.com/mozilla/grcov.git ~/grcov/
cd ~/grcov
# Hardcode the version of crossbeam-epoch. See
# https://github.com/uutils/coreutils/issues/3680
sed -i -e "s|tempfile =|crossbeam-epoch = \"=0.9.8\"\ntempfile =|" Cargo.toml
cargo install --path .
cargo install --path . --locked
cd -
# Uncomment when the upstream issue
# https://github.com/mozilla/grcov/issues/849 is fixed
@@ -134,32 +128,44 @@ jobs:
# display coverage files
grcov . --binary-path="${COVERAGE_REPORT_DIR}" --output-type files --ignore build.rs --ignore "vendor/*" --ignore "/*" --ignore "[a-zA-Z]:/*" --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()" | sort --unique
# generate coverage report
grcov . --binary-path="${COVERAGE_REPORT_DIR}" --output-type lcov --output-path "${COVERAGE_REPORT_FILE}" --branch --ignore build.rs --ignore "vendor/*" --ignore "/*" --ignore "[a-zA-Z]:/*" --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()"
grcov . --binary-path="${COVERAGE_REPORT_DIR}" --output-type lcov --output-path "${COVERAGE_REPORT_FILE}" --branch --ignore build.rs --ignore "vendor/*" --ignore "/*" --ignore "[a-zA-Z]:/*" --excl-br-line "^\s*((debug_)?assert(_eq|_ne)?!|#\[derive\()" --source-dir .
echo "report=${COVERAGE_REPORT_FILE}" >> $GITHUB_OUTPUT
- name: Upload coverage results (to Codecov.io)
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@v7
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: ${{ steps.coverage.outputs.report }}
## flags: IntegrationTests, UnitTests, ${{ steps.vars.outputs.CODECOV_FLAGS }}
flags: ${{ steps.vars.outputs.CODECOV_FLAGS }}
name: codecov-umbrella
fail_ci_if_error: false
verbose: true
directory: ./
root_dir: ./
fuzz:
name: Run the fuzzers
runs-on: ubuntu-latest
env:
RUN_FOR: 60
# cargo-fuzz uses nightly features
RUSTC_BOOTSTRAP: 1
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@nightly
- uses: actions/checkout@v7
- name: Install `cargo-fuzz`
run: cargo install cargo-fuzz
uses: taiki-e/install-action@v2
with:
tool: cargo-fuzz
- uses: Swatinem/rust-cache@v2
- name: Run from_str for XX seconds
- name: Run fuzz_parse_datetime for XX seconds
shell: bash
run: |
## Run it
cd fuzz
cargo +nightly fuzz run fuzz_parse_datetime -- -max_total_time=${{ env.RUN_FOR }} -detect_leaks=0
# https://github.com/rust-fuzz/cargo-fuzz/issues/398
cargo fuzz run --target host-tuple fuzz_parse_datetime -- -max_total_time=${{ env.RUN_FOR }} -detect_leaks=0
- name: Run fuzz_large_year for XX seconds
shell: bash
run: |
## Run it
cd fuzz
cargo fuzz run --target host-tuple fuzz_large_year -- -max_total_time=${{ env.RUN_FOR }} -detect_leaks=0
+37
View File
@@ -0,0 +1,37 @@
name: CodSpeed
on:
push:
branches:
- "main"
pull_request:
# `workflow_dispatch` allows CodSpeed to trigger backtest
# performance analysis in order to generate initial data.
workflow_dispatch:
permissions:
contents: read
id-token: write
jobs:
codspeed:
name: Run benchmarks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Setup rust toolchain, cache and cargo-codspeed binary
uses: moonrepo/setup-rust@v1
with:
channel: stable
cache-target: release
bins: cargo-codspeed
- name: Build the benchmark target(s)
run: cargo codspeed build
- name: Run the benchmarks
uses: CodSpeedHQ/action@v5
with:
mode: simulation
run: cargo codspeed run
Generated
+842 -134
View File
File diff suppressed because it is too large. Load diff
+14 -5
View File
@@ -1,7 +1,7 @@
[package]
name = "parse_datetime"
description = "parsing human-readable time strings and converting them to a DateTime"
version = "0.10.0"
description = "parsing human-readable time strings and converting them to a Zoned datetime"
version = "0.17.0"
edition = "2021"
license = "MIT"
repository = "https://github.com/uutils/parse_datetime"
@@ -9,7 +9,16 @@ readme = "README.md"
rust-version = "1.71.1"
[dependencies]
regex = "1.10.4"
chrono = { version="0.4.38", default-features=false, features=["std", "alloc", "clock"] }
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.27"
[dev-dependencies.codspeed-criterion-compat]
version = "5.0"
[[bench]]
name = "parse_datetime"
harness = false
+39 -17
View File
@@ -3,16 +3,18 @@
[![Crates.io](https://img.shields.io/crates/v/parse_datetime.svg)](https://crates.io/crates/parse_datetime)
[![License](http://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/uutils/parse_datetime/blob/main/LICENSE)
[![CodeCov](https://codecov.io/gh/uutils/parse_datetime/branch/main/graph/badge.svg)](https://codecov.io/gh/uutils/parse_datetime)
[![CodSpeed](https://img.shields.io/endpoint?url=https://codspeed.io/badge.json)](https://codspeed.io/uutils/parse_datetime?utm_source=badge)
A Rust crate for parsing human-readable relative time strings and human-readable datetime strings and converting them to a `DateTime`.
A Rust crate for parsing human-readable relative time strings and
human-readable datetime strings.
## Features
- 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
- Relies on Jiff
## Usage
@@ -25,26 +27,45 @@ cargo add parse_datetime
Then, import the crate and use the `parse_datetime_at_date` function:
```rs
use chrono::{Duration, Local};
use parse_datetime::parse_datetime_at_date;
use jiff::{ToSpan, Zoned};
use parse_datetime::{parse_datetime_at_date, ParsedDateTime};
let now = Local::now();
let after = parse_datetime_at_date(now, "+3 days");
let now = Zoned::now();
let after = parse_datetime_at_date(now.clone(), "+3 days");
assert_eq!(
(now + Duration::days(3)).naive_utc(),
after.unwrap().naive_utc()
);
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 parse_datetime::parse_datetime;
use chrono::{Local, TimeZone};
use jiff::{civil::{date, time} ,Zoned};
use parse_datetime::{parse_datetime, ParsedDateTime};
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());
match dt.unwrap() {
ParsedDateTime::InRange(z) => assert_eq!(z, Zoned::now().with().date(date(2021, 2, 14)).time(time(6, 37, 47, 0)).build().unwrap()),
ParsedDateTime::Extended(_) => unreachable!("unexpected for this input"),
}
```
For years beyond jiff's representable range (e.g., year 10000+), the result is an `ExtendedDateTime`:
```rs
use parse_datetime::{parse_datetime, ParsedDateTime};
let dt = parse_datetime("12000-01-01").unwrap();
match dt {
ParsedDateTime::Extended(ext) => {
assert_eq!(ext.year, 12000);
assert_eq!(ext.month, 1);
assert_eq!(ext.day, 1);
}
ParsedDateTime::InRange(_) => unreachable!("year 12000 is out of jiff range"),
}
```
### Supported Formats
@@ -56,9 +77,8 @@ The `parse_datetime` and `parse_datetime_at_date` functions support absolute dat
- "now" or "today"
- "yesterday"
- "tomorrow"
- use "ago" for the past
- use "ago" for the past or "hence" for the future
- use "next" or "last" with `unit` (e.g., "next week", "last year")
- combined units with "and" or "," (e.g., "2 years and 1 month", "1 day, 2 hours" or "2 weeks 1 second")
- unix timestamps (for example "@0" "@1344000")
`num` can be a positive or negative integer.
@@ -70,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(DateTime<FixedOffset>)` - If the input string can be parsed as a datetime
- `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
+155
View File
@@ -0,0 +1,155 @@
use codspeed_criterion_compat::{criterion_group, criterion_main, Criterion};
use jiff::Zoned;
use parse_datetime::{parse_datetime, parse_datetime_at_date};
fn bench_iso_datetime(c: &mut Criterion) {
c.bench_function("parse_iso_datetime", |b| {
b.iter(|| parse_datetime("2021-02-14 06:37:47 +0000"))
});
}
fn bench_iso_datetime_t_sep(c: &mut Criterion) {
c.bench_function("parse_iso_datetime_t_separator", |b| {
b.iter(|| parse_datetime("2021-02-14T22:37:47-0800"))
});
}
fn bench_date_only(c: &mut Criterion) {
c.bench_function("parse_date_only", |b| {
b.iter(|| parse_datetime("1997-01-01"))
});
}
fn bench_date_slash_format(c: &mut Criterion) {
c.bench_function("parse_date_slash_format", |b| {
b.iter(|| parse_datetime("05/07/1987"))
});
}
fn bench_epoch_timestamp(c: &mut Criterion) {
c.bench_function("parse_epoch_timestamp", |b| {
b.iter(|| parse_datetime("@1613371067"))
});
}
fn bench_relative_time(c: &mut Criterion) {
let now = Zoned::now();
c.bench_function("parse_relative_time", |b| {
b.iter(|| parse_datetime_at_date(now.clone(), "+3 days"))
});
}
fn bench_relative_time_complex(c: &mut Criterion) {
let now = Zoned::now();
c.bench_function("parse_relative_time_complex", |b| {
b.iter(|| parse_datetime_at_date(now.clone(), "1 year 3 months 2 days ago"))
});
}
fn bench_relative_keywords(c: &mut Criterion) {
c.bench_function("parse_yesterday", |b| {
b.iter(|| parse_datetime("yesterday"))
});
c.bench_function("parse_tomorrow", |b| b.iter(|| parse_datetime("tomorrow")));
c.bench_function("parse_now", |b| b.iter(|| parse_datetime("now")));
}
fn bench_weekday(c: &mut Criterion) {
// Use a fixed reference date: the cost of resolving a weekday depends on
// how many days separate it from "today", so relying on the current date
// makes the measurement vary with the day the benchmark runs.
// 2000-01-03 is a Monday, so "wednesday" resolves two days ahead.
let base = jiff::civil::DateTime::from(jiff::civil::date(2000, 1, 3))
.to_zoned(jiff::tz::TimeZone::UTC)
.unwrap();
c.bench_function("parse_weekday", |b| {
b.iter(|| parse_datetime_at_date(base.clone(), "wednesday"))
});
}
fn bench_timezone_offset(c: &mut Criterion) {
c.bench_function("parse_timezone_offset", |b| {
b.iter(|| parse_datetime("UTC+07:00"))
});
}
fn bench_datetime_with_delta(c: &mut Criterion) {
c.bench_function("parse_datetime_with_delta", |b| {
b.iter(|| parse_datetime("1997-01-01 00:00:00 +0000 +1 year"))
});
}
fn bench_ctime_format(c: &mut Criterion) {
c.bench_function("parse_ctime_format", |b| {
b.iter(|| parse_datetime("Wed Jan 1 00:00:00 1997"))
});
}
fn bench_datetime_with_timezone_name(c: &mut Criterion) {
c.bench_function("parse_datetime_with_tz_name", |b| {
b.iter(|| parse_datetime("1997-01-19 08:17:48 BRT"))
});
}
fn bench_datetime_ending_in_z(c: &mut Criterion) {
c.bench_function("parse_datetime_ending_in_z", |b| {
b.iter(|| parse_datetime("2023-06-03 12:00:01Z"))
});
}
fn bench_invalid_input(c: &mut Criterion) {
c.bench_function("parse_invalid_input", |b| {
b.iter(|| parse_datetime("NotADate"))
});
}
fn bench_extended_year(c: &mut Criterion) {
c.bench_function("parse_extended_year", |b| {
b.iter(|| parse_datetime("10000-01-01"))
});
}
fn bench_extended_year_rollover(c: &mut Criterion) {
c.bench_function("parse_extended_year_rollover", |b| {
b.iter(|| parse_datetime("9999-12-31 +1 day"))
});
}
fn bench_extended_year_relative(c: &mut Criterion) {
let base = jiff::civil::DateTime::from(jiff::civil::date(2000, 1, 1))
.to_zoned(jiff::tz::TimeZone::UTC)
.unwrap();
c.bench_function("parse_extended_year_relative", |b| {
b.iter(|| parse_datetime_at_date(base.clone(), "10000-01-01 +1 day"))
});
}
fn bench_extended_large_year(c: &mut Criterion) {
c.bench_function("parse_extended_large_year", |b| {
b.iter(|| parse_datetime("999999-06-15"))
});
}
criterion_group!(
benches,
bench_iso_datetime,
bench_iso_datetime_t_sep,
bench_date_only,
bench_date_slash_format,
bench_epoch_timestamp,
bench_relative_time,
bench_relative_time_complex,
bench_relative_keywords,
bench_weekday,
bench_timezone_offset,
bench_datetime_with_delta,
bench_ctime_format,
bench_datetime_with_timezone_name,
bench_datetime_ending_in_z,
bench_invalid_input,
bench_extended_year,
bench_extended_year_rollover,
bench_extended_year_relative,
bench_extended_large_year,
);
criterion_main!(benches);
+233 -243
View File
File diff suppressed because it is too large. Load diff
+8 -2
View File
@@ -8,8 +8,8 @@ cargo-fuzz = true
[dependencies]
libfuzzer-sys = "0.4.7"
regex = "1.10.4"
chrono = { version="0.4", default-features=false, features=["std", "alloc", "clock"] }
arbitrary = { version = "1", features = ["derive"] }
jiff = "0.2"
[dependencies.parse_datetime]
path = "../"
@@ -19,3 +19,9 @@ name = "fuzz_parse_datetime"
path = "fuzz_targets/parse_datetime.rs"
test = false
doc = false
[[bin]]
name = "fuzz_large_year"
path = "fuzz_targets/large_year.rs"
test = false
doc = false
+64
View File
@@ -0,0 +1,64 @@
#![no_main]
use arbitrary::Arbitrary;
use jiff::{civil::DateTime, tz::TimeZone};
use libfuzzer_sys::fuzz_target;
#[derive(Arbitrary, Debug)]
struct Input {
/// Year for the base date (biased toward boundary years).
base_year_selector: u8,
/// Year to embed in a constructed large-year input string.
input_year: u32,
month: u8,
day: u8,
/// Suffix appended after the constructed date (e.g. relative items).
suffix: String,
/// Whether to also call parse_datetime (no base).
try_no_base: bool,
}
fn base_year(selector: u8) -> i16 {
match selector % 6 {
0 => 2024,
1 => 9998,
2 => 9999,
3 => 1,
4 => 100,
_ => (selector as i16) * 40,
}
}
fn clamp_year(y: u32) -> u32 {
// Focus on the interesting range: 9990..=100_000 and 0..=20_000
match y % 4 {
0 => 9990 + (y % 20), // near boundary
1 => 10000 + (y % 90_000), // large years
2 => y % 20_000, // general range
_ => 2_147_485_540 + (y % 10), // near GNU_MAX_YEAR
}
}
fuzz_target!(|input: Input| {
let year = clamp_year(input.input_year);
let month = (input.month % 12) + 1;
let day = (input.day % 28) + 1;
let date_str = format!("{year:04}-{month:02}-{day:02} {}", input.suffix);
// Test parse_datetime (uses current time as base).
if input.try_no_base {
let _ = parse_datetime::parse_datetime(&date_str);
}
// Test parse_datetime_at_date with a controlled base.
let by = base_year(input.base_year_selector);
if let Ok(base) = DateTime::new(by, 1, 1, 0, 0, 0, 0) {
if let Ok(base) = base.to_zoned(TimeZone::UTC) {
let _ = parse_datetime::parse_datetime_at_date(base, &date_str);
}
}
// Also try a bare large year as a pure number.
let bare = format!("{year}");
let _ = parse_datetime::parse_datetime(&bare);
});
+757
View File
File diff suppressed because it is too large. Load diff
+1069
View File
File diff suppressed because it is too large. Load diff
+72 -21
View File
@@ -12,40 +12,63 @@
//! > 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.
//! > support nanosecond timestamp resolution; excess precision is silently
//! > discarded.
use winnow::{
combinator::{alt, trace},
seq, ModalResult, Parser,
ModalResult, Parser,
};
use crate::items::space;
use super::{
date::{self, Date},
s,
time::{self, Time},
};
use super::{date, primitive::s, time};
#[derive(PartialEq, Debug, Clone, Default)]
pub struct DateTime {
pub(crate) date: Date,
pub(crate) time: Time,
pub(crate) struct DateTime {
pub(crate) date: date::Date,
pub(crate) time: time::Time,
}
pub fn parse(input: &mut &str) -> ModalResult<DateTime> {
seq!(DateTime {
date: trace("date iso", alt((date::iso1, date::iso2))),
// Note: the `T` is lowercased by the main parse function
_: alt((s('t').void(), (' ', space).void())),
time: trace("time iso", time::iso),
})
.parse_next(input)
fn remaining_starts_with_meridiem(input: &str) -> bool {
let trimmed = input.trim_start();
trimmed.starts_with("am")
|| trimmed.starts_with("pm")
|| trimmed.starts_with("a.m.")
|| trimmed.starts_with("p.m.")
}
/// Parse the separator and time of a combined item whose ISO date has already
/// been parsed.
pub(crate) fn parse_time_after_date(input: &mut &str, date: date::Date) -> ModalResult<DateTime> {
// Note: the `T` is lowercased by the main parse function
alt((s('t').void(), (' ', space).void())).parse_next(input)?;
let mut iso_input = *input;
if let Ok(parsed_time) = trace("iso_time", time::iso).parse_next(&mut iso_input) {
if !remaining_starts_with_meridiem(iso_input) {
*input = iso_input;
return Ok(DateTime {
date,
time: parsed_time,
});
}
}
let time = trace("iso_time", time::parse).parse_next(input)?;
Ok(DateTime { date, time })
}
#[cfg(test)]
mod tests {
use super::{parse, DateTime};
use crate::items::{date::Date, time::Time};
use winnow::{combinator::alt, ModalResult, Parser};
use super::{parse_time_after_date, DateTime};
use crate::items::{date, date::Date, time::Time};
fn parse(input: &mut &str) -> ModalResult<DateTime> {
let date = alt((date::iso1, date::iso2)).parse_next(input)?;
parse_time_after_date(input, date)
}
#[test]
fn some_date() {
@@ -58,7 +81,8 @@ mod tests {
time: Time {
hour: 10,
minute: 10,
second: 55.0,
second: 55,
nanosecond: 0,
offset: None,
},
});
@@ -75,4 +99,31 @@ mod tests {
assert_eq!(parse(&mut s).ok(), reference, "Failed string: {old_s}")
}
}
#[test]
fn date_and_time_ampm() {
let reference = Some(DateTime {
date: Date {
day: 15,
month: 6,
year: Some(2024),
},
time: Time {
hour: 15,
minute: 0,
second: 0,
nanosecond: 0,
offset: None,
},
});
for mut s in [
"2024-06-15 3:00 pm",
"2024-06-15 3:00pm",
"2024-06-15 3:00 p.m.",
] {
let old_s = s.to_owned();
assert_eq!(parse(&mut s).ok(), reference, "Failed string: {old_s}");
}
}
}
+497 -133
View File
File diff suppressed because it is too large. Load diff
+137
View File
@@ -0,0 +1,137 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
//! Parse a timestamp item.
//!
//! From the GNU docs:
//!
//! > If you precede a number with ‘@’, it represents an internal timestamp as
//! > a count of seconds. The number can contain an internal decimal point
//! > (either ‘.’ or ‘,’); any excess precision not supported by the internal
//! > representation is truncated toward minus infinity. Such a number cannot
//! > be combined with any other date item, as it specifies a complete
//! > timestamp.
//! >
//! > On most hosts, these counts ignore the presence of leap seconds. For
//! > example, on most hosts ‘@1483228799’ represents 2016-12-31 23:59:59 UTC,
//! > ‘@1483228800’ represents 2017-01-01 00:00:00 UTC, and there is no way to
//! > represent the intervening leap second 2016-12-31 23:59:60 UTC.
use winnow::{
ascii::digit1,
combinator::{opt, preceded},
token::one_of,
ModalResult, Parser,
};
use super::primitive::{dec_uint, plus_or_minus, s};
/// Represents a timestamp with nanosecond accuracy.
///
/// # Invariants
///
/// - `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, Clone, Copy)]
pub(super) struct Timestamp {
second: i64,
nanosecond: u32,
}
impl TryFrom<Timestamp> for jiff::Timestamp {
type Error = &'static str;
fn try_from(ts: Timestamp) -> Result<Self, Self::Error> {
jiff::Timestamp::new(
ts.second,
i32::try_from(ts.nanosecond).map_err(|_| "nanosecond in timestamp exceeds i32::MAX")?,
)
.map_err(|_| "timestamp value is out of valid range")
}
}
/// Parse a timestamp in the form of `@1234567890` or `@-1234567890.12345` or
/// `@1234567890,12345`.
pub(super) fn parse(input: &mut &str) -> ModalResult<Timestamp> {
(s("@"), opt(plus_or_minus), s(sec_and_nsec))
.verify_map(|(_, sign, (sec, nsec))| {
let sec = i64::try_from(sec).ok()?;
let (second, nanosecond) = match (sign, nsec) {
(Some('-'), 0) => (-sec, 0),
// Truncate towards minus infinity.
(Some('-'), _) => ((-sec).checked_sub(1)?, 1_000_000_000 - nsec),
_ => (sec, nsec),
};
Some(Timestamp { second, nanosecond })
})
.parse_next(input)
}
/// Parse a second value in the form of `1234567890` or `1234567890.12345` or
/// `1234567890,12345`.
///
/// The first part represents whole seconds. The optional second part represents
/// fractional seconds, parsed as a nanosecond value from up to 9 digits
/// (padded with zeros on the right if fewer digits are present). If the second
/// part is omitted, it defaults to 0 nanoseconds.
pub(super) fn sec_and_nsec(input: &mut &str) -> ModalResult<(u64, u32)> {
(dec_uint, opt(preceded(one_of(['.', ',']), digit1)))
.verify_map(|(sec, opt_nsec_str)| match opt_nsec_str {
Some(nsec_str) if nsec_str.len() >= 9 => Some((sec, nsec_str[..9].parse().ok()?)),
Some(nsec_str) => {
let multiplier = 10_u32.pow(9 - nsec_str.len() as u32);
Some((sec, nsec_str.parse::<u32>().ok()?.checked_mul(multiplier)?))
}
None => Some((sec, 0)),
})
.parse_next(input)
}
#[cfg(test)]
mod tests {
use super::*;
fn ts(second: i64, nanosecond: u32) -> Timestamp {
Timestamp { second, nanosecond }
}
#[test]
fn parse_sec_and_nsec() {
for (input, expected) in [
("1234567890", (1234567890, 0)), // only seconds
("1234567890.12345", (1234567890, 123450000)), // seconds and nanoseconds, '.' as floating point
("1234567890,12345", (1234567890, 123450000)), // seconds and nanoseconds, ',' as floating point
("1234567890.1234567890123", (1234567890, 123456789)), // nanoseconds with more than 9 digits, truncated
] {
let mut s = input;
assert_eq!(sec_and_nsec(&mut s).unwrap(), expected, "{input}");
}
for input in [
".1234567890", // invalid: no leading seconds
"-1234567890", // invalid: negative input not allowed
] {
let mut s = input;
assert!(sec_and_nsec(&mut s).is_err(), "{input}");
}
}
#[test]
fn timestamp() {
for (input, expected) in [
("@1234567890", ts(1234567890, 0)), // positive seconds, no nanoseconds
("@ 1234567890", ts(1234567890, 0)), // space after '@', positive seconds, no nanoseconds
("@-1234567890", ts(-1234567890, 0)), // negative seconds, no nanoseconds
("@ -1234567890", ts(-1234567890, 0)), // space after '@', negative seconds, no nanoseconds
("@ - 1234567890", ts(-1234567890, 0)), // space after '@' and after '-', negative seconds, no nanoseconds
("@1234567890.12345", ts(1234567890, 123450000)), // positive seconds with nanoseconds, '.' as floating point
("@1234567890,12345", ts(1234567890, 123450000)), // positive seconds with nanoseconds, ',' as floating point
("@-1234567890.12345", ts(-1234567891, 876550000)), // negative seconds with nanoseconds, '.' as floating point
("@1234567890.1234567890123", ts(1234567890, 123456789)), // nanoseconds with more than 9 digits, truncated
] {
let mut s = input;
assert_eq!(parse(&mut s).unwrap(), expected, "{input}");
}
}
}
+38
View File
@@ -0,0 +1,38 @@
use std::fmt;
use winnow::error::{ContextError, ErrMode};
#[derive(Debug)]
pub(crate) enum Error {
ParseError(String),
}
impl std::error::Error for Error {}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::ParseError(reason) => {
write!(f, "{reason}")
}
}
}
}
impl From<&'static str> for Error {
fn from(reason: &'static str) -> Self {
Error::ParseError(reason.to_owned())
}
}
impl From<ErrMode<ContextError>> for Error {
fn from(err: ErrMode<ContextError>) -> Self {
Error::ParseError(err.to_string())
}
}
impl From<jiff::Error> for Error {
fn from(err: jiff::Error) -> Self {
Error::ParseError(err.to_string())
}
}
+746 -410
View File
File diff suppressed because it is too large. Load diff
+566
View File
File diff suppressed because it is too large. Load diff
+41 -4
View File
@@ -1,14 +1,15 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use super::s;
use winnow::{
ascii::{alpha1, dec_uint},
ascii::alpha1,
combinator::{alt, opt},
ModalResult, Parser,
};
pub fn ordinal(input: &mut &str) -> ModalResult<i32> {
use super::primitive::{dec_uint, s};
pub(super) fn ordinal(input: &mut &str) -> ModalResult<i32> {
alt((text_ordinal, number_ordinal)).parse_next(input)
}
@@ -34,7 +35,7 @@ fn text_ordinal(input: &mut &str) -> ModalResult<i32> {
"fifth" => 5,
"sixth" => 6,
"seventh" => 7,
"eight" => 8,
"eighth" => 8,
"ninth" => 9,
"tenth" => 10,
"eleventh" => 11,
@@ -44,3 +45,39 @@ fn text_ordinal(input: &mut &str) -> ModalResult<i32> {
})
.parse_next(input)
}
#[cfg(test)]
mod tests {
use super::ordinal;
#[test]
fn text_ordinal() {
for (s, expected) in [
("last", -1),
("this", 0),
("next", 1),
("first", 1),
("third", 3),
("fourth", 4),
("fifth", 5),
("sixth", 6),
("seventh", 7),
("eighth", 8),
("ninth", 9),
("tenth", 10),
("eleventh", 11),
("twelfth", 12),
] {
let mut input = s;
assert_eq!(ordinal(&mut input).unwrap(), expected, "{s}");
assert!(input.is_empty(), "{s}");
}
}
#[test]
fn invalid_text_ordinal() {
for mut s in ["eight", "second", "foo"] {
assert!(ordinal(&mut s).is_err(), "{s}");
}
}
}
+256
View File
@@ -0,0 +1,256 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
//! Primitive combinators.
use std::str::FromStr;
use winnow::{
ascii::{digit1, Uint},
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)
}
/// Same as winnow's `multispace0`, but also accepting `\v` and `\f`, like
/// the whitespace GNU `date` accepts between tokens
fn multispace0<'a, E>(input: &mut &'a str) -> winnow::Result<&'a str, E>
where
E: ParserError<&'a str>,
{
let len = input
.bytes()
.take_while(|b| matches!(b, b' ' | b'\t' | b'\n' | b'\x0B' | b'\x0C' | b'\r'))
.count();
let (ws, rest) = input.split_at(len);
*input = rest;
Ok(ws)
}
/// Same as [`multispace0`], but requiring at least one character
pub(super) fn multispace1<'a, E>(input: &mut &'a str) -> winnow::Result<&'a str, E>
where
E: ParserError<&'a str>,
{
take_while(1.., (' ', '\t', '\n', '\x0B', '\x0C', '\r')).parse_next(input)
}
/// Parse the space in-between tokens
///
/// You probably want to use the [`s`] combinator instead.
pub(super) fn space<'a, E>(input: &mut &'a str) -> winnow::Result<(), E>
where
E: ParserError<&'a str>,
{
// Fast path: most calls see no comment or ignorable sign
multispace0.parse_next(input)?;
if !input.starts_with(['(', '-', '+']) {
return Ok(());
}
separated(0.., multispace0, alt((comment, ignored_hyphen_or_plus))).parse_next(input)
}
/// 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, O, E>(input: &mut &'a str) -> winnow::Result<O, E>
where
O: Uint + FromStr,
E: ParserError<&'a str>,
{
dec_uint_str
.verify_map(|s: &str| s.parse().ok())
.parse_next(input)
}
/// Parse an unsigned decimal integer as a string slice.
pub(super) fn dec_uint_str<'a, E>(input: &mut &'a str) -> winnow::Result<&'a str, E>
where
E: ParserError<&'a str>,
{
digit1.void().take().parse_next(input)
}
/// Parse a colon preceded by whitespace.
pub(super) fn colon<'a, E>(input: &mut &'a str) -> winnow::Result<(), E>
where
E: ParserError<&'a str>,
{
s(':').void().parse_next(input)
}
/// Parse a plus or minus character optionally preceeded by whitespace.
pub(super) fn plus_or_minus<'a, E>(input: &mut &'a str) -> winnow::Result<char, E>
where
E: ParserError<&'a str>,
{
s(alt(('+', '-'))).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
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_dec_int() {
for (input, expected) in [
("123", 123), // positive without sign
("+123", 123), // positive with '+' sign
("-123", -123), // negative with '-' sign
("0", 0), // zero
("+0", 0), // zero with '+' sign
("-0", 0), // zero with '-' sign (parses as 0)
("012", 12), // zero-prefixed (the main reason for this function)
("+012", 12), // zero-prefixed with '+' sign
("-012", -12), // zero-prefixed with '-' sign
("00123", 123), // multiple leading zeros
("2147483647", 2147483647), // i32::MAX
("-2147483648", -2147483648), // i32::MIN
] {
let mut s = input;
assert_eq!(
dec_int::<ContextError>(&mut s).unwrap(),
expected,
"{input}"
);
}
for input in [
"", // empty string
"+", // sign without digits
"-", // sign without digits
"abc", // non-numeric
"12a", // starts with digits but has non-digit after (but should parse "12" successfully)
] {
let mut s = input;
let result = dec_int::<ContextError>(&mut s);
// Note: "12a" will actually succeed and parse "12", leaving "a" unparsed
if input == "12a" {
assert_eq!(result.unwrap(), 12, "{input}");
assert_eq!(s, "a", "Should leave 'a' unparsed");
} else {
assert!(result.is_err(), "{input} should fail");
}
}
// Test overflow cases
for input in [
"2147483648", // i32::MAX + 1
"-2147483649", // i32::MIN - 1
"99999999999", // way too large
] {
let mut s = input;
assert!(
dec_int::<ContextError>(&mut s).is_err(),
"{input} should overflow"
);
}
}
#[test]
fn space_skips_comments_and_ignored_signs() {
for (input, rest) in [
("", ""),
("abc", "abc"),
(" \t abc", "abc"),
("(foo) abc", "abc"),
(" (a (b)) abc", "abc"),
("- abc", "abc"),
("+ (c) abc", "abc"),
("- (c) 12", "12"),
// A sign followed by a digit is not ignored.
("- 12", "- 12"),
(" +12", "+12"),
// An unbalanced comment is not skipped.
("(foo abc", "(foo abc"),
] {
let mut s = input;
space::<ContextError>(&mut s).unwrap();
assert_eq!(s, rest, "{input:?}");
}
}
}
Loaded 20 of 32 files, more files were not shown because too many files have changed in this diff. Show more