167 Commits

Author SHA1 Message Date
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 Zhang 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
Sylvestre Ledru 1552608bce Merge pull request #145 from yuankunzhang/fallible-offset-conversion
fallible offset conversion
2025-05-26 18:08:50 +02:00
Sylvestre Ledru ee9beb07d3 prepare release 0.10.0 2025-05-26 16:50:31 +02:00
Daniel Hofstetter 6f073addf5 Merge pull request #146 from drinkcat/fix-tests
Fix/improve tests
2025-05-26 13:59:44 +02:00
Nicolas Boichat a9676626be src/items: Fix timezone tests
The current tests look wrong ("%Z" shouldn't print an offset, but
this accidentally works due to the way the parsing logic operates),
fix that.
2025-05-25 21:34:22 +02:00
Nicolas Boichat da0af495e8 test: Replace %+ format with explicit one
%+ is an extension that jiff doesn't support (if we still want to
convert parse_datetime in the future, and since these are tests,
it's probably better to use an explicit format.
2025-05-25 21:34:22 +02:00
Nicolas Boichat e00555cd96 src/lib: Add end-to-end timezone test
Also make the offset test a little bit smarter.
2025-05-25 21:34:22 +02:00
yuankunzhang ab91b2b6d7 fallible offset conversion 2025-05-20 21:30:35 +08:00
Terts Diepraam cc7c143b21 Merge pull request #141 from yuankunzhang/main
Upgrade winnow to 0.7.10
2025-05-19 15:05:43 +02:00
Daniel Hofstetter 7721ae2cd8 Merge pull request #142 from yanganto/msrv
Specify MSRV 1.71.1
2025-05-14 16:06:27 +02:00
Antonio Yang 557c3a3ae3 Specify MSRV 1.71.1 2025-05-14 00:59:47 +08:00
yuankunzhang bea8ec2641 upgrade winnow to 0.7.10 2025-05-12 19:46:33 +08:00
Terts Diepraam 852e49e224 Merge pull request #138 from yuankunzhang/code-cleanup
Code cleanup
2025-05-09 13:31:02 +02:00
yuankunzhang 2390ef0b25 code cleanup 2025-05-09 18:55:10 +08:00
Terts Diepraam 77156f46d2 Merge pull request #137 from yuankunzhang/replace-the-regex-parser
Replace the regex parser with winnow-based parse
2025-05-09 11:35:48 +02:00
yuankunzhang bc6b70bc9e code fixup to pass all tests 2025-05-07 21:54:42 +08:00
Daniel Hilst 2479c61dd5 Find a compatibility intersection between chrono and GNU 2025-04-30 19:53:34 +08:00
Daniel Hilst 4c3d2221da Restore the old API and add the missing combinators 2025-04-30 19:53:34 +08:00
Terts Diepraam 07d4b80ea5 start parsing date with winnow 2025-04-30 19:53:34 +08:00
Daniel Hofstetter b481a2bdec Merge pull request #136 from uutils/renovate/chrono-0.x-lockfile
fix(deps): update rust crate chrono to v0.4.41
2025-04-29 18:28:41 +02:00
renovate[bot] a387463373 fix(deps): update rust crate chrono to v0.4.41 2025-04-29 15:59:06 +00:00
Daniel Hofstetter b6795d9b2e Merge pull request #134 from dan-hipschman/handle-uppercase
Allow uppercase for words like "today", "next", etc.
2025-04-26 17:56:24 +02:00
Dan Hipschman 3bdafeb2cf Allow uppercase for words like "today", "next", etc. 2025-04-26 08:07:10 -07:00
30 changed files with 4676 additions and 2324 deletions
+20 -6
View File
@@ -4,6 +4,7 @@ name: Basic CI
env:
CARGO_TERM_COLOR: always
RUST_MIN_SRV: "1.71.1"
jobs:
check:
@@ -13,7 +14,7 @@ jobs:
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
- run: cargo check
@@ -24,7 +25,7 @@ jobs:
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
- run: cargo test
@@ -32,7 +33,7 @@ jobs:
name: cargo fmt --all -- --check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
- run: rustup component add rustfmt
- run: cargo fmt --all -- --check
@@ -44,11 +45,24 @@ jobs:
matrix:
os: [ubuntu-latest, macOS-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@stable
- run: rustup component add clippy
- 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@v5
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_MIN_SRV }}
- run: cargo test
coverage:
name: Code Coverage
runs-on: ${{ matrix.job.os }}
@@ -60,7 +74,7 @@ jobs:
- { os: macos-latest , features: macos }
- { os: windows-latest , features: windows }
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Initialize workflow variables
id: vars
shell: bash
@@ -138,7 +152,7 @@ jobs:
env:
RUN_FOR: 60
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: dtolnay/rust-toolchain@nightly
- name: Install `cargo-fuzz`
run: cargo install cargo-fuzz
Generated
+286 -184
View File
File diff suppressed because it is too large Load Diff
+8 -3
View File
@@ -1,13 +1,18 @@
[package]
name = "parse_datetime"
description = "parsing human-readable time strings and converting them to a DateTime"
version = "0.9.0"
version = "0.13.0"
edition = "2021"
license = "MIT"
repository = "https://github.com/uutils/parse_datetime"
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"] }
nom = "8.0.0"
winnow = "0.7.10"
num-traits = "0.2.19"
jiff = { version = "0.2.15", default-features = false, features = ["tz-system", "tzdb-bundle-platform", "tzdb-zoneinfo"] }
[dev-dependencies]
rstest = "0.26"
+11 -12
View File
@@ -4,15 +4,15 @@
[![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)
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 and converting them to a jiff's `Zoned` object.
## 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 +25,26 @@ cargo add parse_datetime
Then, import the crate and use the `parse_datetime_at_date` function:
```rs
use chrono::{Duration, Local};
use jiff::{ToSpan, Zoned};
use parse_datetime::parse_datetime_at_date;
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()
now.checked_add(3.days()).unwrap(),
after.unwrap()
);
```
For DateTime parsing, import the `parse_datetime` function:
```rs
use jiff::{civil::{date, time} ,Zoned};
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());
assert_eq!(dt.unwrap(), Zoned::now().with().date(date(2021, 2, 14)).time(time(6, 37, 47, 0)).build().unwrap());
```
### Supported Formats
@@ -58,7 +58,6 @@ The `parse_datetime` and `parse_datetime_at_date` functions support absolute dat
- "tomorrow"
- use "ago" for the past
- 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 +69,7 @@ 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(Zoned)` - If the input string can be parsed as a `Zoned` object
- `Err(ParseDateTimeError::InvalidInput)` - If the input string cannot be parsed
## Fuzzer
+165 -169
View File
@@ -4,52 +4,32 @@ version = 4
[[package]]
name = "aho-corasick"
version = "1.0.1"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67fc08ce920c31afb70f013dcce1bfc3a3195de6a228474e45e1f145b36f8d04"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "arbitrary"
version = "1.3.0"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2d098ff73c1ca148721f37baad5ea6a465a13f9573aba8641fbbbae8164a54e"
checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1"
[[package]]
name = "autocfg"
version = "1.1.0"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "bumpalo"
version = "3.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
[[package]]
name = "cc"
version = "1.2.10"
version = "1.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229"
checksum = "65193589c6404eb80b450d618eaf9a2cafaaafd57ecce47370519ef674a7bd44"
dependencies = [
"find-msvc-tools",
"jobserver",
"libc",
"shlex",
@@ -57,90 +37,98 @@ dependencies = [
[[package]]
name = "cfg-if"
version = "1.0.0"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9"
[[package]]
name = "chrono"
version = "0.4.40"
name = "find-msvc-tools"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c"
dependencies = [
"android-tzdata",
"iana-time-zone",
"num-traits",
"windows-link",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
checksum = "7fd99930f64d146689264c637b5af2f0233a933bef0d8570e2526bf9e083192d"
[[package]]
name = "fuzz_parse_datetime"
version = "0.2.0"
dependencies = [
"chrono",
"libfuzzer-sys",
"parse_datetime",
"regex",
]
[[package]]
name = "iana-time-zone"
version = "0.1.61"
name = "getrandom"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
"cfg-if",
"libc",
"r-efi",
"wasi",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
name = "jiff"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49"
dependencies = [
"cc",
"jiff-static",
"jiff-tzdb-platform",
"log",
"portable-atomic",
"portable-atomic-util",
"serde",
"windows-sys",
]
[[package]]
name = "jiff-static"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "jiff-tzdb"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524"
[[package]]
name = "jiff-tzdb-platform"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8"
dependencies = [
"jiff-tzdb",
]
[[package]]
name = "jobserver"
version = "0.1.32"
version = "0.1.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
dependencies = [
"getrandom",
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.70"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.142"
version = "0.2.175"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317"
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
[[package]]
name = "libfuzzer-sys"
version = "0.4.9"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf78f52d400cf2d84a3a973a78a592b4adc535739e0a5597a0da6f0c357adc75"
checksum = "5037190e1f70cbeef565bd267599242926f724d3b8a9f510fd7e0b540cfa4404"
dependencies = [
"arbitrary",
"cc",
@@ -148,72 +136,79 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.18"
version = "0.4.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "518ef76f2f87365916b142844c16d8fefd85039bc5699050210a7778ee1cd1de"
checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432"
[[package]]
name = "memchr"
version = "2.6.2"
version = "2.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5486aed0026218e61b8a01d5fbd5a0a134649abb71a0e53b7bc088529dced86e"
[[package]]
name = "nom"
version = "8.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
dependencies = [
"memchr",
]
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
[[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",
]
[[package]]
name = "once_cell"
version = "1.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
name = "parse_datetime"
version = "0.11.0"
dependencies = [
"jiff",
"num-traits",
"regex",
"winnow",
]
[[package]]
name = "parse_datetime"
version = "0.8.0"
name = "portable-atomic"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
[[package]]
name = "portable-atomic-util"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
dependencies = [
"chrono",
"nom",
"regex",
"portable-atomic",
]
[[package]]
name = "proc-macro2"
version = "1.0.59"
version = "1.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6aeca18b86b413c660b781aa319e4e2648a3e6f9eadc9b47e9038e6fe9f3451b"
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.28"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b9ab9c7eadfd8df19006f1cf1a4aed13540ed5cbc047010ece5826e10825488"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.11.1"
name = "r-efi"
version = "5.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
[[package]]
name = "regex"
version = "1.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23d7fd106d8c02486a8d64e778353d1cffe08ce79ac2e82f540c86d0facf6912"
dependencies = [
"aho-corasick",
"memchr",
@@ -223,9 +218,9 @@ dependencies = [
[[package]]
name = "regex-automata"
version = "0.4.8"
version = "0.4.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3"
checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6"
dependencies = [
"aho-corasick",
"memchr",
@@ -234,9 +229,38 @@ dependencies = [
[[package]]
name = "regex-syntax"
version = "0.8.5"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001"
[[package]]
name = "serde"
version = "1.0.221"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "341877e04a22458705eb4e131a1508483c877dca2792b3781d4e5d8a6019ec43"
dependencies = [
"serde_core",
]
[[package]]
name = "serde_core"
version = "1.0.221"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c459bc0a14c840cb403fc14b148620de1e0778c96ecd6e0c8c3cacb6d8d00fe"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.221"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6185cf75117e20e62b1ff867b9518577271e58abe0037c40bb4794969355ab0"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "shlex"
@@ -246,9 +270,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "syn"
version = "2.0.18"
version = "2.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32d41677bcbe24c20c52e7c70b0d8db04134c5d1066bf98662e2871ad200ea3e"
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
dependencies = [
"proc-macro2",
"quote",
@@ -257,80 +281,37 @@ dependencies = [
[[package]]
name = "unicode-ident"
version = "1.0.9"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15811caf2415fb889178633e7724bad2509101cde276048e013b9def5e51fa0"
checksum = "f63a545481291138910575129486daeaf8ac54aee4387fe7906919f7830c7d9d"
[[package]]
name = "wasm-bindgen"
version = "0.2.93"
name = "wasi"
version = "0.14.5+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5"
checksum = "a4494f6290a82f5fe584817a676a34b9d6763e8d9d18204009fb31dceca98fd4"
dependencies = [
"cfg-if",
"once_cell",
"wasm-bindgen-macro",
"wasip2",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.93"
name = "wasip2"
version = "1.0.0+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b"
checksum = "03fa2761397e5bd52002cd7e73110c71af2109aca4e521a9f40473fe685b0a24"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
"wit-bindgen",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.93"
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.93"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484"
[[package]]
name = "windows-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-link"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dccfd733ce2b1753b03b6d3c65edf020262ea35e20ccdf3e288043e6dd620e3"
[[package]]
name = "windows-targets"
version = "0.52.6"
@@ -394,3 +375,18 @@ name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "winnow"
version = "0.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21a0236b59786fed61e2a80582dd500fe61f18b5dca67a4a067d0bc9039339cf"
dependencies = [
"memchr",
]
[[package]]
name = "wit-bindgen"
version = "0.45.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c573471f125075647d03df72e026074b7203790d41351cd6edc96f46bcccd36"
-2
View File
@@ -8,8 +8,6 @@ cargo-fuzz = true
[dependencies]
libfuzzer-sys = "0.4.7"
regex = "1.10.4"
chrono = { version="0.4", default-features=false, features=["std", "alloc", "clock"] }
[dependencies.parse_datetime]
path = "../"
+290
View File
@@ -0,0 +1,290 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
use jiff::{civil, Span, Zoned};
use super::{date, epoch, error, offset, relative, time, weekday, year, Item};
/// 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(crate) struct DateTimeBuilder {
base: Option<Zoned>,
timestamp: Option<epoch::Timestamp>,
date: Option<date::Date>,
time: Option<time::Time>,
weekday: Option<weekday::Weekday>,
offset: Option<offset::Offset>,
timezone: Option<jiff::tz::TimeZone>,
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: Zoned) -> Self {
self.base = Some(base);
self
}
/// Sets the timezone rule for the builder.
///
/// By default, the builder uses the time zone rules indicated by the `TZ`
/// environment variable, or the system default rules if `TZ` is not set.
/// This method allows overriding the time zone rules.
fn set_timezone(mut self, tz: jiff::tz::TimeZone) -> Result<Self, &'static str> {
if self.timezone.is_some() {
return Err("timezone rule cannot appear more than once");
}
self.timezone = Some(tz);
Ok(self)
}
/// Sets a timestamp value. Timestamp values are exclusive to other date/time
/// items (date, time, weekday, timezone, relative adjustments).
pub(super) fn set_timestamp(mut self, ts: epoch::Timestamp) -> Result<Self, &'static str> {
if self.timestamp.is_some() {
return Err("timestamp cannot appear more than once");
} else if self.date.is_some()
|| self.time.is_some()
|| self.weekday.is_some()
|| self.offset.is_some()
|| !self.relative.is_empty()
{
return Err("timestamp cannot be combined with other date/time items");
}
self.timestamp = Some(ts);
Ok(self)
}
fn set_date(mut self, date: date::Date) -> Result<Self, &'static str> {
if self.timestamp.is_some() {
return Err("timestamp cannot be combined with other date/time items");
} else if self.date.is_some() {
return Err("date cannot appear more than once");
}
self.date = Some(date);
Ok(self)
}
fn set_time(mut self, time: time::Time) -> Result<Self, &'static str> {
if self.timestamp.is_some() {
return Err("timestamp cannot be combined with other date/time items");
} else if self.time.is_some() {
return Err("time cannot appear more than once");
} else if self.offset.is_some() && time.offset.is_some() {
return Err("time offset and timezone are mutually exclusive");
}
self.time = Some(time);
Ok(self)
}
fn set_weekday(mut self, weekday: weekday::Weekday) -> Result<Self, &'static str> {
if self.timestamp.is_some() {
return Err("timestamp cannot be combined with other date/time items");
} else if self.weekday.is_some() {
return Err("weekday cannot appear more than once");
}
self.weekday = Some(weekday);
Ok(self)
}
fn set_offset(mut self, timezone: offset::Offset) -> Result<Self, &'static str> {
if self.timestamp.is_some() {
return Err("timestamp cannot be combined with other date/time items");
} else if self.offset.is_some()
|| self.time.as_ref().and_then(|t| t.offset.as_ref()).is_some()
{
return Err("time offset cannot appear more than once");
}
self.offset = Some(timezone);
Ok(self)
}
fn push_relative(mut self, relative: relative::Relative) -> Result<Self, &'static str> {
if self.timestamp.is_some() {
return Err("timestamp cannot be combined with other date/time items");
}
self.relative.push(relative);
Ok(self)
}
/// Sets a pure number that can be interpreted as either a year or time
/// depending on the current state of the builder.
///
/// If a date is already set but lacks a year, the number is interpreted as
/// a year. Otherwise, it's interpreted as a time in HHMM, HMM, HH, or H
/// format.
fn set_pure(mut self, pure: String) -> Result<Self, &'static str> {
if self.timestamp.is_some() {
return Err("timestamp cannot be combined with other date/time items");
}
if let Some(date) = self.date.as_mut() {
if date.year.is_none() {
date.year = Some(year::year_from_str(&pure)?);
return Ok(self);
}
}
let (mut hour_str, mut minute_str) = match pure.len() {
1..=2 => (pure.as_str(), "0"),
3..=4 => pure.split_at(pure.len() - 2),
_ => {
return Err("pure number must be 1-4 digits when interpreted as time");
}
};
let hour = time::hour24(&mut hour_str).map_err(|_| "invalid hour in pure number")?;
let minute = time::minute(&mut minute_str).map_err(|_| "invalid minute in pure number")?;
let time = time::Time {
hour,
minute,
..Default::default()
};
self.set_time(time)
}
pub(super) fn build(self) -> Result<Zoned, error::Error> {
let base = self.base.unwrap_or(if let Some(tz) = &self.timezone {
jiff::Timestamp::now().to_zoned(tz.clone())
} else {
Zoned::now()
});
// If a timestamp is set, we use it to build the `Zoned` object.
if let Some(ts) = self.timestamp {
return Ok(jiff::Timestamp::try_from(ts)?.to_zoned(base.offset().to_time_zone()));
}
// 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.date.is_none()
&& self.time.is_none()
&& self.weekday.is_none()
&& self.offset.is_none()
&& self.timezone.is_none()
{
base
} else {
base.with().time(civil::time(0, 0, 0, 0)).build()?
};
if let Some(date) = self.date {
let d: civil::Date = if date.year.is_some() {
date.try_into()?
} else {
date.with_year(dt.date().year() as u16).try_into()?
};
dt = dt.with().date(d).build()?;
}
if let Some(time) = self.time.clone() {
if let Some(offset) = &time.offset {
dt = dt.datetime().to_zoned(offset.try_into()?)?;
}
let t: civil::Time = time.try_into()?;
dt = dt.with().time(t).build()?;
}
if let Some(weekday::Weekday { offset, day }) = self.weekday {
if self.time.is_none() {
dt = dt.with().time(civil::time(0, 0, 0, 0)).build()?;
}
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.date().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.since(civil::Weekday::Monday) as i32
- dt.date().weekday().since(civil::Weekday::Monday) as i32)
.rem_euclid(7)
+ offset.checked_mul(7).ok_or("multiplication overflow")?;
dt = dt.checked_add(Span::new().try_days(delta)?)?;
}
for rel in self.relative {
dt = dt.checked_add::<Span>(if let relative::Relative::Months(x) = rel {
// *NOTE* This is done in this way to conform to GNU behavior.
let days = dt.date().last_of_month().day() as i32;
Span::new().try_days(days.checked_mul(x).ok_or("multiplication overflow")?)?
} else {
rel.try_into()?
})?;
}
if let Some(offset) = self.offset {
let (offset, hour_adjustment) = offset.normalize();
dt = dt.checked_add(Span::new().hours(hour_adjustment))?;
dt = dt.datetime().to_zoned((&offset).try_into()?)?;
}
Ok(dt)
}
}
impl TryFrom<Vec<Item>> for DateTimeBuilder {
type Error = &'static str;
fn try_from(items: Vec<Item>) -> Result<Self, Self::Error> {
let mut builder = DateTimeBuilder::new();
for item in items {
builder = match item {
Item::Timestamp(ts) => builder.set_timestamp(ts)?,
Item::DateTime(dt) => builder.set_date(dt.date)?.set_time(dt.time)?,
Item::Date(d) => builder.set_date(d)?,
Item::Time(t) => builder.set_time(t)?,
Item::Weekday(weekday) => builder.set_weekday(weekday)?,
Item::Offset(offset) => builder.set_offset(offset)?,
Item::Relative(rel) => builder.push_relative(rel)?,
Item::TimeZone(tz) => builder.set_timezone(tz)?,
Item::Pure(pure) => builder.set_pure(pure)?,
}
}
Ok(builder)
}
}
+76
View File
@@ -0,0 +1,76 @@
// 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, primitive::s, time};
#[derive(PartialEq, Debug, Clone, Default)]
pub(crate) struct DateTime {
pub(crate) date: date::Date,
pub(crate) time: time::Time,
}
pub(crate) fn parse(input: &mut &str) -> ModalResult<DateTime> {
seq!(DateTime {
date: trace("iso_date", alt((date::iso1, date::iso2))),
// Note: the `T` is lowercased by the main parse function
_: alt((s('t').void(), (' ', space).void())),
time: trace("iso_time", time::iso),
})
.parse_next(input)
}
#[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,
nanosecond: 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}")
}
}
}
+604
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)]
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())
}
}
+576
View File
File diff suppressed because it is too large Load Diff
+468
View File
@@ -0,0 +1,468 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
//! Parse an offset item.
//!
//! From the GNU docs:
//!
//! > A “time zone item” specifies an international time zone, indicated by a
//! > small set of letters, e.g., UTC or Z for Coordinated Universal Time.
//! > Any included periods are ignored. By following a non-daylight-saving
//! > time zone by the string DST in a separate word (that is, separated by
//! > some white space), the corresponding daylight saving time zone may be
//! > specified. Alternatively, a non-daylight-saving time zone can be
//! > followed by a time zone correction, to add the two values. This is
//! > normally done only for UTC; for example, UTC+05:30 is equivalent to
//! > +05:30.
//! >
//! > Time zone items other than UTC and Z are obsolescent and are not
//! > recommended, because they are ambiguous; for example, EST has a
//! > different meaning in Australia than in the United States, and A has
//! > different meaning as a military time zone than as an obsolete RFC 822
//! > time zone. Instead, it's better to use unambiguous numeric time zone
//! > corrections like -0500, as described in the previous section.
//! >
//! > If neither a time zone item nor a time zone correction is supplied,
//! > timestamps are interpreted using the rules of the default time zone.
use std::fmt::Display;
use winnow::{
combinator::{alt, peek},
error::{ContextError, ErrMode},
stream::{AsChar, Stream},
token::take_while,
ModalResult, Parser,
};
use super::{
primitive::{colon, ctx_err, dec_uint, dec_uint_str, plus_or_minus, s},
relative,
};
/// Represents a time zone offset from UTC.
///
/// This struct is used to represent a time zone offset in hours and minutes,
/// with a boolean indicating whether the offset is negative (i.e., west of
/// UTC).
#[derive(PartialEq, Debug, Clone, Default)]
pub(super) struct Offset {
negative: bool,
hours: u8,
minutes: u8,
}
impl Offset {
/// Merge two timezone offsets.
///
/// Note: when parsing an offset from a string (e.g., "+08:00"), the hours
/// and minutes are validated to ensure they fall within valid bounds. In
/// contrast, merging two offsets does not perform such validation. This
/// behavior is intentional to match GNU date.
fn merge(self, offset: Offset) -> Offset {
fn combine(a: u16, neg_a: bool, b: u16, neg_b: bool) -> (u16, bool) {
if neg_a == neg_b {
(a + b, neg_a)
} else if a > b {
(a - b, neg_a)
} else {
(b - a, neg_b)
}
}
let (total_minutes, negative) = combine(
(self.hours as u16) * 60 + (self.minutes as u16),
self.negative,
(offset.hours as u16) * 60 + (offset.minutes as u16),
offset.negative,
);
let hours = (total_minutes / 60) as u8;
let minutes = (total_minutes % 60) as u8;
Offset {
negative,
hours,
minutes,
}
}
/// Normalize the offset so that the hour field is within the accepted range.
///
/// - If the hour field is less than 24, or exactly 24 with a zero minute,
/// the offset is already normalized, and the function returns the offset
/// itself along with a zero hour adjustment.
/// - Otherwise, the hour field is reduced to 23 while preserving the minute
/// field, and the function returns the normalized offset along with the
/// hour adjustment needed to reach the original offset.
pub(super) fn normalize(self) -> (Offset, i8) {
if self.hours < 24 || (self.hours == 24 && self.minutes == 0) {
return (self, 0);
}
let hour_adjustment = (self.hours as i8 - 23) * if self.negative { 1 } else { -1 };
(
Offset {
negative: self.negative,
hours: 23,
minutes: self.minutes,
},
hour_adjustment,
)
}
}
impl TryFrom<(bool, u8, u8)> for Offset {
type Error = &'static str;
fn try_from((negative, hours, minutes): (bool, u8, u8)) -> Result<Self, Self::Error> {
if hours > 24 {
return Err("timezone hour must be between 0 and 24");
}
if minutes > 60 || (hours == 24 && minutes != 0) {
return Err("timezone minute must be between 0 and 60");
}
Ok(Offset {
negative,
hours,
minutes,
})
}
}
impl TryFrom<&Offset> for jiff::tz::TimeZone {
type Error = &'static str;
fn try_from(
Offset {
negative,
hours,
minutes,
}: &Offset,
) -> Result<Self, Self::Error> {
let secs = (*hours as i32) * 3600 + (*minutes as i32) * 60;
let secs = if *negative { -secs } else { secs };
let offset = jiff::tz::Offset::from_seconds(secs).map_err(|_| "offset is invalid")?;
Ok(jiff::tz::TimeZone::fixed(offset))
}
}
impl Display for Offset {
fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
write!(
fmt,
"{}{:02}:{:02}",
if self.negative { "-" } else { "+" },
self.hours,
self.minutes
)
}
}
pub(super) fn parse(input: &mut &str) -> ModalResult<Offset> {
timezone_name_offset.parse_next(input)
}
/// Parse a timezone starting with `+` or `-`.
pub(super) fn timezone_offset(input: &mut &str) -> ModalResult<Offset> {
// Strings like "+8 years" are ambiguous, they can either be parsed as a
// timezone offset "+8" and a relative time "years", or just a relative time
// "+8 years". GNU date parses them the second way, so we do the same here.
//
// Return early if the input can be parsed as a relative time.
if peek(relative::parse).parse_next(input).is_ok() {
return Err(ErrMode::Backtrack(ContextError::new()));
}
alt((timezone_offset_colon, timezone_offset_colonless)).parse_next(input)
}
/// Parse a timezone by name, with an optional numeric offset appended.
fn timezone_name_offset(input: &mut &str) -> ModalResult<Offset> {
/// I'm assuming there are no timezone abbreviations with more
/// than 6 charactres
const MAX_TZ_SIZE: usize = 6;
let nextword = s(take_while(1..=MAX_TZ_SIZE, AsChar::is_alpha)).parse_next(input)?;
let tz = timezone_name_to_offset(nextword)?;
// Strings like "UTC +8 years" are ambiguous, they can either be parsed as
// "UTC+8" and "years", or "UTC" and "+8 years". GNU date parses them the
// second way, so we do the same here.
//
// Only process if the input cannot be parsed as a relative time.
if peek(relative::parse).parse_next(input).is_err() {
let start = input.checkpoint();
if let Ok(other_tz) = timezone_offset.parse_next(input) {
let new_tz = tz.merge(other_tz);
return Ok(new_tz);
};
input.reset(&start);
}
Ok(tz)
}
/// Parse a timezone offset with a colon separating hours and minutes, e.g.,
/// `+08:00`, `+8:00`, `+8:0`.
fn timezone_offset_colon(input: &mut &str) -> ModalResult<Offset> {
(plus_or_minus, s(dec_uint), s(colon), s(dec_uint))
.parse_next(input)
.and_then(|(sign, hours, _, minutes)| {
(sign == '-', hours, minutes)
.try_into()
.map_err(|e| ErrMode::Cut(ctx_err(e)))
})
}
/// Parse a timezone offset without colon, e.g., `+0800`, `+800`, `+08`, `+8`.
fn timezone_offset_colonless(input: &mut &str) -> ModalResult<Offset> {
(plus_or_minus, s(dec_uint_str))
.verify_map(|(sign, s)| {
// GNU date accepts numeric offset strings with leading zeroes. For
// example, `+000000110` is valid. In such cases, the string is
// truncated to the last four characters. Thus, `+000000110` becomes
// `+0110` (note that one leading zero is kept).
let s = if s.len() > 4 && s.trim_start_matches('0').len() <= 4 {
&s[s.len() - 4..]
} else {
s
};
// Hour and minute values are dependent on the length of the string.
// For example:
//
// - "5" -> 05:00
// - "05" -> 05:00
// - "530" -> 05:30 (the minute is the last two characters here)
// - "0530"-> 05:30
// - "0000530" -> 05:30
let (h_str, m_str) = match s.len() {
1 | 2 => (s, "0"),
3 => s.split_at(1),
4 => s.split_at(2),
_ => return None,
};
let hours = h_str.parse::<u8>().ok()?;
let minutes = m_str.parse::<u8>().ok()?;
Some((sign, hours, minutes))
})
.parse_next(input)
.and_then(|(sign, hours, minutes)| {
(sign == '-', hours, minutes)
.try_into()
.map_err(|e| ErrMode::Cut(ctx_err(e)))
})
}
/// Named timezone list.
///
/// The full list of timezones can be extracted from
/// https://www.timeanddate.com/time/zones/. GNU date only supports a subset of
/// these. We support the same subset as GNU date.
fn timezone_name_to_offset(input: &str) -> ModalResult<Offset> {
let mut offset_str = match input {
"z" => Ok("+0"),
"y" => Ok("-12"),
"x" => Ok("-11"),
"wet" => Ok("+0"),
"west" => Ok("+1"),
"wat" => Ok("+1"),
"w" => Ok("-10"),
"v" => Ok("-9"),
"utc" => Ok("+0"),
"u" => Ok("-8"),
"t" => Ok("-7"),
"sst" => Ok("-11"),
"sgt" => Ok("+8"),
"sast" => Ok("+2"),
"s" => Ok("-6"),
"r" => Ok("-5"),
"q" => Ok("-4"),
"pst" => Ok("-8"),
"pdt" => Ok("-7"),
"p" => Ok("-3"),
"o" => Ok("-2"),
"nzst" => Ok("+12"),
"nzdt" => Ok("+13"),
"nst" => Ok("-3:30"),
"ndt" => Ok("-2:30"),
"n" => Ok("-1"),
"mst" => Ok("-7"),
"msk" => Ok("+3"),
"msd" => Ok("+4"),
"mdt" => Ok("-6"),
"m" => Ok("+12"),
"l" => Ok("+11"),
"k" => Ok("+10"),
"jst" => Ok("+9"),
"ist" => Ok("+5:30"),
"i" => Ok("+9"),
"hst" => Ok("-10"),
"h" => Ok("+8"),
"gst" => Ok("+4"),
"gmt" => Ok("+0"),
"g" => Ok("+7"),
"f" => Ok("+6"),
"est" => Ok("-5"),
"eet" => Ok("+2"),
"eest" => Ok("+3"),
"edt" => Ok("-4"),
"eat" => Ok("+3"),
"e" => Ok("+5"),
"d" => Ok("+4"),
"cst" => Ok("-6"),
"clt" => Ok("-4"),
"clst" => Ok("-3"),
"cet" => Ok("+1"),
"cest" => Ok("+2"),
"cdt" => Ok("-5"),
"cat" => Ok("+2"),
"c" => Ok("+3"),
"bst" => Ok("+6"),
"brt" => Ok("-3"),
"brst" => Ok("-2"),
"b" => Ok("+2"),
"ast" => Ok("-3"),
"art" => Ok("-3"),
"akst" => Ok("-9"),
"akdt" => Ok("-8"),
"adt" => Ok("+4"),
"a" => Ok("+1"),
_ => Err(ErrMode::Backtrack(ContextError::new())),
}?;
timezone_offset(&mut offset_str)
}
#[cfg(test)]
mod tests {
use super::*;
fn off(negative: bool, hours: u8, minutes: u8) -> Offset {
Offset {
negative,
hours,
minutes,
}
}
#[test]
fn timezone_offset_with_colon() {
for (input, expected) in [
("+00:00", off(false, 0, 0)), // UTC
("-00:00", off(true, 0, 0)), // UTC
("+01:00", off(false, 1, 0)), // positive offset
("-06:00", off(true, 6, 0)), // negative offset
("+05:30", off(false, 5, 30)), // positive offset with non-zero minutes
("-03:30", off(true, 3, 30)), // negative offset with non-zero minutes
("- 06:00", off(true, 6, 0)), // space after sign
("- 06 : 00", off(true, 6, 0)), // space around colon
("+5:3", off(false, 5, 3)), // single-digit hours and single-digit minutes
("+5:03", off(false, 5, 3)), // single-digit hours
("+05:3", off(false, 5, 3)), // single-digit minutes
("+00005:00030", off(false, 5, 30)), // leading zeroes in hours and minutes
("+00:00abc", off(false, 0, 0)), // space separator can be ignored if immediately followed by alphas (GNU date behavior)
] {
let mut s = input;
assert_eq!(timezone_offset(&mut s).unwrap(), expected, "{input}");
}
for input in [
"+25:00", // invalid: hours > 24
"-23:61", // invalid: minutes > 60
"+24:01", // invalid: minutes > 0 when hours == 24
] {
let mut s = input;
assert!(timezone_offset(&mut s).is_err(), "{input}");
}
}
#[test]
fn timezone_offset_without_colon() {
for (input, expected) in [
("+0000", off(false, 0, 0)), // UTC
("-0000", off(true, 0, 0)), // UTC
("+0100", off(false, 1, 0)), // positive offset
("-0600", off(true, 6, 0)), // negative offset
("+0530", off(false, 5, 30)), // positive offset with non-zero minutes
("-0330", off(true, 3, 30)), // negative offset with non-zero minutes
("- 0330", off(true, 3, 30)), // space after sign
("+530", off(false, 5, 30)), // single-digit hours
("+05", off(false, 5, 0)), // double-digit hours and no minutes
("+5", off(false, 5, 0)), // single-digit hours and no minutes
("+00000530", off(false, 5, 30)), // leading zeroes
("+0000abc", off(false, 0, 0)), // space separator can be ignored if immediately followed by alphas (GNU date behavior)
] {
let mut s = input;
assert_eq!(timezone_offset(&mut s).unwrap(), expected, "{input}");
}
for input in [
"+2500", // invalid: hours > 24
"-2361", // invalid: minutes > 60
"+2401", // invalid: minutes > 0 when hours == 24
"+23 days", // invalid: ambiguous with relative time parsing
] {
let mut s = input;
assert!(timezone_offset(&mut s).is_err(), "{input}");
}
}
#[test]
fn timezone_name_without_offset() {
for (input, expected) in [
("utc", off(false, 0, 0)), // UTC
("gmt", off(false, 0, 0)), // UTC
("z", off(false, 0, 0)), // UTC
("west", off(false, 1, 0)), // positive offset
("cst", off(true, 6, 0)), // negative offset
("ist", off(false, 5, 30)), // positive offset with non-zero minutes
("nst", off(true, 3, 30)), // negative offset with non-zero minutes
("z123", off(false, 0, 0)), // space separator can be ignored if immediately followed by digits (GNU date behavior)
] {
let mut s = input;
assert_eq!(timezone_name_offset(&mut s).unwrap(), expected, "{input}");
}
for input in [
"abc", // invalid: non-existent timezone
"utcabc", // invalid: non-existent timezone
] {
let mut s = input;
assert!(timezone_name_offset(&mut s).is_err(), "{input}");
}
}
#[test]
fn timezone_name_with_offset() {
for (input, expected) in [
("utc+5:30", off(false, 5, 30)), // UTC with possitive offset
("utc-5:30", off(true, 5, 30)), // UTC with negative offset
("utc +5:30", off(false, 5, 30)), // space after timezone name
("utc + 5 : 30", off(false, 5, 30)), // spaces
("a+5:30", off(false, 6, 30)), // merge two positive offsets (a=+1)
("a-5:30", off(true, 4, 30)), // merge positive and negative offsets (a=+1)
("n-5:30", off(true, 6, 30)), // merge two negative offsets (n=-1)
("n+5:30", off(false, 4, 30)), // merge negative and positive offsets (n=-1)
("m+24", off(false, 36, 0)), // maximum possible positive offset (m=+12)
("y-24", off(true, 36, 0)), // maximum possible negative offset (y=-12)
] {
let mut s = input;
assert_eq!(timezone_name_offset(&mut s).unwrap(), expected, "{input}");
}
for input in [
"abc+08:00", // invalid: non-existent timezone
"utc+25", // invalid: invalid offset
"utc+23 days", // invalid: ambiguous with relative time parsing
] {
let mut s = input;
assert!(
timezone_name_offset(&mut s).is_err() || !s.is_empty(),
"{input}"
);
}
}
}
+47
View File
@@ -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)
}
+144
View File
@@ -0,0 +1,144 @@
// 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, multispace0, 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)
}
/// 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, 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
}
+37
View File
@@ -0,0 +1,37 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
//! Parse a pure number string.
//!
//! From the GNU docs:
//!
//! > The precise interpretation of a pure decimal number depends on the
//! > context in the date string.
//! >
//! > If the decimal number is of the form YYYYMMDD and no other calendar
//! > date item (*note Calendar date items::) appears before it in the date
//! > string, then YYYY is read as the year, MM as the month number and DD as
//! > the day of the month, for the specified calendar date.
//! >
//! > If the decimal number is of the form HHMM and no other time of day
//! > item appears before it in the date string, then HH is read as the hour
//! > of the day and MM as the minute of the hour, for the specified time of
//! > day. MM can also be omitted.
//! >
//! > If both a calendar date and a time of day appear to the left of a
//! > number in the date string, but no relative item, then the number
//! > overrides the year.
use winnow::{ModalResult, Parser};
use super::primitive::{dec_uint_str, s};
/// Parse a pure number string and return it as an owned `String`. We return a
/// `String` here because the interpretation of the number depends on the
/// parsing context in which it appears. The interpretation is deferred to the
/// result building phase.
pub(super) fn parse(input: &mut &str) -> ModalResult<String> {
s(dec_uint_str)
.map(|s: &str| s.to_owned())
.parse_next(input)
}
+202
View File
@@ -0,0 +1,202 @@
// 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::{epoch::sec_and_nsec, ordinal::ordinal, primitive::s};
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum Relative {
Years(i32),
Months(i32),
Days(i32),
Hours(i32),
Minutes(i32),
Seconds(i64, u32),
}
impl TryFrom<Relative> for jiff::Span {
type Error = &'static str;
fn try_from(relative: Relative) -> Result<Self, Self::Error> {
match relative {
Relative::Years(years) => jiff::Span::new().try_years(years),
Relative::Months(months) => jiff::Span::new().try_months(months),
Relative::Days(days) => jiff::Span::new().try_days(days),
Relative::Hours(hours) => jiff::Span::new().try_hours(hours),
Relative::Minutes(minutes) => jiff::Span::new().try_minutes(minutes),
Relative::Seconds(seconds, nanoseconds) => jiff::Span::new()
.try_seconds(seconds)
.and_then(|span| span.try_nanoseconds(nanoseconds)),
}
.map_err(|_| "relative value is invalid")
}
}
pub(super) 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,
displacement,
))
.parse_next(input)
}
fn seconds(input: &mut &str) -> ModalResult<Relative> {
(
opt(alt((s('+').value(1), s('-').value(-1)))),
sec_and_nsec,
s(alpha1).verify(|s: &str| matches!(s, "seconds" | "second" | "sec" | "secs")),
ago,
)
.verify_map(|(sign, (sec, nsec), _, ago)| {
let sec = i64::try_from(sec).ok()?;
let sign = sign.unwrap_or(1) * if ago { -1 } else { 1 };
let (second, nanosecond) = match (sign, nsec) {
(-1, 0) => (-sec, 0),
// Truncate towards minus infinity.
(-1, _) => ((-sec).checked_sub(1)?, 1_000_000_000 - nsec),
_ => (sec, nsec),
};
Some(Relative::Seconds(second, nanosecond))
})
.parse_next(input)
}
fn displacement(input: &mut &str) -> ModalResult<Relative> {
(opt(ordinal), s(alpha1), ago)
.verify_map(|(n, unit, ago): (Option<i32>, &str, bool)| {
let multiplier = n.unwrap_or(1) * if ago { -1 } else { 1 };
Some(match unit.strip_suffix('s').unwrap_or(unit) {
"year" => Relative::Years(multiplier),
"month" => Relative::Months(multiplier),
"fortnight" => Relative::Days(multiplier.checked_mul(14)?),
"week" => Relative::Days(multiplier.checked_mul(7)?),
"day" => Relative::Days(multiplier),
"hour" => Relative::Hours(multiplier),
"minute" | "min" => Relative::Minutes(multiplier),
"second" | "sec" => Relative::Seconds(multiplier as i64, 0),
_ => return None,
})
})
.parse_next(input)
}
fn ago(input: &mut &str) -> ModalResult<bool> {
opt(s("ago")).map(|o| o.is_some()).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, 500_000_000)),
("-3.5 seconds", Relative::Seconds(-4, 500_000_000)),
("+3.5 seconds", Relative::Seconds(3, 500_000_000)),
("3.5 seconds ago", Relative::Seconds(-4, 500_000_000)),
("-3.5 seconds ago", Relative::Seconds(3, 500_000_000)),
// 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}")
}
}
}
+444
View File
@@ -0,0 +1,444 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
// spell-checker:ignore shhmm colonless
//! Parse a time item (without a date).
//!
//! The GNU docs state:
//!
//! > More generally, the time of day may be given as hour:minute:second,
//! > where hour is a number between 0 and 23, minute is a number between 0 and
//! > 59, and second is a number between 0 and 59 possibly followed by . or
//! > , and a fraction containing one or more digits. Alternatively,
//! > :second can be omitted, in which case it is taken to be zero. On the
//! > rare hosts that support leap seconds, second may be 60.
//! >
//! > If the time is followed by am or pm (or a.m. or p.m.), hour is
//! > restricted to run from 1 to 12, and :minute may be omitted (taken to be
//! > zero). am indicates the first half of the day, pm indicates the
//! > second half of the day. In this notation, 12 is the predecessor of 1:
//! > midnight is 12am while noon is 12pm. (This is the zero-oriented
//! > interpretation of 12am and 12pm, as opposed to the old tradition
//! > derived from Latin which uses 12m for noon and 12pm for midnight.)
//! >
//! > The time may alternatively be followed by a time zone correction,
//! > expressed as shhmm, where s is + or -, hh is a number of zone hours
//! > and mm is a number of zone minutes. The zone minutes term, mm, may be
//! > omitted, in which case the one- or two-digit correction is interpreted as
//! > a number of hours. You can also separate hh from mm with a colon. When a
//! > time zone correction is given this way, it forces interpretation of the
//! > time relative to Coordinated Universal Time (UTC), overriding any
//! > previous specification for the time zone or the local time zone. For
//! > example, +0530 and +05:30 both stand for the time zone 5.5 hours
//! > ahead of UTC (e.g., India). This is the best way to specify a time zone
//! > correction by fractional parts of an hour. The maximum zone correction is
//! > 24 hours.
//! >
//! > Either am/pm or a time zone correction may be specified, but not both.
use winnow::{
combinator::{alt, opt, preceded},
error::ErrMode,
ModalResult, Parser,
};
use super::{
epoch::sec_and_nsec,
offset::{timezone_offset, Offset},
primitive::{colon, ctx_err, dec_uint, s},
};
#[derive(PartialEq, Clone, Debug, Default)]
pub(crate) struct Time {
pub(crate) hour: u8,
pub(crate) minute: u8,
pub(crate) second: u8,
pub(crate) nanosecond: u32,
pub(super) offset: Option<Offset>,
}
impl TryFrom<Time> for jiff::civil::Time {
type Error = &'static str;
fn try_from(time: Time) -> Result<Self, Self::Error> {
jiff::civil::Time::new(
time.hour as i8,
time.minute as i8,
time.second as i8,
time.nanosecond as i32,
)
.map_err(|_| "time is not valid")
}
}
#[derive(Clone)]
enum Meridiem {
Am,
Pm,
}
pub(crate) fn parse(input: &mut &str) -> ModalResult<Time> {
alt((am_pm_time, iso)).parse_next(input)
}
/// Parse an ISO 8601 time string
///
/// Also used by the [`combined`](super::combined) module
pub(super) fn iso(input: &mut &str) -> ModalResult<Time> {
alt((
(hour24, timezone_offset).map(|(hour, offset)| Time {
hour,
minute: 0,
second: 0,
nanosecond: 0,
offset: Some(offset),
}),
(
hour24,
colon,
minute,
opt(preceded(colon, second)),
opt(timezone_offset),
)
.map(|(hour, _, minute, sec_nsec, offset)| Time {
hour,
minute,
second: sec_nsec.map_or(0, |(s, _)| s),
nanosecond: sec_nsec.map_or(0, |(_, ns)| ns),
offset,
}),
))
.parse_next(input)
}
/// Parse a time ending with AM or PM
///
/// The hours are restricted to 12 or lower in this format
fn am_pm_time(input: &mut &str) -> ModalResult<Time> {
let (h, m, sec_nsec, meridiem) = (
hour12,
opt(preceded(colon, minute)),
opt(preceded(colon, second)),
alt((
s("am").value(Meridiem::Am),
s("a.m.").value(Meridiem::Am),
s("pm").value(Meridiem::Pm),
s("p.m.").value(Meridiem::Pm),
)),
)
.parse_next(input)?;
if h == 0 {
return Err(ErrMode::Cut(ctx_err(
"hour must be greater than 0 when meridiem is specified",
)));
}
let mut h = h % 12;
if let Meridiem::Pm = meridiem {
h += 12;
}
Ok(Time {
hour: h,
minute: m.unwrap_or(0),
second: sec_nsec.map_or(0, |(s, _)| s),
nanosecond: sec_nsec.map_or(0, |(_, ns)| ns),
offset: None,
})
}
/// Parse a number of hours in `0..24`.
pub(super) fn hour24(input: &mut &str) -> ModalResult<u8> {
s(dec_uint).verify(|x| *x < 24).parse_next(input)
}
/// Parse a number of hours in `0..=12`.
fn hour12(input: &mut &str) -> ModalResult<u8> {
s(dec_uint).verify(|x| *x <= 12).parse_next(input)
}
/// Parse a number of minutes in `0..60`.
pub(super) fn minute(input: &mut &str) -> ModalResult<u8> {
s(dec_uint).verify(|x| *x < 60).parse_next(input)
}
/// Parse a number of seconds in `0..60` and an optional number of nanoseconds
/// (default to 0 if not set).
fn second(input: &mut &str) -> ModalResult<(u8, u32)> {
s(sec_and_nsec)
.verify_map(|(s, ns)| if s < 60 { Some((s as u8, ns)) } else { None })
.parse_next(input)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn simple() {
let reference = Time {
hour: 20,
minute: 2,
second: 0,
nanosecond: 0,
offset: None,
};
for mut s in [
"20:02:00.000000",
"20:02:00",
"20:02+:00",
"20:02-:00",
"20----:02--(these hyphens are ignored)--:00",
"20++++:02++(these plusses are ignored)++:00",
"20: (A comment!) 02 (Another comment!) :00",
"20:02 (A nested (comment!)) :00",
"20:02 (So (many (nested) comments!!!!)) :00",
"20 : 02 : 00.000000",
"20:02",
"20 : 02",
"8:02pm",
"8: 02 pm",
"8:02p.m.",
"8: 02 p.m.",
] {
let old_s = s.to_owned();
assert_eq!(
parse(&mut s).ok(),
Some(reference.clone()),
"Format string: {old_s}"
);
}
}
#[test]
fn invalid() {
assert!(parse(&mut "00:00am").is_err());
assert!(parse(&mut "00:00:00am").is_err());
}
#[test]
fn hours_only() {
let reference = Time {
hour: 11,
minute: 0,
second: 0,
nanosecond: 0,
offset: None,
};
for mut s in [
"11am",
"11 am",
"11 - am",
"11 + am",
"11 a.m.",
"11 : 00",
"11:00:00",
] {
let old_s = s.to_owned();
assert_eq!(
parse(&mut s).ok(),
Some(reference.clone()),
"Format string: {old_s}"
);
}
}
#[test]
fn nanoseconds() {
let reference = Time {
hour: 11,
minute: 0,
second: 0,
nanosecond: 123450000,
offset: None,
};
for mut s in ["11:00:00.12345", "11:00:00.12345am"] {
let old_s = s.to_owned();
assert_eq!(
parse(&mut s).ok(),
Some(reference.clone()),
"Format string: {old_s}"
);
}
let reference = Time {
hour: 11,
minute: 0,
second: 0,
nanosecond: 123456789,
offset: None,
};
for mut s in ["11:00:00.123456789", "11:00:00.1234567890123"] {
let old_s = s.to_owned();
assert_eq!(
parse(&mut s).ok(),
Some(reference.clone()),
"Format string: {old_s}"
);
}
}
#[test]
fn noon() {
let reference = Time {
hour: 12,
minute: 0,
second: 0,
nanosecond: 0,
offset: None,
};
for mut s in [
"12:00",
"12pm",
"12 pm",
"12 (A comment!) pm",
"12 pm",
"12 p.m.",
] {
let old_s = s.to_owned();
assert_eq!(
parse(&mut s).ok(),
Some(reference.clone()),
"Format string: {old_s}"
);
}
}
#[test]
fn midnight() {
let reference = Time {
hour: 0,
minute: 0,
second: 0,
nanosecond: 0,
offset: None,
};
for mut s in ["00:00", "12am"] {
let old_s = s.to_owned();
assert_eq!(
parse(&mut s).ok(),
Some(reference.clone()),
"Format string: {old_s}"
);
}
}
#[test]
fn offset_hours() {
let reference = Time {
hour: 1,
minute: 23,
second: 0,
nanosecond: 0,
offset: Some((false, 5, 0).try_into().unwrap()),
};
for mut s in [
"1:23+5",
"1:23 + 5",
"1:23+05",
"1:23 + 5 : 00",
"1:23+05:00",
"1:23+05:0",
] {
let old_s = s.to_owned();
assert_eq!(
parse(&mut s).ok(),
Some(reference.clone()),
"Format string: {old_s}"
);
}
}
#[test]
fn offset_hours_and_minutes() {
let reference = Time {
hour: 3,
minute: 45,
second: 0,
nanosecond: 0,
offset: Some((false, 5, 35).try_into().unwrap()),
};
for mut s in [
"3:45+535",
"3:45-+535",
"03:45+535",
"3 : 45 + 535",
"3:45+0535",
"3:45+5:35",
"3:45+05:35",
"3:45 + 05 : 35",
] {
let old_s = s.to_owned();
assert_eq!(
parse(&mut s).ok(),
Some(reference.clone()),
"Format string: {old_s}"
);
}
}
#[test]
fn offset_minutes() {
let reference = Time {
hour: 3,
minute: 45,
second: 0,
nanosecond: 0,
offset: Some((false, 0, 35).try_into().unwrap()),
};
for mut s in [
"3:45+035",
"03:45+035",
"3 : 45 + 035",
"3:45+0035",
"3:45+0:35",
"3:45+00:35",
"3:45 + 00 : 35",
] {
let old_s = s.to_owned();
assert_eq!(
parse(&mut s).ok(),
Some(reference.clone()),
"Format string: {old_s}"
);
}
}
#[test]
fn offset_negative() {
let reference = Time {
hour: 3,
minute: 45,
second: 0,
nanosecond: 0,
offset: Some((true, 5, 35).try_into().unwrap()),
};
for mut s in [
"3:45-535",
"03:45-535",
"3 : 45 - 535",
"3:45-0535",
"3:45-5:35",
"3:45-05:35",
"3:45 - 05 : 35",
] {
let old_s = s.to_owned();
assert_eq!(
parse(&mut s).ok(),
Some(reference.clone()),
"Format string: {old_s}"
);
}
}
}
+340
View File
@@ -0,0 +1,340 @@
// For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code.
//! Parse a timezone item. The timezone item must be at the beginning of the
//! input string and in the `TZ="..."` format.
//!
//! From the GNU docs:
//!
//! > Normally, dates are interpreted using the rules of the current time zone,
//! > which in turn are specified by the TZ environment variable, or by a
//! > system default if TZ is not set. To specify a different set of default
//! > time zone rules that apply just to one date, start the date with a string
//! > of the form TZ="RULE". The two quote characters (") must be present in
//! > the date, and any quotes or backslashes within RULE must be escaped by a
//! > backslash.
use jiff::tz::{Offset, TimeZone};
use winnow::{
combinator::{alt, delimited, opt, preceded, repeat},
stream::AsChar,
token::{one_of, take_while},
ModalResult, Parser,
};
use super::primitive::{dec_uint, plus_or_minus};
pub(super) fn parse(input: &mut &str) -> ModalResult<TimeZone> {
delimited("TZ=\"", preceded(opt(':'), alt((posix, iana))), '"').parse_next(input)
}
/// Parse a posix (proleptic) timezone string (e.g., "UTC7", "JST-9").
///
/// TODO: This implementation is incomplete. It currently only parses the
/// `STDOFFSET` part of the format.
///
/// From the GNU docs:
///
/// > The proleptic format is:
/// >
/// > STDOFFSET[DST[OFFSET][,START[/TIME],END[/TIME]]]
/// >
/// > The STD string specifies the time zone abbreviation, which must be at
/// > least three bytes long. ...
/// >
/// > The OFFSET specifies the time value you must add to the local time to
/// > get a UTC value. It has syntax like:
/// >
/// > [+|-]HH[:MM[:SS]]
/// >
/// > This is positive if the local time zone is west of the Prime Meridian
/// > and negative if it is east; this is opposite from the usual convention
/// > that positive time zone offsets are east of the Prime Meridian. The
/// > hour HH must be between 0 and 24 and may be a single digit, and the
/// > minutes MM and seconds SS, if present, must be between 0 and 59.
fn posix(input: &mut &str) -> ModalResult<TimeZone> {
(take_while(3.., AsChar::is_alpha), posix_offset)
.verify_map(|(_, offset)| Offset::from_seconds(offset).ok().map(|o| o.to_time_zone()))
.parse_next(input)
}
/// Parse an IANA (geographical) timezone string (e.g., "Europe/Paris"). If the
/// string is not a valid IANA timezone name, the UTC timezone is returned.
///
/// Compatibility notes:
///
/// - The implementation uses `jiff::tz::TimeZone::get()` to resolve time zones.
/// Only canonical/aliased IANA names are accepted. Absolute file paths are
/// not supported.
/// - GNU `date` resolves time zones from the tzdata files under
/// `/usr/share/zoneinfo` (respecting `TZDIR`) and also accepts an absolute
/// path when the string starts with `/`.
///
/// From the GNU docs:
///
/// > If the format's CHARACTERS begin with / it is an absolute file
/// > name; otherwise the library looks for the file
/// > /usr/share/zoneinfo/CHARACTERS. The zoneinfo directory contains
/// > data files describing time zone rulesets in many different parts of the
/// > world. The names represent major cities, with subdirectories for
/// > geographical areas; for example, America/New_York, Europe/London,
/// > Asia/Tokyo. These data files are installed by the system
/// > administrator, who also sets /etc/localtime to point to the data file
/// > for the local time zone ruleset.
fn iana(input: &mut &str) -> ModalResult<TimeZone> {
repeat(
0..,
alt((
preceded('\\', one_of(['\\', '"'])).map(|c: char| c.to_string()),
take_while(1, |c| c != '"' && c != '\\').map(str::to_string),
)),
)
.map(|parts: Vec<String>| parts.concat())
.map(|s| TimeZone::get(&s).unwrap_or(TimeZone::UTC))
.parse_next(input)
}
fn posix_offset(input: &mut &str) -> ModalResult<i32> {
let uint = dec_uint::<u32, _>;
(
opt(plus_or_minus),
alt((
(uint, preceded(':', uint), preceded(':', uint)).map(|(h, m, s)| (h, m, s)),
(uint, preceded(':', uint)).map(|(h, m)| (h, m, 0)),
uint.map(|h| (h, 0, 0)),
)),
)
.map(|(sign, (h, m, s))| {
// The sign is opposite from the usual convention:
// - Positive offsets are west of UTC.
// - Negative offsets are east of UTC.
let sign = if sign == Some('-') { 1 } else { -1 };
// - If hour is greater than 24, clamp it to 24.
// - If minute is greater than 59, clamp it to 59.
// - If second is greater than 59, clamp it to 59.
let h = h.min(24) as i32;
let m = m.min(59) as i32;
let s = s.min(59) as i32;
sign * (h * 3600 + m * 60 + s)
})
.parse_next(input)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tz_rule() {
// empty string
for (input, expected) in [
(r#"TZ="""#, "UTC"),
(r#"TZ=":""#, "UTC"),
(r#"TZ=" ""#, "UTC"),
(r#"TZ=": ""#, "UTC"),
] {
let mut s = input;
assert_eq!(
parse(&mut s).unwrap().iana_name(),
Some(expected),
"{input}"
);
}
// iana
for (input, expected) in [
(r#"TZ="Etc/Zulu""#, "Etc/Zulu"),
(r#"TZ=":Etc/Zulu""#, "Etc/Zulu"),
(r#"TZ="America/New_York""#, "America/New_York"),
(r#"TZ=":America/New_York""#, "America/New_York"),
(r#"TZ="Asia/Tokyo""#, "Asia/Tokyo"),
(r#"TZ=":Asia/Tokyo""#, "Asia/Tokyo"),
(r#"TZ="Unknown/Timezone""#, "UTC"),
(r#"TZ=":Unknown/Timezone""#, "UTC"),
] {
let mut s = input;
assert_eq!(
parse(&mut s).unwrap().iana_name(),
Some(expected),
"{input}"
);
}
// posix
for (input, expected) in [
(r#"TZ="UTC0""#, 0),
(r#"TZ=":UTC0""#, 0),
(r#"TZ="UTC+5""#, -5 * 3600),
(r#"TZ=":UTC+5""#, -5 * 3600),
(r#"TZ="UTC-5""#, 5 * 3600),
(r#"TZ=":UTC-5""#, 5 * 3600),
(r#"TZ="UTC+5:20""#, -(5 * 3600 + 20 * 60)),
(r#"TZ=":UTC+5:20""#, -(5 * 3600 + 20 * 60)),
(r#"TZ="UTC-5:20""#, 5 * 3600 + 20 * 60),
(r#"TZ=":UTC-5:20""#, 5 * 3600 + 20 * 60),
(r#"TZ="UTC+5:20:15""#, -(5 * 3600 + 20 * 60 + 15)),
(r#"TZ=":UTC+5:20:15""#, -(5 * 3600 + 20 * 60 + 15)),
(r#"TZ="UTC-5:20:15""#, 5 * 3600 + 20 * 60 + 15),
(r#"TZ=":UTC-5:20:15""#, 5 * 3600 + 20 * 60 + 15),
] {
let mut s = input;
assert_eq!(
parse(&mut s).unwrap().to_fixed_offset().unwrap().seconds(),
expected,
"{input}"
);
}
// invalid
for input in [
r#"UTC"#, // missing "TZ="
r#"tz="UTC""#, // lowercase "tz"
r#"TZ=UTC"#, // missing quotes
] {
let mut s = input;
assert!(parse(&mut s).is_err(), "{input}");
}
}
#[test]
fn parse_iana() {
for (input, expected) in [
("UTC", "UTC"), // utc timezone
("Etc/Zulu", "Etc/Zulu"), // etc timezone
("America/New_York", "America/New_York"), // named timezone
("Asia/Tokyo", "Asia/Tokyo"), // named timezone
("Unknown/Timezone", "UTC"), // unknown timezone
] {
let mut s = input;
assert_eq!(iana(&mut s).unwrap().iana_name(), Some(expected), "{input}");
}
}
#[test]
fn parse_posix() {
let to_seconds = |input: &str| {
let mut s = input;
posix(&mut s).unwrap().to_fixed_offset().unwrap().seconds()
};
// hour
for (input, expected) in [
("UTC0", 0),
("UTC+0", 0),
("UTC-0", 0),
("UTC000", 0),
("UTC+5", -5 * 3600),
("UTC-5", 5 * 3600),
("ABC0", 0),
("ABC+5", -5 * 3600),
("ABC-5", 5 * 3600),
] {
assert_eq!(to_seconds(input), expected, "{input}");
}
// hour:minute
for (input, expected) in [
("UTC0:0", 0),
("UTC+0:0", 0),
("UTC-0:0", 0),
("UTC00:00", 0),
("UTC+5:20", -(5 * 3600 + 20 * 60)),
("UTC-5:20", 5 * 3600 + 20 * 60),
("ABC0:0", 0),
("ABC+5:20", -(5 * 3600 + 20 * 60)),
("ABC-5:20", 5 * 3600 + 20 * 60),
] {
assert_eq!(to_seconds(input), expected, "{input}");
}
// hour:minute:second
for (input, expected) in [
("UTC0:0:0", 0),
("UTC+0:0:0", 0),
("UTC-0:0:0", 0),
("UTC00:00:00", 0),
("UTC+5:20:15", -(5 * 3600 + 20 * 60 + 15)),
("UTC-5:20:15", 5 * 3600 + 20 * 60 + 15),
("ABC0:0:0", 0),
("ABC+5:20:15", -(5 * 3600 + 20 * 60 + 15)),
("ABC-5:20:15", 5 * 3600 + 20 * 60 + 15),
] {
assert_eq!(to_seconds(input), expected, "{input}");
}
// invalid
for input in [
"AB", // too short
"A1C", // not just letters
] {
let mut s = input;
assert!(posix(&mut s).is_err(), "{input}");
}
}
#[test]
fn parse_posix_offset() {
// hour
for (input, expected) in [
("0", 0), // zero hour
("00", 0), // zero hour, two digits
("000", 0), // zero hour, three digits
("+0", 0), // zero hour, explicit plus
("-0", 0), // zero hour, explicit minus
("5", -5 * 3600), // positive hour
("-5", 5 * 3600), // negative hour
("005", -5 * 3600), // positive hour with leading zeros
("-05", 5 * 3600), // negative hour with leading zeros
("25", -24 * 3600), // hour > 24, clamps to 24
("-25", 24 * 3600), // hour > 24, clamps to 24
] {
let mut s = input;
assert_eq!(posix_offset(&mut s).unwrap(), expected, "{input}");
}
// hour:minute
for (input, expected) in [
("0:0", 0), // zero hour and minute
("00:00", 0), // zero hour and minute, two digits
("000:000", 0), // zero hour and minute, three digits
("+0:0", 0), // zero hour and minute, explicit plus
("-0:0", 0), // zero hour and minute, explicit minus
("5:20", -(5 * 3600 + 20 * 60)), // positive hour and minute
("-5:20", 5 * 3600 + 20 * 60), // negative hour and minute
("005:020", -(5 * 3600 + 20 * 60)), // positive hour and minute with leading zeros
("-05:20", 5 * 3600 + 20 * 60), // negative hour and minute with leading zeros
("25:20", -(24 * 3600 + 20 * 60)), // hour > 24, clamps to 24
("-25:20", 24 * 3600 + 20 * 60), // hour > 24, clamps to 24
("5:60", -(5 * 3600 + 59 * 60)), // minute > 59, clamps to 59
("-5:60", 5 * 3600 + 59 * 60), // minute > 59, clamps to 59
] {
let mut s = input;
assert_eq!(posix_offset(&mut s).unwrap(), expected, "{input}");
}
// hour:minute:second
for (input, expected) in [
("0:0:0", 0), // zero hour, minute, and second
("00:00:00", 0), // zero hour, minute, and second, two digits
("000:000:000", 0), // zero hour, minute, and second, three digits
("+0:0:0", 0), // zero hour, minute, and second, explicit plus
("-0:0:0", 0), // zero hour, minute, and second, explicit minus
("5:20:15", -(5 * 3600 + 20 * 60 + 15)), // positive hour, minute, and second
("-5:20:15", 5 * 3600 + 20 * 60 + 15), // negative hour, minute, and second
("005:020:015", -(5 * 3600 + 20 * 60 + 15)), // positive hour, minute, and second with leading zeros
("-05:20:15", 5 * 3600 + 20 * 60 + 15), // negative hour, minute, and second with leading zeros
("25:20:15", -(24 * 3600 + 20 * 60 + 15)), // hour > 24, clamps to 24
("-25:20:15", 24 * 3600 + 20 * 60 + 15), // hour > 24, clamps to 24
("5:60:15", -(5 * 3600 + 59 * 60 + 15)), // minute > 59, clamps to 59
("-5:60:15", 5 * 3600 + 59 * 60 + 15), // minute > 59, clamps to 59
("5:20:60", -(5 * 3600 + 20 * 60 + 59)), // second > 59, clamps to 59
("-5:20:60", 5 * 3600 + 20 * 60 + 59), // second > 59, clamps to 59
] {
let mut s = input;
assert_eq!(posix_offset(&mut s).unwrap(), expected, "{input}");
}
}
}
+156
View File
@@ -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(crate) struct Weekday {
pub(crate) offset: i32,
pub(crate) day: Day,
}
impl From<Day> for jiff::civil::Weekday {
fn from(value: Day) -> Self {
match value {
Day::Monday => jiff::civil::Weekday::Monday,
Day::Tuesday => jiff::civil::Weekday::Tuesday,
Day::Wednesday => jiff::civil::Weekday::Wednesday,
Day::Thursday => jiff::civil::Weekday::Thursday,
Day::Friday => jiff::civil::Weekday::Friday,
Day::Saturday => jiff::civil::Weekday::Saturday,
Day::Sunday => jiff::civil::Weekday::Sunday,
}
}
}
/// 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