Vendor dependencies

This commit is contained in:
2026-08-01 16:11:49 +03:00
parent 7f139a0241
commit 6b5e7f0f8b
29706 changed files with 9575646 additions and 0 deletions
@@ -0,0 +1 @@
{"$comment":"This file only protects against accidental modifications. It is not a security mechanism and does not protect against malicious changes.","files":{".cargo_vcs_info.json":"0d5cb43f0664558923d7b8e9684f11510418d0601b5c11fe8bf2cda0b613fbe9",".github/workflows/ci.yaml":"ce632b4dbe36f33e59e31452ce65dcedd3d209ce325b77df9d426ddbe4b97ef9","Cargo.lock":"d50617b4059841e0cb117191a5609d70d7054cecea28d3447058c40b7ce99752","Cargo.toml":"af680fdebddcdb1091ca45265884f44c91b6ab6484a778ddcb845b00f165ec00","Cargo.toml.orig":"0e30e67bac0de2fc9289e6462fb09f77de9eb1ca82363cd2680e25271be198c0","LICENSE":"18be09656b3d0f27971acdcf2d6b398bdc4beda80f64e7e05658a99ed5f4b15e","README.md":"f39310864e8c4aaaa21ba63d51d7743475672a6d3028f00e72932a99e0297138","examples/from_str.rs":"b28194e693254310510f0c66596d5ba19ea709384e3c2a9861d4791ace5626dd","examples/run.rs":"c3a130b8187c12d0c927c85c0fe70c13226b8d64320466521722576f283fbc02","src/lib.rs":"5089c48ca439468710228a459aec4b46d09da41089ee43cdf4f889bb1b324a5a","src/str_to_cron/action/clock_time.rs":"6d2310a07e104af22081925815a437b69e5de37378c3f5799416e0729fcfa0ac","src/str_to_cron/action/day.rs":"b20e83ae7c02f1803a63b7a6116d60d87aad250d607cf17b7325f286822fc229","src/str_to_cron/action/frequency_only.rs":"e9f60436b11a5fc9faafe1b626b1a9f3fb3c703d0a6dcf6275adf0136c77654e","src/str_to_cron/action/frequency_with.rs":"a418c2b87d29a89745e13f42aa71ab94c47cb8b1622dbad66f0d68223533b76e","src/str_to_cron/action/hour.rs":"dd867309473b5cc07f733c3a3703a895217a729174580e7b5963e5bfaf57170c","src/str_to_cron/action/minute.rs":"2c829ccd3ca72d0181b69b2856dfc4751a248060873c81350b9cf9287837445b","src/str_to_cron/action/mod.rs":"e3c35c474cae54c5124c903581703f7f0f244aebd072e2bdb73b7ecb4011ed83","src/str_to_cron/action/month.rs":"ea027ce2537c4e70405ed159af16d65586c41452b16d0b2e8d4fe6dd79031a5d","src/str_to_cron/action/range_end.rs":"39a8b4e24bd71f7d5aae9dc742c2f0e9e0d4536cabd9b43df244d044ac0f52a6","src/str_to_cron/action/range_start.rs":"2a7a012d7b2afbce94392f536cd2bb30b4cd5f31d0c1f1363cfc9267620379a9","src/str_to_cron/action/seconds.rs":"c5954c5d6418f84802c0bd98d1cfb47fa0274e7438d364ac4c87684dffc88d3b","src/str_to_cron/action/year.rs":"b152efc35d734255525802fab96c12c3e22c5c63b5ef5242a8e375bb5b832b82","src/str_to_cron/cron.rs":"0a02201fdd0a6fb1817e189da9fc02715c63f919501c6f98b87bcc1a8f84b68c","src/str_to_cron/errors.rs":"812c7dad9546e338fe7f901cc79e20014daab59a85ea133dc26630e74c5b0e95","src/str_to_cron/mod.rs":"69dd3fa3912945315a099aea85f46b10d87134a5c0244228b9206e58249aaa8a","src/str_to_cron/stack.rs":"20124f4355f08156dcb54775c16aa49d0c9c07fdf0c89c42611eea6b5c8dde28","src/str_to_cron/tokens.rs":"433ba47da586b26fa88c9cdb606648aef781e34f75d95aedd644de9072675a3d","tests/test.rs":"00e017bf9d02069f60ee9f305b1e3423691cec0ee51088b86d3d244546f70091"},"package":"3c3d16f6dc9dc43a9a2fd5bce09b6cf8df250dcf77cffdaa66be21c527e2d05c"}
@@ -0,0 +1,6 @@
{
"git": {
"sha1": "2f6fb824ed3185063e3886badf9b5a0cad3ad1cd"
},
"path_in_vcs": ""
}
@@ -0,0 +1,66 @@
name: CI
on:
push:
branches:
- main
pull_request:
env:
RUST_TOOLCHAIN: stable
TOOLCHAIN_PROFILE: minimal
jobs:
rustfmt:
name: Check Style
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout the code
uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
components: rustfmt
- name: Run cargo fmt
run: cargo fmt --all -- --check
clippy:
name: Run Clippy
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout the code
uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
- name: Run cargo clippy
run: cargo clippy --all-features -- -D warnings -W clippy::pedantic -W clippy::nursery -W rust-2018-idioms
test:
name: Run Tests
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- name: Checkout the code
uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUST_TOOLCHAIN }}
- name: Setup Rust cache
uses: Swatinem/rust-cache@v2
- name: Run cargo test
run: cargo test --all-features --all
+332
View File
@@ -0,0 +1,332 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "aho-corasick"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
dependencies = [
"memchr",
]
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "english-to-cron"
version = "0.1.7"
dependencies = [
"regex",
"rstest",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "futures"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
name = "futures-core"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-executor"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-macro"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-sink"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
[[package]]
name = "futures-task"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-timer"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
[[package]]
name = "futures-util"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
name = "glob"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
[[package]]
name = "hashbrown"
version = "0.15.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3"
[[package]]
name = "indexmap"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "pin-project-lite"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "proc-macro-crate"
version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35"
dependencies = [
"toml_edit",
]
[[package]]
name = "proc-macro2"
version = "1.0.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
[[package]]
name = "relative-path"
version = "1.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
[[package]]
name = "rstest"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b423f0e62bdd61734b67cd21ff50871dfaeb9cc74f869dcd6af974fbcb19936"
dependencies = [
"futures",
"futures-timer",
"rstest_macros",
"rustc_version",
]
[[package]]
name = "rstest_macros"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5e1711e7d14f74b12a58411c542185ef7fb7f2e7f8ee6e2940a883628522b42"
dependencies = [
"cfg-if",
"glob",
"proc-macro-crate",
"proc-macro2",
"quote",
"regex",
"relative-path",
"rustc_version",
"syn",
"unicode-ident",
]
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]]
name = "semver"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0"
[[package]]
name = "slab"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
dependencies = [
"autocfg",
]
[[package]]
name = "syn"
version = "2.0.101"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "toml_datetime"
version = "0.6.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3"
[[package]]
name = "toml_edit"
version = "0.22.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e"
dependencies = [
"indexmap",
"toml_datetime",
"winnow",
]
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "winnow"
version = "0.7.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c06928c8748d81b05c9be96aad92e1b6ff01833332f281e8cfca3be4b35fc9ec"
dependencies = [
"memchr",
]
+52
View File
@@ -0,0 +1,52 @@
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
#
# When uploading crates to the registry Cargo will automatically
# "normalize" Cargo.toml files for maximal compatibility
# with all versions of Cargo and also rewrite `path` dependencies
# to registry (e.g., crates.io) dependencies.
#
# If you are reading this file be aware that the original Cargo.toml
# will likely look very different (and much more reasonable).
# See Cargo.toml.orig for the original contents.
[package]
edition = "2021"
name = "english-to-cron"
version = "0.1.7"
authors = ["Elad Kaplan <kaplan.elad@gmail.com>"]
build = false
autolib = false
autobins = false
autoexamples = false
autotests = false
autobenches = false
description = "converts natural language into cron expressions"
homepage = "https://docs.rs/english-to-cron"
documentation = "https://docs.rs/english-to-cron"
readme = "README.md"
license = "Apache-2.0"
repository = "https://github.com/kaplanelad/english-to-cron"
[lib]
name = "english_to_cron"
path = "src/lib.rs"
[[example]]
name = "from_str"
path = "examples/from_str.rs"
[[example]]
name = "run"
path = "examples/run.rs"
[[test]]
name = "test"
path = "tests/test.rs"
[dependencies.regex]
version = "1.10.6"
features = ["unicode-case"]
default-features = false
[dev-dependencies.rstest]
version = "0.22.0"
+18
View File
@@ -0,0 +1,18 @@
[package]
name = "english-to-cron"
version = "0.1.7"
edition = "2021"
description = "converts natural language into cron expressions"
homepage = "https://docs.rs/english-to-cron"
documentation = "https://docs.rs/english-to-cron"
authors = ["Elad Kaplan <kaplan.elad@gmail.com>"]
repository = "https://github.com/kaplanelad/english-to-cron"
license = "Apache-2.0"
[dependencies]
regex = { version = "1.10.6", default-features = false, features = [
"unicode-case",
] }
[dev-dependencies]
rstest = "0.22.0"
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [2024] Elad Kaplan
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+52
View File
@@ -0,0 +1,52 @@
# English to CronJob Syntax Converter
[![Crates.io](https://img.shields.io/crates/v/english-to-cron.svg)](https://crates.io/crates/english-to-cron)
[![Docs.rs](https://docs.rs/english-to-cron/badge.svg)](https://docs.rs/english-to-cron)
This project is inspired by the library natural-cron, which converts natural language into cron expressions. `english-to-cron` brings similar functionality to the Rust ecosystem, allowing developers to easily schedule cron jobs using English text.
## Features
- Converts various English text descriptions into cron job syntax.
- Supports complex patterns including specific days, time ranges, and more.
- Handles multiple time formats including AM/PM and 24-hour notation.
## Installation
Add the following line to your `Cargo.toml` under `[dependencies]`:
```toml
english_to_cron = "0.1"
```
## Usage
Simply provide an English phrase describing the schedule, and the library will return the corresponding cron job syntax.
```rust
use english_to_cron::str_cron_syntax;
fn main() {
assert_eq!(str_cron_syntax("every 15 seconds").unwrap(), "0/15 * * * * ? *");
assert_eq!(str_cron_syntax("every minute").unwrap(), "0 * * * * ? *");
assert_eq!(str_cron_syntax("every day at 4:00 pm").unwrap(), "0 0 16 */1 * ? *");
assert_eq!(str_cron_syntax("at 10:00 am").unwrap(), "0 0 10 * * ? *");
assert_eq!(str_cron_syntax("Run at midnight on the 1st and 15th of the month").unwrap(), "0 0 0 1,15 * ? *");
assert_eq!(str_cron_syntax("on Sunday at 12:00").unwrap(), "0 0 12 ? * SUN *");
}
```
## Full List of Supported English Patterns
| English Phrase | CronJob Syntax |
|------------------------------------------------------------------ |---------------------------- |
| every 15 seconds | 0/15 * * * * ? * |
| run every minute | 0 * * * * ? * |
| fire every day at 4:00 pm | 0 0 16 */1 * ? * |
| at 10:00 am | 0 0 10 * * ? * |
| run at midnight on the 1st and 15th of the month | 0 0 0 1,15 * ? * |
| On Sunday at 12:00 | 0 0 12 ? * SUN * |
| 7pm every Thursday | 0 0 19 ? * THU * |
| midnight on Tuesdays | 0 0 ? * TUE * |
## Contributing
Contributions are welcome! Feel free to open issues or submit pull requests to help improve the library.
@@ -0,0 +1,19 @@
use std::str::FromStr;
fn main() {
let texts = vec![
"every 15 seconds",
"every minute",
"every day at 4:00 pm",
"at 10:00 am",
"Run at midnight on the 1st and 15th of the month",
"on Sunday at 12:00",
];
for text in texts {
match english_to_cron::Cron::from_str(text) {
Ok(res) => println!("{text}: {res}"),
Err(e) => eprintln!("Error parsing '{text}': {e}"),
}
}
}
+15
View File
@@ -0,0 +1,15 @@
fn main() {
let texts = vec![
"every 15 seconds",
"every minute",
"every day at 4:00 pm",
"at 10:00 am",
"Run at midnight on the 1st and 15th of the month",
"on Sunday at 12:00",
];
for text in texts {
let res = english_to_cron::str_cron_syntax(text);
println!("{text}: {res:#?}");
}
}
+43
View File
@@ -0,0 +1,43 @@
#[allow(clippy::needless_doctest_main)]
#[allow(clippy::doc_markdown)]
#[doc = include_str!("../README.md")]
mod str_to_cron;
pub use str_to_cron::{Cron, Error, Result};
/// Converts an English description of a schedule into cronjob syntax.
///
/// This function takes a natural language description of a recurring schedule
/// (e.g., "Run every 15 seconds", "Run at 6:00 pm every Monday through Friday")
/// and converts it into a valid cron expression that can be used to schedule jobs.
///
/// # Examples
///
/// Basic usage:
///
/// ```rust
/// use english_to_cron::str_cron_syntax;
///
/// assert_eq!(str_cron_syntax("every 15 seconds").unwrap(), "0/15 * * * * ? *");
/// assert_eq!(str_cron_syntax("every minute").unwrap(), "0 * * * * ? *");
/// assert_eq!(str_cron_syntax("every day at 4:00 pm").unwrap(), "0 0 16 */1 * ? *");
/// assert_eq!(str_cron_syntax("at 10:00 am").unwrap(), "0 0 10 * * ? *");
/// assert_eq!(str_cron_syntax("Run at midnight on the 1st and 15th of the month").unwrap(), "0 0 0 1,15 * ? *");
/// assert_eq!(str_cron_syntax("on Sunday at 12:00").unwrap(), "0 0 12 ? * SUN *");
/// ```
///
/// # Errors
///
/// This function returns an [`Error`] if it is unable to parse the provided string
/// into a valid cron syntax. This may occur when the input is incomplete, ambiguous,
/// or does not follow a recognizable pattern.
///
/// # Return
///
/// Returns a [`Result`] containing the parsed cron expression as a `String` on success,
/// or an [`Error`] if parsing fails.
///
/// [`Error`]: str_to_cron::Error
pub fn str_cron_syntax(input: &str) -> str_to_cron::Result<String> {
let cron = str_to_cron::Cron::new(input)?;
Ok(format!("{cron}"))
}
@@ -0,0 +1,177 @@
//! This file provides functionality for processing clock time tokens, converting them into the
//! appropriate format for cron syntax. It recognizes various time formats, including 12-hour
//! format with AM/PM and 24-hour format, as well as specific keywords like "noon" and "midnight".
//!
//! The regex patterns defined here help to match and extract hours and minutes from the tokens.
//!
//! This is part of a broader module that converts human-readable strings into cron syntax.
use super::super::{
action::Kind,
cron::Cron,
stack::{Stack, StartEnd},
Error, Result,
};
use regex::Regex;
use std::sync::LazyLock;
/// A regex pattern that matches various clock time formats, including:
/// - 12-hour format with AM/PM (e.g., "5 PM", "7 AM")
/// - 24-hour format (e.g., "13:00")
/// - Special cases for "noon" and "midnight"
static RE_MATCH: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?i)^([0-9]+:)?[0-9]+ *(AM|PM)$|^([0-9]+:[0-9]+)$|(noon|midnight)").unwrap()
});
/// A regex pattern to extract the hour from a time token.
static RE_HOUR: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^[0-9]+").unwrap());
/// A regex pattern to extract the minute from a time token.
static RE_MINUTE: LazyLock<Regex> = LazyLock::new(|| Regex::new(r":[0-9]+").unwrap());
/// A regex pattern that matches the keywords "noon" and "midnight".
static RE_NOON_MIDNIGHT: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(noon|midnight)").unwrap());
/// Checks if a given string token matches the expected clock time format.
pub fn try_from_token(str: &str) -> bool {
RE_MATCH.is_match(str)
}
#[allow(clippy::too_many_lines)]
/// Processes a clock time token and updates the corresponding fields in the cron syntax structure.
///
/// This function extracts hours and minutes from the token, handles conversions from 12-hour to 24-hour format,
/// and sets the appropriate values in the `Cron` struct. It also handles specific cases for "noon" and "midnight".
///
/// # Errors
///
/// Returns an error if parsing the hour or minute fails, if values are out of range, or if the time is incorrect.
pub fn process(token: &str, cron: &mut Cron) -> Result<()> {
let mut hour = 0;
let mut minute = 0;
if let Some(hour_str) = RE_HOUR.find(token) {
hour = hour_str
.as_str()
.parse::<i32>()
.map_err(|_| Error::ParseToNumber {
state: "clock_time".to_string(),
value: hour_str.as_str().to_string(),
})?;
}
if let Some(minute_str) = RE_MINUTE.find(token) {
if minute_str.as_str().contains(':') {
if let Some(minute_str) = minute_str.as_str().split(':').nth(1) {
minute = minute_str
.parse::<i32>()
.map_err(|_| Error::ParseToNumber {
state: "clock_time".to_string(),
value: minute_str.to_string(),
})?;
if minute >= 60 {
return Err(Error::IncorrectValue {
state: "clock_time".to_string(),
error: format!("minute {minute} should be lower or equal to 60"),
});
}
}
}
}
match token.to_lowercase().as_str() {
_ if token.to_lowercase().contains("pm") => {
match hour.cmp(&12) {
std::cmp::Ordering::Less => hour += 12,
std::cmp::Ordering::Greater => {
return Err(Error::IncorrectValue {
state: "clock_time".to_string(),
error: format!("please correct the time before PM. value: {hour}"),
});
}
std::cmp::Ordering::Equal => {} // Do nothing, hour remains 12
}
}
_ if token.to_lowercase().contains("am") => {
match hour.cmp(&12) {
std::cmp::Ordering::Equal => hour = 0,
std::cmp::Ordering::Greater => {
return Err(Error::IncorrectValue {
state: "clock_time".to_string(),
error: format!("please correct the time before AM. value: {hour}"),
});
}
std::cmp::Ordering::Less => {} // Do nothing, hour remains unchanged
}
}
_ => {} // Handle other cases if necessary
}
if RE_NOON_MIDNIGHT.is_match(token) {
if token == "noon" {
hour = 12;
} else {
hour = 0;
}
minute = 0;
}
if let Some(element) = cron.stack.last_mut() {
if element.owner == Kind::RangeStart {
element.hour = Some(StartEnd {
start: Some(hour),
end: None,
});
return Ok(());
} else if element.owner == Kind::RangeEnd {
if let Some(element_hour) = &mut element.hour {
if element_hour.start == Some(hour) {
element.min = Some(StartEnd {
start: Some(hour),
end: Some(hour),
});
cron.syntax.hour = format!("{hour}-{hour}");
} else {
element_hour.end = Some(hour);
if element.is_and_connector && !element.is_between_range {
// Use comma for "and" connector but not in a "between X and Y" context
// Check if the syntax hour already has values
if cron.syntax.hour.contains(',') {
// If it already has comma-separated values, append the new hour
cron.syntax.hour = format!("{},{}", cron.syntax.hour, hour);
} else {
cron.syntax.hour =
format!("{},{}", element_hour.start.unwrap_or_default(), hour);
}
} else {
// Use hyphen for other range connectors or for "between X and Y"
cron.syntax.hour =
format!("{}-{}", element_hour.start.unwrap_or_default(), hour);
}
}
}
return Ok(());
}
}
cron.syntax.min = minute.to_string();
cron.syntax.hour = hour.to_string();
cron.stack.push(
Stack::builder(Kind::ClockTime)
.hour(StartEnd {
start: Some(hour),
end: None,
})
.min(StartEnd {
start: Some(minute),
end: None,
})
.build(),
);
Ok(())
}
@@ -0,0 +1,169 @@
//! This module provides utilities for processing and validating day-related tokens
//! for use in cron expressions. It leverages regular expressions to match and
//! parse inputs related to weekdays, allowing for flexible input formats.
//!
//! The module defines constants for the days of the week and provides functions
//! to determine whether a given token is valid as a day input, as well as to
//! process that token into a `Cron` structure.
use super::super::{
action::Kind,
cron::Cron,
stack::{Stack, StartEndString},
Error, Result,
};
use regex::Regex;
use std::fmt::Write;
use std::sync::LazyLock;
/// Matches various formats for days, including full names and abbreviations.
static RE_MATCH: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?i)^((days|day)|(((monday|tuesday|wednesday|thursday|friday|saturday|sunday|WEEKEND|MON|TUE|WED|THU|FRI|SAT|SUN)( ?and)?,? ?)+))$")
.unwrap()
});
/// Matches the tokens "day" or "days".
static RE_DAY: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)^(day|days)$").unwrap());
/// Matches the abbreviations for weekdays and the term "WEEKEND".
static RE_WEEKDAYS: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?i)(MON|TUE|WED|THU|FRI|SAT|SUN|WEEKEND)").unwrap());
// Constant array representing the days of the week in uppercase.
const WEEK_DAYS: [&str; 7] = ["MON", "TUE", "WED", "THU", "FRI", "SAT", "SUN"];
/// Checks if the provided string matches the expected day token formats.
pub fn try_from_token(str: &str) -> bool {
RE_MATCH.is_match(str)
}
/// Processes the given token to update the `cron` object with the specified day of the week information.
///
/// This function determines whether the input token specifies days in a "day" or "days" format, or specific weekdays.
/// It then updates the `day_of_week` and `day_of_month` fields in the provided `cron` object based on the matched days.
///
/// # Returns
///
/// * [`Result<()>`] - Returns `Ok(())` if the processing is successful, or an `Error` if the token does not match expected formats.
pub fn process(token: &str, cron: &mut Cron) -> Result<()> {
if RE_DAY.is_match(token) {
cron.syntax.day_of_week = "?".to_string();
if cron.syntax.min == "*" {
cron.syntax.min = "0".to_string();
}
if cron.syntax.hour == "*" {
cron.syntax.hour = "0".to_string();
}
if let Some(element) = cron.stack.last() {
if element.owner == Kind::FrequencyOnly {
cron.syntax.day_of_month = format!("*/{}", element.frequency_to_string());
cron.stack.pop();
} else if element.owner == Kind::FrequencyWith {
cron.syntax.day_of_month = element.frequency_to_string();
cron.stack.pop();
} else {
cron.syntax.day_of_month = "*".to_string();
}
} else {
cron.syntax.day_of_month = "*/1".to_string();
}
} else {
let matches: Vec<_> = RE_WEEKDAYS.find_iter(token).collect();
if matches.is_empty() {
return Err(Error::IncorrectValue {
state: "day".to_string(),
error: format!("value {token} is not a weekend format"),
});
}
// Set the day of week
cron.syntax.day_of_week = String::new();
let days: Vec<String> = matches
.iter()
.map(|day| day.as_str().to_uppercase())
.collect::<Vec<_>>();
if let Some(element) = cron.stack.last_mut() {
if element.owner == Kind::RangeStart {
element.day = Some(StartEndString {
start: days.first().cloned(),
end: element.day.clone().and_then(|a| a.end),
});
return Ok(());
} else if element.owner == Kind::RangeEnd {
let data = StartEndString {
start: element.day.clone().and_then(|a| a.start),
end: days.first().cloned(),
};
element.day = Some(data.clone());
if let (Some(start), Some(end)) = (data.start, data.end) {
write!(cron.syntax.day_of_week, "{start}-{end}").map_err(|_| {
Error::IncorrectValue {
state: "day".to_string(),
error: "Failed to format day of week range".to_string(),
}
})?;
}
cron.syntax.day_of_month = "?".to_string();
cron.stack.pop();
return Ok(());
} else if element.owner == Kind::OnlyOn {
// Special case for "only on" syntax
let day = days.first().cloned().ok_or_else(|| Error::IncorrectValue {
state: "day".to_string(),
error: "Expected at least one day in 'only on' syntax but found none"
.to_string(),
})?;
cron.syntax.day_of_week = day;
cron.syntax.day_of_month = "?".to_string();
// Remove the "only on" entry from the stack
cron.stack.pop();
return Ok(());
}
// For other cases, clear the stack to start fresh
cron.stack.clear();
}
// Normal processing for days
for &day in &WEEK_DAYS {
if days.contains(&day.to_string()) && !cron.syntax.day_of_week.contains(day) {
write!(cron.syntax.day_of_week, "{day},").map_err(|_| Error::IncorrectValue {
state: "day".to_string(),
error: "Failed to format day of week".to_string(),
})?;
}
}
// Handle the WEEKEND case
if days.contains(&"WEEKEND".to_string()) {
for &day in &["SAT", "SUN"] {
if !cron.syntax.day_of_week.contains(day) {
write!(cron.syntax.day_of_week, "{day},").map_err(|_| {
Error::IncorrectValue {
state: "day".to_string(),
error: "Failed to format weekend days".to_string(),
}
})?;
}
}
}
cron.syntax.day_of_week = cron.syntax.day_of_week.trim_end_matches(',').to_string();
cron.syntax.day_of_month = "?".to_string();
}
cron.stack.push(
Stack::builder(Kind::Day)
.day_of_week(cron.syntax.day_of_week.clone())
.build(),
);
Ok(())
}
@@ -0,0 +1,41 @@
/// This module provides functionality for processing frequency-related tokens
/// within cron expressions. It defines a function to validate frequency inputs
/// and another to process these inputs, updating the associated `Cron` structure.
///
use super::super::{action::Kind, cron::Cron, stack::Stack};
use regex::Regex;
use std::sync::LazyLock;
static RE_MATCH: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^[0-9]+$").unwrap());
/// Checks if the given string is a valid frequency token.
pub fn try_from_token(str: &str) -> bool {
RE_MATCH.is_match(str)
}
/// Processes the given frequency and updates the specified `Cron` structure.
///
/// This function modifies the `cron` stack based on the provided frequency.
/// If the last item in the stack indicates the start or end of a range,
/// the function updates the corresponding frequency fields. If the stack
/// is empty, it adds a new entry with the specified frequency.
pub fn process(frequency: i32, cron: &mut Cron) {
if !cron.stack.is_empty() {
if let Some(last_stack) = cron.stack.last_mut() {
if last_stack.owner == Kind::RangeEnd {
last_stack.frequency_end = Some(frequency);
return;
} else if last_stack.owner == Kind::RangeStart {
last_stack.frequency_start = Some(frequency);
return;
}
} else {
panic!("handle later")
}
}
cron.stack.push(
Stack::builder(Kind::FrequencyOnly)
.frequency(frequency)
.build(),
);
}
@@ -0,0 +1,65 @@
//! This file defines functionality for handling frequency-based tokens that include qualifiers
//! such as "3rd" or "5th". These tokens are parsed and processed in relation to their position
//! in a cron syntax structure. The regex patterns help in identifying such tokens, and
//! the `process` function applies the detected frequency to the appropriate cron field.
//!
//! The file is a part of a larger module that converts human-readable strings into cron syntax.
use super::super::{action::Kind, cron::Cron, stack::Stack, Error, Result};
use regex::Regex;
use std::sync::LazyLock;
/// A regex pattern that matches frequency tokens with ordinal suffixes like "th", "nd", "rd", or "st".
static RE_MATCH: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^[0-9]+(th|nd|rd|st)$").unwrap());
/// A regex pattern that extracts the numeric prefix of a token, assuming it starts with a number.
static RE_NUMERIC_PREFIX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^[0-9]+").unwrap());
/// Checks if a given string token matches the pattern for ordinal-based frequency (e.g., "3rd", "5th").
pub fn try_from_token(str: &str) -> bool {
RE_MATCH.is_match(str)
}
/// Processes a frequency-based token and applies the corresponding value to the cron syntax structure.
///
/// This function parses the numeric prefix from the token, such as "3" from "3rd", and then
/// updates the cron's internal state based on the token's context (e.g., if it's a range start,
/// range end, or general frequency).
///
/// # Errors
///
/// Returns an error if the token doesn't contain a numeric prefix or if parsing the number fails.
///
pub fn process(token: &str, cron: &mut Cron) -> Result<()> {
let maybe_numeric_prefix = RE_NUMERIC_PREFIX
.find(token)
.ok_or_else(|| Error::Capture {
state: "frequency_with".to_string(),
token: token.to_string(),
})?;
let frequency =
maybe_numeric_prefix
.as_str()
.parse::<i32>()
.map_err(|_| Error::ParseToNumber {
state: "frequency_with".to_string(),
value: maybe_numeric_prefix.as_str().to_string(),
})?;
if let Some(element) = cron.stack.last_mut() {
if element.owner == Kind::RangeEnd {
element.frequency_end = Some(frequency);
return Ok(());
} else if element.owner == Kind::RangeStart {
element.frequency_start = Some(frequency);
return Ok(());
}
}
cron.stack.push(
Stack::builder(Kind::FrequencyWith)
.frequency(frequency)
.day_of_week(cron.syntax.day_of_week.clone())
.build(),
);
Ok(())
}
@@ -0,0 +1,83 @@
//! This module handles processing of hour-related tokens for cron expressions.
//! It validates input tokens representing hours and updates the `Cron` structure
//! accordingly.
//!
use super::super::{
action::Kind,
cron::Cron,
stack::{Stack, StartEnd},
};
use regex::Regex;
use std::sync::LazyLock;
/// Regex pattern for matching any form of the word "hour" (including "hrs" and "hours").
/// This pattern is case-insensitive and matches both singular and plural forms.
static RE_MATCH: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)(hour|hrs|hours)").unwrap());
/// Regex pattern to specifically match the exact words "hour", "hrs", or "hours".
/// This pattern is case-sensitive and is used to verify if a token is strictly
/// one of the specified hour terms.
static RE_HOUR: LazyLock<Regex> = LazyLock::new(|| Regex::new("^(hour|hrs|hours)$").unwrap());
/// Checks if the given string is a valid hour token.
pub fn try_from_token(str: &str) -> bool {
RE_MATCH.is_match(str)
}
/// Processes the given hour token and updates the specified `Cron` structure.
///
/// This function modifies the `cron` stack based on the provided hour token.
/// If the last item in the stack indicates a frequency, the function updates the
/// corresponding hour fields. If a range start or end is detected, it adjusts
/// the hour range accordingly.
pub fn process(token: &str, cron: &mut Cron) {
if RE_HOUR.is_match(token) {
let mut hour = None;
if let Some(element) = cron.stack.last_mut() {
if element.owner == Kind::FrequencyOnly {
hour = Some(StartEnd {
start: element.frequency,
end: None,
});
cron.syntax.hour = format!("0/{}", element.frequency_to_string());
cron.syntax.min = "0".to_string();
cron.stack.pop();
} else if element.owner == Kind::FrequencyWith {
hour = Some(StartEnd {
start: element.frequency,
end: None,
});
cron.syntax.hour = element.frequency_to_string();
cron.syntax.min = "0".to_string();
cron.stack.pop();
} else if element.owner == Kind::RangeStart {
element.min = Some(StartEnd {
start: element.frequency_start,
end: None,
});
return;
} else if element.owner == Kind::RangeEnd {
element.min = Some(StartEnd {
start: element.frequency_start,
end: element.frequency_end,
});
element.frequency_end = None;
if let (Some(frequency_start), Some(frequency_end)) =
(element.frequency_start, element.frequency_end)
{
cron.syntax.hour = format!("{frequency_start}-{frequency_end}",);
cron.syntax.min = "0".to_string();
}
return;
}
}
cron.syntax.min = "0".to_string();
if let Some(hour) = hour {
cron.stack
.push(Stack::builder(Kind::Minute).hour(hour).build());
}
}
}
@@ -0,0 +1,81 @@
//! This module handles processing of minute-related tokens for cron expressions.
//! It validates input tokens representing minutes and updates the `Cron` structure
//! accordingly.
use super::super::{
action::Kind,
cron::Cron,
stack::{Stack, StartEnd},
};
use regex::Regex;
use std::sync::LazyLock;
/// Regex pattern for matching any form of the word "minute" (including "mins" and "minutes").
/// This pattern is case-insensitive and matches both singular and plural forms.
static RE_MATCH: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?i)(minutes|minute|mins|min)").unwrap());
/// Regex pattern to specifically match the exact words "minute", "mins", or "minutes".
/// This pattern is case-sensitive and is used to verify if a token is strictly
/// one of the specified minute terms.
static RE_MINUTES: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?i)^(minutes|minute|mins|min)$").unwrap());
/// Checks if the given string is a valid minute token.
pub fn try_from_token(str: &str) -> bool {
RE_MATCH.is_match(str)
}
/// Processes the given minute token and updates the specified `Cron` structure.
///
/// This function modifies the `cron` stack based on the provided minute token.
/// If the last item in the stack indicates a frequency, the function updates the
/// corresponding minute fields. If a range start or end is detected, it adjusts
/// the minute range accordingly.
pub fn process(token: &str, cron: &mut Cron) {
if RE_MINUTES.is_match(token) {
let mut minutes = None;
if let Some(element) = cron.stack.last_mut() {
if element.owner == Kind::FrequencyOnly {
minutes = Some(StartEnd {
start: element.frequency,
end: None,
});
cron.syntax.min = format!("0/{}", element.frequency_to_string());
cron.stack.pop();
} else if element.owner == Kind::FrequencyWith {
minutes = Some(StartEnd {
start: element.frequency,
end: None,
});
cron.syntax.min = element.frequency_to_string();
cron.stack.pop();
} else if element.owner == Kind::RangeStart {
element.min = Some(StartEnd {
start: element.frequency_start,
end: None,
});
return;
} else if element.owner == Kind::RangeEnd {
element.min = Some(StartEnd {
start: element.frequency_start,
end: element.frequency_end,
});
element.frequency_end = None;
if let (Some(frequency_start), Some(frequency_end)) =
(element.frequency_start, element.frequency_end)
{
cron.syntax.min = format!("{frequency_start}-{frequency_end}",);
}
return;
}
}
if let Some(minutes) = minutes {
cron.stack
.push(Stack::builder(Kind::Minute).min(minutes).build());
}
}
}
@@ -0,0 +1,121 @@
//! This module defines the various kinds of tokens that can be processed in a cron expression.
//! It provides functions to match and process these tokens accordingly.
use super::{cron::Cron, Error, Result};
mod clock_time;
mod day;
mod frequency_only;
mod frequency_with;
mod hour;
mod minute;
mod month;
mod range_end;
mod range_start;
mod seconds;
mod year;
/// An enumeration of the kinds of tokens that can be processed in a cron expression.
#[derive(Debug, PartialEq, Eq, Clone, Copy, PartialOrd, Ord)]
pub enum Kind {
/// Token indicating a frequency with specified intervals.
FrequencyWith,
/// Token indicating a frequency without specific intervals.
FrequencyOnly,
/// Token indicating a specific time on a clock.
ClockTime,
/// Token indicating days of the week.
Day,
/// Token indicating secund.
Secund,
/// Token indicating minutes.
Minute,
/// Token indicating hours.
Hour,
/// Token indicating months.
Month,
/// Token indicating years.
Year,
/// Token indicating the start of a range.
RangeStart,
/// Token indicating the end of a range.
RangeEnd,
/// Token indicating "only on" directive.
OnlyOn,
}
/// Attempts to match the provided token to one of the `Kind` enumerations.
/// Returns `Some(Kind)` if a match is found, or `None` if no match exists.
pub fn try_from_token(token: &str) -> Option<Kind> {
for state_kind in Kind::iterator() {
let is_match = match state_kind {
Kind::FrequencyWith => frequency_with::try_from_token(token),
Kind::FrequencyOnly => frequency_only::try_from_token(token),
Kind::ClockTime => clock_time::try_from_token(token),
Kind::Day => day::try_from_token(token),
Kind::Secund => seconds::try_from_token(token),
Kind::Minute => minute::try_from_token(token),
Kind::Hour => hour::try_from_token(token),
Kind::Month => month::try_from_token(token),
Kind::Year => year::try_from_token(token),
Kind::RangeStart => range_start::try_from_token(token),
Kind::RangeEnd => range_end::try_from_token(token),
Kind::OnlyOn => token.to_lowercase() == "only on",
};
if is_match {
return Some(state_kind);
}
}
None
}
impl Kind {
/// Provides an iterator over all possible [`Kind`] values.
const fn iterator() -> [Self; 12] {
[
Self::FrequencyWith,
Self::FrequencyOnly,
Self::ClockTime,
Self::Day,
Self::Secund,
Self::Minute,
Self::Hour,
Self::Month,
Self::Year,
Self::RangeStart,
Self::RangeEnd,
Self::OnlyOn,
]
}
/// Processes the token based on the kind of token.
/// Each variant has its own processing logic defined in the respective module.
/// Returns a `Result<()>` indicating success or failure of the operation.
pub fn process(self, token: &str, cron: &mut Cron) -> Result<()> {
match self {
Self::FrequencyWith => frequency_with::process(token, cron)?,
Self::FrequencyOnly => {
let frequency = token.parse::<i32>().map_err(|_| Error::ParseToNumber {
state: "frequency_only".to_string(),
value: token.to_string(),
})?;
frequency_only::process(frequency, cron);
}
Self::ClockTime => clock_time::process(token, cron)?,
Self::Day => day::process(token, cron)?,
Self::Secund => seconds::process(token, cron),
Self::Minute => minute::process(token, cron),
Self::Hour => hour::process(token, cron),
Self::Month => month::process(token, cron)?,
Self::Year => year::process(token, cron)?,
Self::RangeStart => range_start::process(token, cron),
Self::RangeEnd => range_end::process(token, cron),
Self::OnlyOn => {
// When "only on" is encountered, we don't need to do anything special
// The next token should be a day, which will be handled correctly
}
}
Ok(())
}
}
@@ -0,0 +1,153 @@
//! Module for processing month tokens in cron expressions.
//!
//! This module provides functionality to interpret and process month-related tokens
//! in the context of cron scheduling. It defines regular expressions for matching
//! various month formats and provides functions for token validation and processing.
use super::super::{
action::Kind,
cron::Cron,
stack::{Stack, StartEndString},
Error, Result,
};
use regex::Regex;
use std::fmt::Write;
use std::sync::LazyLock;
/// Regular expression to match valid month input in various formats (e.g., "January", "JAN").
static RE_MATCH: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?i)^((months|month)|(((january|february|march|april|may|june|july|august|september|october|november|december|JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEPT|OCT|NOV|DEC)( ?and)?,? ?)+))$").unwrap()
});
/// Regular expression to match the word "month" or "months".
static RE_MONTH: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)^(month|months)$").unwrap());
/// Regular expression to find month abbreviations in the input string.
static RE_MONTHS_ABBREVIATION: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?i)(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)").unwrap());
const MONTHS: [&str; 12] = [
"JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC",
];
/// Checks if the provided token is a valid month representation.
pub fn try_from_token(str: &str) -> bool {
RE_MATCH.is_match(str)
}
/// Processes the given month token and updates the cron structure accordingly.
///
/// This function interprets the month token and modifies the `cron` object to
/// reflect the corresponding month settings. It handles various scenarios, such as
/// frequency specifications, ranges, and default settings.
///
/// # Returns
/// A [`Result<()>`] indicating success or failure. In case of an incorrect month format,
/// an `Error::IncorrectValue` is returned.
///
pub fn process(token: &str, cron: &mut Cron) -> Result<()> {
if RE_MONTH.is_match(token) {
if let Some(element) = cron.stack.last() {
if element.owner == Kind::FrequencyOnly {
// cron.syntax.month = format!("0/{}", element.frequency_to_string());
cron.syntax.month = element.frequency_to_string();
cron.stack.pop();
} else if element.owner == Kind::FrequencyWith {
cron.syntax.month = element.frequency_to_string();
cron.stack.pop();
} else if element.owner == Kind::RangeEnd {
cron.syntax.day_of_month = format!(
"{},{}",
element.frequency_start.unwrap_or_default(),
element.frequency_end.unwrap_or_default()
);
} else {
cron.syntax.month = "*".to_string();
}
} else {
cron.syntax.month = "*".to_string();
}
} else {
let matches: Vec<_> = RE_MONTHS_ABBREVIATION.find_iter(token).collect();
if matches.is_empty() {
return Err(Error::IncorrectValue {
state: "month".to_string(),
error: format!("value {token} is not a month format"),
});
}
cron.syntax.month = String::new();
let months: Vec<String> = matches
.iter()
.map(|month| month.as_str().to_uppercase())
.collect::<Vec<_>>();
if let Some(element) = cron.stack.last_mut() {
if element.owner == Kind::FrequencyOnly || element.owner == Kind::FrequencyWith {
cron.syntax.day_of_month = element.frequency_to_string();
cron.stack.pop();
} else if element.owner == Kind::RangeStart {
element.month = Some(element.month.as_ref().map_or_else(
|| StartEndString {
start: months.first().cloned(),
end: None,
},
|month| StartEndString {
start: months.first().cloned(),
end: month.end.clone(),
},
));
cron.stack.pop();
return Ok(());
} else if element.owner == Kind::RangeEnd {
if let Some(frequency_end) = element.frequency_end {
cron.syntax.day_of_week = "?".to_string();
if let Some(frequency_start) = element.frequency_start {
cron.syntax.day_of_month = format!("{frequency_start}-{frequency_end}");
}
}
let data = element.month.as_ref().map_or_else(
|| StartEndString {
start: None,
end: months.first().cloned(),
},
|month| StartEndString {
start: month.start.clone(),
end: months.first().cloned(),
},
);
element.month = Some(data.clone());
if let (Some(start), Some(end)) = (data.start, data.end) {
cron.syntax.month = format!("{start}-{end}");
}
cron.stack.pop();
return Ok(());
} else {
cron.stack.pop();
}
}
for &month in &MONTHS {
if months.contains(&month.to_string()) && !cron.syntax.month.contains(month) {
write!(cron.syntax.month, "{month},").unwrap();
}
}
cron.syntax.month = cron.syntax.month.trim_end_matches(',').to_string();
}
cron.stack.push(
Stack::builder(Kind::Month)
.month(StartEndString {
start: Some(cron.syntax.month.clone()),
end: None,
})
.build(),
);
Ok(())
}
@@ -0,0 +1,63 @@
/// Module for processing range-related tokens in cron expressions.
///
/// This module handles the interpretation of tokens that represent ranges or connections
/// between elements in cron scheduling, such as "to", "through", "ending", and "and".
use super::super::{action::Kind, cron::Cron, stack::StartEndString};
use regex::Regex;
use std::sync::LazyLock;
/// Regular expression to match range-related keywords (e.g., "to", "through").
static RE_MATCH: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?i)(to|through|ending|end|and)").unwrap());
/// Regular expression to specifically match "and".
static RE_MATCH_AND: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)(and)").unwrap());
/// Checks if the provided token matches range-related keywords.
pub fn try_from_token(str: &str) -> bool {
RE_MATCH.is_match(str)
}
/// Processes the cron object to interpret range-related tokens.
pub fn process(token: &str, cron: &mut Cron) {
// Check if the token is "and" specifically
let is_and = RE_MATCH_AND.is_match(token);
if let Some(element) = cron.stack.last_mut() {
// Set the is_and flag in the element so we know to use comma instead of hyphen
element.is_and_connector = is_and;
match element.owner {
Kind::FrequencyWith | Kind::FrequencyOnly => {
element.frequency_start = element.frequency;
}
Kind::Day => {
element.day = match &element.day {
Some(day) => Some(StartEndString {
start: element.day_of_week.clone(),
end: day.end.clone(),
}),
None => Some(StartEndString {
start: element.day_of_week.clone(),
end: None,
}),
};
}
Kind::Month => {
element.owner = Kind::RangeEnd;
}
Kind::RangeStart => element.owner = Kind::RangeEnd,
Kind::Year
| Kind::ClockTime
| Kind::Minute
| Kind::Hour
| Kind::RangeEnd
| Kind::Secund
| Kind::OnlyOn => {}
}
element.owner = Kind::RangeEnd;
}
}
@@ -0,0 +1,27 @@
//! Module for processing range start-related tokens in cron expressions.
use super::super::{action::Kind, cron::Cron, stack::Stack};
use regex::Regex;
use std::sync::LazyLock;
/// Regular expression to match keywords indicating the start of a range (e.g., "between", "starting").
static RE_MATCH: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?i)(between|starting|start)").unwrap());
/// Regular expression to specifically match "between".
static RE_MATCH_BETWEEN: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)(between)").unwrap());
/// Checks if the provided token matches range start-related keywords.
pub fn try_from_token(str: &str) -> bool {
RE_MATCH.is_match(str)
}
/// Processes the cron object to interpret range start-related tokens.
pub fn process(token: &str, cron: &mut Cron) {
let mut stack = Stack::builder(Kind::RangeStart).build();
// Detect if this is a "between" range
stack.is_between_range = RE_MATCH_BETWEEN.is_match(token);
cron.stack.push(stack);
}
@@ -0,0 +1,45 @@
//! Module for processing second-related tokens in cron expressions.
//!
//! This module interprets tokens that specify seconds, including keywords like
//! "second", "seconds", "sec", and "secs". It updates the `Cron` object with
//! the appropriate values based on the input token.
use super::super::{action::Kind, cron::Cron, stack::Stack};
use regex::Regex;
use std::sync::LazyLock;
/// Regular expression to match any form of the word "second".
static RE_MATCH: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?i)(seconds|second|sec|secs)").unwrap());
/// Regular expression to match exactly the words "second" or "seconds".
static RE_SECUND: LazyLock<Regex> =
LazyLock::new(|| Regex::new("^(seconds|second|sec|secs)$").unwrap());
/// Checks if the provided token matches second-related keywords or formats.
pub fn try_from_token(str: &str) -> bool {
RE_MATCH.is_match(str)
}
/// Processes the provided token to update the cron object with second information.
///
/// This function interprets second-related tokens, updating the `cron` object's
/// syntax seconds based on the provided token. It handles both exact keyword matches
/// and updates the cron stack appropriately.
pub fn process(token: &str, cron: &mut Cron) {
if RE_SECUND.is_match(token) {
if let Some(element) = cron.stack.last_mut() {
if element.owner == Kind::FrequencyOnly {
cron.syntax.seconds = format!("0/{}", element.frequency_to_string());
cron.stack.pop();
} else if element.owner == Kind::FrequencyWith {
cron.syntax.seconds = element.frequency_to_string();
cron.stack.pop();
}
} else {
cron.syntax.seconds = "*".to_string();
}
cron.stack.push(Stack::builder(Kind::Secund).build());
}
}
@@ -0,0 +1,117 @@
//! Module for processing year-related tokens in cron expressions.
//!
//! This module handles the interpretation of tokens that specify years,
//! including keywords like "year" or "years" and numeric year values.
use super::super::{
action::Kind,
cron::Cron,
stack::{Stack, StartEnd},
Error, Result,
};
use regex::Regex;
use std::sync::LazyLock;
/// Regular expression to match keywords related to years (e.g., "years", "year") and numeric values.
static RE_MATCH: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"(?i)((years|year)|([0-9]{4}[0-9]*(( ?and)?,? ?))+)").unwrap());
/// Regular expression to match just the keywords for years.
static RE_YEARS: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"(?i)^(years|year)$").unwrap());
/// Regular expression to match numeric values.
static RE_NUMERIC: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"[0-9]+").unwrap());
/// Regular expression to validate year format (four digits).
static RE_YEAR_FORMAT: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^[0-9]{4}$").unwrap());
/// Checks if the provided token matches year-related keywords or formats.
pub fn try_from_token(str: &str) -> bool {
RE_MATCH.is_match(str)
}
/// Processes the provided token to update the cron object with year information.
///
/// This function interprets year-related tokens, updating the cron's syntax year
/// based on the provided token. It handles both keyword matches and numeric year values.
pub fn process(token: &str, cron: &mut Cron) -> Result<()> {
if RE_YEARS.is_match(token) {
cron.syntax.year = "?".to_string();
if let Some(element) = cron.stack.last_mut() {
if element.owner == Kind::FrequencyOnly {
cron.syntax.year = format!("0/{}", element.frequency_to_string());
cron.stack.pop();
} else if element.owner == Kind::FrequencyWith {
cron.syntax.year = element.frequency_to_string();
} else {
cron.syntax.year = "*".to_string();
}
}
} else {
let matches: Vec<_> = RE_NUMERIC.find_iter(token).collect();
let years: Vec<i32> = matches
.iter()
.filter_map(|year| {
if RE_YEAR_FORMAT.is_match(year.as_str()) {
if let Ok(year) = year.as_str().parse::<i32>() {
return Some(year);
}
}
None
})
.collect::<Vec<_>>();
if let Some(element) = cron.stack.last_mut() {
if element.owner == Kind::RangeStart {
element.year = Some(element.year.as_ref().map_or_else(
|| StartEnd {
start: years.first().copied(),
end: None,
},
|year| StartEnd {
start: years.first().copied(),
end: year.end,
},
));
return Ok(());
} else if element.owner == Kind::RangeEnd {
let year = element.year.as_ref().map_or_else(
|| StartEnd {
start: None,
end: years.first().copied(),
},
|year| StartEnd {
start: year.start,
end: years.first().copied(),
},
);
cron.syntax.year = format!(
"{}-{}",
year.start.unwrap_or_default(),
year.end.unwrap_or_default()
);
cron.stack.pop();
return Ok(());
}
}
if years.is_empty() {
return Err(Error::IncorrectValue {
state: "year".to_string(),
error: format!("value {token} is not a year format"),
});
}
cron.syntax.year = String::new();
for year in years {
cron.syntax.year = format!("{}{},", cron.syntax.year, year);
}
cron.syntax.year = cron.syntax.year.trim_end_matches(',').to_string();
}
cron.stack.push(Stack::builder(Kind::Year).build());
Ok(())
}
@@ -0,0 +1,87 @@
use crate::str_to_cron::Tokenizer;
use std::str::FromStr;
use super::{action, stack::Stack, Error, Result};
#[derive(Default, Debug)]
pub struct Cron {
pub syntax: Syntax,
pub stack: Vec<Stack>,
}
#[derive(Debug)]
pub struct Syntax {
pub seconds: String,
pub min: String,
pub hour: String,
pub day_of_month: String,
pub day_of_week: String,
pub month: String,
pub year: String,
}
impl Default for Syntax {
fn default() -> Self {
Self {
seconds: "0".to_string(),
min: "*".to_string(),
hour: "*".to_string(),
day_of_month: "*".to_string(),
day_of_week: "?".to_string(),
month: "*".to_string(),
year: "*".to_string(),
}
}
}
impl std::fmt::Display for Cron {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{} {} {} {} {} {} {}",
self.syntax.seconds.trim(),
self.syntax.min.trim(),
self.syntax.hour.trim(),
self.syntax.day_of_month.trim(),
self.syntax.month.trim(),
self.syntax.day_of_week.trim(),
self.syntax.year.trim(),
)
}
}
impl Cron {
/// Creates a new `Cron` instance from a given cron expression string.
///
/// This function tokenizes the input string and processes each token to construct
/// a valid `Cron` representation. If the input is empty or contains invalid tokens,
/// an error is returned.
///
/// # Errors
///
/// Returns [`Error::InvalidInput`] if the input is empty or contains invalid tokens.
///
pub fn new(text: &str) -> Result<Self> {
let tokenizer = Tokenizer::new();
let tokens = tokenizer.run(text);
if tokens.is_empty() {
return Err(Error::InvalidInput);
}
let mut cron = Self::default();
for token in tokens {
if let Some(state) = action::try_from_token(&token) {
state.process(&token, &mut cron)?;
}
}
Ok(cron)
}
}
impl FromStr for Cron {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::new(s)
}
}
@@ -0,0 +1,76 @@
//! This module defines error types and handling for the "English to Corn" project.
//!
//! The `Error` enum represents the different kinds of errors that can occur during
//! the processing of input data. Each error variant captures specific details about the error,
//! allowing for more descriptive and accurate error reporting.
//!
//! The module also provides a type alias `Result<T>` for convenience, defaulting to
//! using the `Error` type as the error variant in the `std::result::Result`.
/// Represents the different kinds of errors that can occur in the "English to Corn" project.
///
/// The variants capture specific error scenarios, such as invalid input or failed parsing
/// operations, along with relevant state or context information.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Error {
/// Error variant for invalid input.
/// This variant is used when the input provided is not in a human-readable format.
InvalidInput,
/// Error variant for capture-related failures.
/// This occurs when a specific token cannot be captured within a given state.
///
/// # Fields
/// - `state`: The state in which the error occurred.
/// - `token`: The token that could not be captured.
Capture { state: String, token: String },
/// Error variant for failed parsing to a number.
/// This occurs when a value could not be parsed as a number within a specific state.
///
/// # Fields
/// - `state`: The state in which the error occurred.
/// - `value`: The value that could not be parsed into a number.
ParseToNumber { state: String, value: String },
/// Error variant for incorrect or invalid values.
/// This is triggered when an invalid value is encountered in a given state.
///
/// # Fields
/// - `state`: The state in which the error occurred.
/// - `error`: A description of the error or the reason why the value is considered invalid.
IncorrectValue { state: String, error: String },
}
/// Implements the `Display` trait for the `Error` enum.
///
/// This allows for user-friendly error messages to be printed, making it easier
/// to understand the cause of an error when it occurs.
impl std::fmt::Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::InvalidInput => write!(f, "Please enter human readable"),
Self::Capture { state, token } => {
write!(f, "Could not capture: {token} in state: {state} ")
}
Self::ParseToNumber { state, value } => {
write!(f, "Could not parse: {value} to number. state: {state} ")
}
Self::IncorrectValue { state, error } => {
write!(
f,
"value is invalid in state: {state}. description: {error} "
)
}
}
}
}
/// Implements the `std::error::Error` trait for the `Error` enum.
///
/// This allows the error type to be used with the `?` operator and error handling
/// libraries like `anyhow` and `thiserror`. The default implementation is sufficient
/// since `Error` already implements `Display` and `Debug`.
impl std::error::Error for Error {}
/// Custom `Result` type alias for the "English to Corn" project.
///
/// This is a convenience alias for `std::result::Result` where the error type defaults to the `Error` enum.
pub type Result<T, E = Error> = std::result::Result<T, E>;
@@ -0,0 +1,9 @@
mod action;
mod cron;
mod errors;
mod stack;
mod tokens;
pub use cron::Cron;
pub use errors::{Error, Result};
pub use tokens::Tokenizer;
@@ -0,0 +1,92 @@
use super::action;
#[derive(Clone, Debug)]
pub struct StartEnd {
pub start: Option<i32>,
pub end: Option<i32>,
}
#[derive(Clone, Debug)]
pub struct StartEndString {
pub start: Option<String>,
pub end: Option<String>,
}
#[derive(Clone, Debug)]
pub struct Stack {
pub owner: action::Kind,
pub frequency: Option<i32>,
pub frequency_end: Option<i32>,
pub frequency_start: Option<i32>,
pub min: Option<StartEnd>,
pub hour: Option<StartEnd>,
pub day: Option<StartEndString>,
pub month: Option<StartEndString>,
pub year: Option<StartEnd>,
pub day_of_week: Option<String>,
pub is_and_connector: bool,
pub is_between_range: bool,
}
impl Stack {
pub const fn builder(owner: action::Kind) -> Builder {
Builder {
stack: Self {
owner,
frequency: None,
frequency_end: None,
frequency_start: None,
min: None,
hour: None,
day: None,
month: None,
year: None,
day_of_week: None,
is_and_connector: false,
is_between_range: false,
},
}
}
}
pub struct Builder {
stack: Stack,
}
impl Builder {
pub const fn frequency(mut self, frequency: i32) -> Self {
self.stack.frequency = Some(frequency);
self
}
pub const fn min(mut self, min: StartEnd) -> Self {
self.stack.min = Some(min);
self
}
pub const fn hour(mut self, hour: StartEnd) -> Self {
self.stack.hour = Some(hour);
self
}
pub fn month(mut self, month: StartEndString) -> Self {
self.stack.month = Some(month);
self
}
pub fn day_of_week(mut self, day_of_week: String) -> Self {
self.stack.day_of_week = Some(day_of_week);
self
}
pub fn build(self) -> Stack {
self.stack
}
}
impl Stack {
pub fn frequency_to_string(&self) -> String {
self.frequency
.map_or_else(|| "*".to_string(), |a| a.to_string())
}
}
@@ -0,0 +1,46 @@
use regex::Regex;
use std::sync::LazyLock;
static RE_TOKENS: LazyLock<Regex> = LazyLock::new(|| {
Regex::new(r"(?i)(?:seconds|second|secs|sec)|(?:hours?|hrs?)|(?:minutes?|mins?|min)|(?:months?|(?:january|february|march|april|may|june|july|august|september|october|november|december|jan|feb|mar|apr|may|jun|jul|aug|sept|oct|nov|dec)(?: ?and)?,? ?)+|[0-9]+(?:th|nd|rd|st)|(?:[0-9]+:)?[0-9]+ ?(?:am|pm)|[0-9]+:[0-9]+|(?:noon|midnight)|(?:days?|(?:monday|tuesday|wednesday|thursday|friday|saturday|sunday|weekend|mon|tue|wed|thu|fri|sat|sun)(?: ?and)?,? ?)+|(?:[0-9]{4}[0-9]*(?: ?and)?,? ?)+|[0-9]+|(?:only on)|(?:to|through|ending|end|and)|(?:between|starting|start)").unwrap()
});
pub struct Tokenizer {
regex: Regex,
}
impl Default for Tokenizer {
fn default() -> Self {
Self::new()
}
}
impl Tokenizer {
pub fn new() -> Self {
Self {
regex: RE_TOKENS.clone(),
}
}
#[must_use]
pub fn run(&self, input_string: &str) -> Vec<String> {
// Preprocess the input to handle special cases
let processed_input = input_string.replace(", ", " and ");
// Handle "only on" followed by day names as a special pattern
let processed_input = if processed_input.contains("only on") {
// Remove "and" before "only on" to prevent misinterpretation
processed_input.replace(" and only on", " only on")
} else {
processed_input
};
let matches = self
.regex
.find_iter(&processed_input)
.map(|m| m.as_str().trim().to_string())
.collect();
matches
}
}
+129
View File
@@ -0,0 +1,129 @@
use english_to_cron::str_cron_syntax;
use rstest::rstest;
use std::result::Result;
// Test case demonstrating std::error::Error implementation without anyhow
// This verifies that the Error type can be used with standard Rust error handling
// and works with the ? operator in functions returning Box<dyn std::error::Error>
#[test]
fn test_error_with_std_error_trait() -> Result<(), Box<dyn std::error::Error>> {
// Test successful case - verify ? operator works
let _cron = str_cron_syntax("every 5 minutes")?;
// Test error case - verify error can be converted to Box<dyn std::error::Error>
let result: Result<String, Box<dyn std::error::Error>> =
str_cron_syntax("invalid input").map_err(|e| Box::new(e) as Box<dyn std::error::Error>);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Please enter human readable"));
Ok(())
}
#[rstest]
// Seconds
#[case("Run second", Ok("* * * * * ? *"))]
#[case("every 5 second", Ok("0/5 * * * * ? *"))]
#[case("every 5 second on september", Ok("0/5 * * * SEP ? *"))]
#[case("every 5 second on 9 month", Ok("0/5 * * * 9 ? *"))]
#[case("Every 2 seconds, only on thursday", Ok("0/2 * * ? * THU *"))]
#[case("Run every 2 second on the 12th day", Ok("0/2 0 0 12 * ? *"))]
#[case("Run every 2 second on Monday thursday", Ok("0/2 * * ? * MON,THU *"))]
#[case(
"Run every 10 seconds Monday through thursday between 6:00 am and 8:00 pm",
Ok("0/10 * 6-20 ? * MON-THU *")
)]
// Minutes
#[case("Run every minute", Ok("0 * * * * ? *"))]
#[case("Run every 15 minutes", Ok("0 0/15 * * * ? *"))]
#[case("every minutes on thursday", Ok("0 * * ? * THU *"))]
#[case("every 2 minutes on Thursday", Ok("0 0/2 * ? * THU *"))]
#[case(
"Run every 10 minutes Monday through Friday every month",
Ok("0 0/10 * ? * MON-FRI *")
)]
#[case(
"Run every 1 minutes Monday through Thursday between 6:00 am and 9:00 pm",
Ok("0 0/1 6-21 ? * MON-THU *")
)]
#[case(
"Run every 5 minutes Monday through Thursday between 6:00 am and 9:00 am",
Ok("0 0/5 6-9 ? * MON-THU *")
)]
#[case("Every 5 minutes, only on Friday", Ok("0 0/5 * ? * FRI *"))]
// Hours
#[case("Run every 3 hours", Ok("0 0 0/3 * * ? *"))]
#[case(
"Run every 6 hours, starting at 1:00 pm on day Monday",
Ok("0 0 0/6 ? * MON *")
)]
#[case("Run every 1 hour only on weekends", Ok("0 0 0/1 ? * SAT,SUN *"))]
#[case("Run every hour only on weekends", Ok("0 0 * ? * SAT,SUN *"))]
#[case(
"2pm on Tuesday, Wednesday and Thursday",
Ok("0 0 14 ? * TUE,WED,THU *")
)]
// Days
#[case("Run every day", Ok("0 0 0 */1 * ? *"))]
#[case("Run every 4 days", Ok("0 0 0 */4 * ? *"))]
#[case("every day at 4:00 pm", Ok("0 0 16 */1 * ? *"))]
#[case("every 2 day at 4:00 pm", Ok("0 0 16 */2 * ? *"))]
#[case("every 5 day at 4:30 pm", Ok("0 30 16 */5 * ? *"))]
#[case("every 5 day at 4:30 pm only in September", Ok("0 30 16 */5 SEP ? *"))]
#[case(
"every 5 day at 4:30 pm Monday through Thursday",
Ok("0 30 16 ? * MON-THU *")
)]
#[case("Run every day from January to March", Ok("0 0 0 */1 JAN-MAR ? *"))]
#[case("Run every 3 days at noon", Ok("0 0 12 */3 * ? *"))]
#[case("Run every 2nd day of the month", Ok("0 0 0 2 * ? *"))]
// Month
#[case("Run every sec from January to March", Ok("* * * * JAN-MAR ? *"))]
#[case("Run every minute from January to March", Ok("0 * * * JAN-MAR ? *"))]
#[case("Run every hours from January to March", Ok("0 0 * * JAN-MAR ? *"))]
// Year
#[case(
"every 2 day from January to August in 2020 and 2024",
Ok("0 0 0 */2 JAN-AUG ? 2020,2024")
)]
// Specific Times (AM/PM)
#[case("Run at 10:00 am", Ok("0 0 10 * * ? *"))]
#[case("Run at 12:15 pm", Ok("0 15 12 * * ? *"))]
#[case(
"Run at 6:00 pm every Monday through Friday",
Ok("0 0 18 ? * MON-FRI *")
)]
#[case("Run at noon every Sunday", Ok("0 0 12 ? * SUN *"))]
#[case(
"Run at midnight on the 1st and 15th of the month",
Ok("0 0 0 1,15 * ? *")
)]
#[case("midnight on Tuesdays", Ok("0 0 0 ? * TUE *"))]
#[case("Run at 5:15am every Tuesday", Ok("0 15 5 ? * TUE *"))]
#[case("7pm every Thursday", Ok("0 0 19 ? * THU *"))]
#[case("2pm and 6pm", Ok("0 0 14,18 * * ? *"))]
#[case("5am, 10am and 3pm", Ok("0 0 5,10,15 * * ? *"))]
#[case("Run every hour only on Monday", Ok("0 0 * ? * MON *"))]
#[case("Run every 30 seconds only on weekends", Ok("0/30 * * ? * SAT,SUN *"))]
#[case("4pm, 5pm and 7pm", Ok("0 0 16,17,19 * * ? *"))]
#[case("4pm, 5pm, and 7pm", Ok("0 0 16,17,19 * * ? *"))]
#[case("4pm, 5pm, 7pm", Ok("0 0 16,17,19 * * ? *"))]
#[case("4pm and 5pm and 7pm", Ok("0 0 16,17,19 * * ? *"))]
#[test]
fn can_parse_string(
#[case] cron_str: &str,
#[case] expected_result: english_to_cron::Result<&str>,
) {
let result = str_cron_syntax(cron_str);
assert_eq!(
result,
expected_result
.clone()
.map(std::string::ToString::to_string),
"Failed for input: '{cron_str}'. Expected: {expected_result:?}, Got: {result:?}"
);
}