feat(sched): ✨ recurrence ruleset
This commit is contained in:
parent
52b6ead7b3
commit
9b0faace04
14
.github/img/logo-tokio-sched.svg
vendored
Normal file
14
.github/img/logo-tokio-sched.svg
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 142 181.60616">
|
||||||
|
<style>
|
||||||
|
.a { fill: #000000; }
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.a { fill: #ffffff; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<path class="a" d="M4.91962,1.516H0V0H20.42877V61.6416h4.6695v1.51648H.3335V61.6416H4.91962ZM32.68567,58.60962q-5.33725-5.3865-5.33649-15.45264,0-10.06072,5.71161-15.49371,5.71014-5.432,16.051-5.432,10.33767,0,15.42578,5.01056Q69.62248,32.25306,69.624,42.5265,69.624,63.999,48.44489,64,38.02069,64,32.68567,58.60962ZM43.35858,39.83124v6.56811q0,9.18109.54187,11.03144a18.93828,18.93828,0,0,0,1.12549,3.032,3.54887,3.54887,0,0,0,3.58575,2.02167q3.252,0,4.25214-3.79071.751-2.69324.75079-12.12592v-7.1575a57.79352,57.79352,0,0,0-.87579-11.916q-.87543-3.74541-4.04382-3.74713a4.16106,4.16106,0,0,0-2.96,1.05242,6.48726,6.48726,0,0,0-1.62616,3.495A64.69988,64.69988,0,0,0,43.35858,39.83124ZM137.33051,61.6416V0H116.90173V1.516h4.91962V61.6416h-4.58618v1.51648H142V61.6416Zm-59.36792-3.032q-5.33792-5.3865-5.33741-15.4526,0-10.06071,5.7116-15.49371,5.71161-5.432,16.051-5.432,10.33839,0,15.42669,5.01056,5.08347,5.01123,5.08538,15.28473Q114.89989,63.999,93.721,64q-10.423,0-15.75841-5.39038ZM88.6355,39.83124v6.56811q0,9.18109.541,11.03144a18.93282,18.93282,0,0,0,1.12641,3.032,3.54716,3.54716,0,0,0,3.58477,2.02167q3.25276,0,4.25269-3.79071.75165-2.69324.75122-12.12592h.00006v-7.1575a57.81812,57.81812,0,0,0-.87634-11.916q-.87533-3.74541-4.04425-3.74713a4.1601,4.1601,0,0,0-2.95954,1.05242,6.48472,6.48472,0,0,0-1.62567,3.495A64.70833,64.70833,0,0,0,88.6355,39.83124Z" />
|
||||||
|
<path class="a" d="M0,80.25H142V78H0Zm0,55.4873H142v-2.25H0Z" />
|
||||||
|
<path class="a" d="M32.7168,175.23047a13.36634,13.36634,0,0,1-5.083-.88379,7.19068,7.19068,0,0,1-3.24707-2.41406l2.82129-2.61817a7.317,7.317,0,0,0,2.39746,1.751,7.48028,7.48028,0,0,0,3.14453.62891,4.9894,4.9894,0,0,0,2.34668-.459,1.50353,1.50353,0,0,0,.84961-1.41114,1.05742,1.05742,0,0,0-.57813-1.03711,5.844,5.844,0,0,0-1.59765-.459l-2.82227-.4414a11.65666,11.65666,0,0,1-2.21-.52734,5.79126,5.79126,0,0,1-1.76758-.96877,4.51762,4.51762,0,0,1-1.19043-1.49609,4.74953,4.74953,0,0,1-.4414-2.14258,5.07767,5.07767,0,0,1,2.10742-4.31738,9.76432,9.76432,0,0,1,5.916-1.56446,12.33256,12.33256,0,0,1,4.40332.69727,6.98233,6.98233,0,0,1,2.90723,1.98926l-2.5166,2.85546a6.39878,6.39878,0,0,0-2.00586-1.42773,6.8788,6.8788,0,0,0-2.958-.57813q-2.92383,0-2.92383,1.76758a1.08491,1.08491,0,0,0,.57813,1.07129,5.76441,5.76441,0,0,0,1.59766.459l2.78808.44238a11.73241,11.73241,0,0,1,2.21.52637,6.01022,6.01022,0,0,1,1.78516.96973,4.42257,4.42257,0,0,1,1.207,1.49609,4.758,4.758,0,0,1,.44141,2.1416,5.15168,5.15168,0,0,1-2.125,4.35157A9.87237,9.87237,0,0,1,32.7168,175.23047Zm19.27734,0a10.38785,10.38785,0,0,1-3.72266-.62891,7.48787,7.48787,0,0,1-2.78808-1.81933,8.05769,8.05769,0,0,1-1.751-2.88965,12.36164,12.36164,0,0,1,0-7.68457,8.07436,8.07436,0,0,1,1.751-2.88965,7.49987,7.49987,0,0,1,2.78808-1.81934,10.2878,10.2878,0,0,1,3.68848-.6289,8.21939,8.21939,0,0,1,4.6416,1.17285,7.58039,7.58039,0,0,1,2.669,3.11133l-3.876,2.1084a5.166,5.166,0,0,0-1.24121-1.7002,3.19768,3.19768,0,0,0-2.19336-.68066,3.46974,3.46974,0,0,0-2.66894,1.0039,3.8531,3.8531,0,0,0-.93457,2.73633v2.85645a3.85466,3.85466,0,0,0,.93457,2.73631,4.22275,4.22275,0,0,0,5.03222.32326,5.68183,5.68183,0,0,0,1.41114-1.76758l3.80761,2.17578a7.6575,7.6575,0,0,1-2.77051,3.11133A8.70027,8.70027,0,0,1,51.99414,175.23047Zm10.33594-25.56836H67.3623v10.8125h.20313a7.45023,7.45023,0,0,1,.748-1.39453,5.1228,5.1228,0,0,1,1.07129-1.13867,5.02915,5.02915,0,0,1,1.46191-.78223,5.74713,5.74713,0,0,1,1.88672-.28906,6.133,6.133,0,0,1,2.3125.4248,4.73688,4.73688,0,0,1,1.80175,1.27539,6.15492,6.15492,0,0,1,1.17285,2.07422,8.59043,8.59043,0,0,1,.42579,2.82129v11.35645H73.41406V164.14648q0-3.3999-2.958-3.40039a4.29111,4.29111,0,0,0-1.13964.15332,3.04878,3.04878,0,0,0-1.00293.47559,2.26221,2.26221,0,0,0-.95118,1.9209v11.52637H62.33008ZM89.835,175.23047q-4.41943,0-6.69726-2.44824a9.3926,9.3926,0,0,1-2.27832-6.66407,11.75017,11.75017,0,0,1,.59472-3.85937,8.29689,8.29689,0,0,1,1.7002-2.92383,7.20108,7.20108,0,0,1,2.68653-1.83594,9.54308,9.54308,0,0,1,3.55272-.6289,9.41885,9.41885,0,0,1,3.53614.6289,7.37005,7.37005,0,0,1,2.65136,1.78516,8.00193,8.00193,0,0,1,1.6836,2.80469,10.81793,10.81793,0,0,1,.59473,3.68945v1.49609h-12.002v.30567a3.66932,3.66932,0,0,0,1.05371,2.7373,4.23592,4.23592,0,0,0,3.09473,1.03711,5.65292,5.65292,0,0,0,2.68555-.5957,6.3569,6.3569,0,0,0,1.93847-1.58008l2.71972,2.958a8.49359,8.49359,0,0,1-2.85645,2.17578A10.401,10.401,0,0,1,89.835,175.23047Zm-.40723-14.75586a3.45,3.45,0,0,0-2.60156,1.00293,3.72752,3.72752,0,0,0-.96875,2.70312v.27149h7.07227v-.27149a3.79745,3.79745,0,0,0-.93554-2.7207,3.36663,3.36663,0,0,0-2.56646-.98535Zm22.81349,11.12012H112.003a6.79869,6.79869,0,0,1-1.88672,2.624,5.0089,5.0089,0,0,1-3.31543,1.01172,6.42716,6.42716,0,0,1-2.66894-.54395,5.40155,5.40155,0,0,1-2.07325-1.666,8.36007,8.36007,0,0,1-1.36035-2.85644,17.40432,17.40432,0,0,1,0-8.22754,8.36011,8.36011,0,0,1,1.36035-2.85645,5.41055,5.41055,0,0,1,2.07325-1.666,6.4273,6.4273,0,0,1,2.66894-.54394,5.866,5.866,0,0,1,1.87011.27441,4.87288,4.87288,0,0,1,2.53321,1.90332,8.56042,8.56042,0,0,1,.79883,1.458h.23828V149.66211h5.03222v25.16016h-5.03222Zm-3.26368-.3086a4.06214,4.06214,0,0,0,2.29493-.6416,2.25613,2.25613,0,0,0,.96875-2.02637v-5.13574a2.25617,2.25617,0,0,0-.96875-2.02637,4.0623,4.0623,0,0,0-2.29493-.6416,3.32294,3.32294,0,0,0-2.61815,1.03028,4.14086,4.14086,0,0,0-.918,2.85449v2.70215a4.13747,4.13747,0,0,0,.918,2.85449,3.31968,3.31968,0,0,0,2.61811,1.03027Z" />
|
||||||
|
<path class="a" d="M84.87354,108.18789v-1.86527H75.818a4.71385,4.71385,0,0,0-.17007-.66088l3.08537-1.8052-.94834-1.576-3.0327,1.79318a4.92488,4.92488,0,0,0-.63407-.61652l4.14093-7.09875L78.7333,95.57l-1.57875-.9465-4.63683,7.89c-.2089-.0647-.41594-.12017-.62345-.16637v-3.4514H70.06736v3.42736a4.07075,4.07075,0,0,0-.66735.17655l-4.12015-7.08859-.47325-.78752-1.57688.9465,4.58415,7.86039a5.66375,5.66375,0,0,0-.6045.60543l-3.03315-1.795-.9465,1.57873,3.08445,1.80334a4.69342,4.69342,0,0,0-.17008.66182H57.12646v1.82737h9.01763a4.58936,4.58936,0,0,0,.17009.66088l-3.08445,1.8052.9465,1.576,3.029-1.7904a4.87987,4.87987,0,0,0,.41409.42888l-4.10813,7.20782-.47325.78844,1.57735.94558,4.5994-7.889a4.39375,4.39375,0,0,0,.85268.23385v3.42182h1.82692V112.0738a4.712,4.712,0,0,0,.90767-.27544l4.14833,7.10984.47233.78937,1.57688-.9465-4.604-7.89737a4.547,4.547,0,0,0,.40207-.41224l3.02621,1.7904.9465-1.57688-3.08445-1.8052a4.69364,4.69364,0,0,0,.17008-.66181Zm-16.9224-.97053A3.04886,3.04886,0,1,1,71,110.26668,3.04886,3.04886,0,0,1,67.95114,107.21736ZM71.89427,96.9798a.91537.91537,0,1,1-.91537-.91537A.91537.91537,0,0,1,71.89427,96.9798Zm-9.27644,4.32314a.91537.91537,0,1,1-1.25042.335A.91537.91537,0,0,1,62.61783,101.30294Zm-.89427,10.1952a.91537.91537,0,1,1-.33505,1.25042A.91536.91536,0,0,1,61.72356,111.49814Zm8.38217,5.87206a.91537.91537,0,1,1,.91537.91537A.91536.91536,0,0,1,70.10573,117.3702Zm9.27643-4.32314a.91537.91537,0,1,1,1.25042-.335A.91538.91538,0,0,1,79.38216,113.04706Zm.89428-10.1952a.91537.91537,0,1,1,.335-1.25042A.91537.91537,0,0,1,80.27644,102.85186Z" />
|
||||||
|
<path class="a" style="opacity:0.1" d="M30.42139,109.69h-2.9043l.33594-1.96778h3.24023l.96-2.832H29.14893l.33593-1.96777H32.7251l1.72754-4.99219h2.16015l-5.71191,16.75195H28.71729Zm8.54394-11.75977h2.16016l-1.7041,4.99219h2.90332l-.33594,1.96777H38.74951l-.96,2.832h2.90332L40.35693,109.69H37.1167l-1.70313,4.99218H33.229Zm6.19141,20.06348,3.59961-21.55176h7.08008l-.31153,1.84863H50.58057L47.605,116.146h4.94336l-.31152,1.84766ZM97.02051,96.44194,93.41992,117.9937H86.33984l.3125-1.84766h4.94434l2.97559-17.85547H89.62793l.3125-1.84863Z" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 7.5 KiB |
346
Cargo.lock
generated
346
Cargo.lock
generated
@ -2,12 +2,101 @@
|
|||||||
# It is not intended for manual editing.
|
# It is not intended for manual editing.
|
||||||
version = 3
|
version = 3
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "addr2line"
|
||||||
|
version = "0.21.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb"
|
||||||
|
dependencies = [
|
||||||
|
"gimli",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "adler"
|
||||||
|
version = "1.0.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
|
||||||
|
|
||||||
|
[[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 = "autocfg"
|
||||||
|
version = "1.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "backtrace"
|
||||||
|
version = "0.3.71"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d"
|
||||||
|
dependencies = [
|
||||||
|
"addr2line",
|
||||||
|
"cc",
|
||||||
|
"cfg-if",
|
||||||
|
"libc",
|
||||||
|
"miniz_oxide",
|
||||||
|
"object",
|
||||||
|
"rustc-demangle",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bitflags"
|
name = "bitflags"
|
||||||
version = "2.5.0"
|
version = "2.5.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
|
checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bumpalo"
|
||||||
|
version = "3.15.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cc"
|
||||||
|
version = "1.0.90"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cfg-if"
|
||||||
|
version = "1.0.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "chrono"
|
||||||
|
version = "0.4.37"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e"
|
||||||
|
dependencies = [
|
||||||
|
"android-tzdata",
|
||||||
|
"iana-time-zone",
|
||||||
|
"js-sys",
|
||||||
|
"num-traits",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "core-foundation-sys"
|
||||||
|
version = "0.8.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "eyre"
|
name = "eyre"
|
||||||
version = "0.6.12"
|
version = "0.6.12"
|
||||||
@ -18,12 +107,56 @@ dependencies = [
|
|||||||
"once_cell",
|
"once_cell",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "gimli"
|
||||||
|
version = "0.28.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone"
|
||||||
|
version = "0.1.60"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
|
||||||
|
dependencies = [
|
||||||
|
"android_system_properties",
|
||||||
|
"core-foundation-sys",
|
||||||
|
"iana-time-zone-haiku",
|
||||||
|
"js-sys",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"windows-core",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iana-time-zone-haiku"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "indenter"
|
name = "indenter"
|
||||||
version = "0.3.3"
|
version = "0.3.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
|
checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "js-sys"
|
||||||
|
version = "0.3.69"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
|
||||||
|
dependencies = [
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "libc"
|
||||||
|
version = "0.2.153"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "log"
|
name = "log"
|
||||||
version = "0.4.21"
|
version = "0.4.21"
|
||||||
@ -35,8 +168,44 @@ name = "lool"
|
|||||||
version = "0.0.5"
|
version = "0.0.5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags",
|
"bitflags",
|
||||||
|
"chrono",
|
||||||
"eyre",
|
"eyre",
|
||||||
"log",
|
"log",
|
||||||
|
"num-traits",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "memchr"
|
||||||
|
version = "2.7.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "miniz_oxide"
|
||||||
|
version = "0.7.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7"
|
||||||
|
dependencies = [
|
||||||
|
"adler",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-traits"
|
||||||
|
version = "0.2.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
|
||||||
|
dependencies = [
|
||||||
|
"autocfg",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "object"
|
||||||
|
version = "0.32.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@ -44,3 +213,180 @@ name = "once_cell"
|
|||||||
version = "1.19.0"
|
version = "1.19.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-lite"
|
||||||
|
version = "0.2.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "proc-macro2"
|
||||||
|
version = "1.0.79"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e"
|
||||||
|
dependencies = [
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "quote"
|
||||||
|
version = "1.0.35"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rustc-demangle"
|
||||||
|
version = "0.1.23"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "2.0.58"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio"
|
||||||
|
version = "1.37.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
|
||||||
|
dependencies = [
|
||||||
|
"backtrace",
|
||||||
|
"pin-project-lite",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicode-ident"
|
||||||
|
version = "1.0.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen"
|
||||||
|
version = "0.2.92"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"wasm-bindgen-macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-backend"
|
||||||
|
version = "0.2.92"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
|
||||||
|
dependencies = [
|
||||||
|
"bumpalo",
|
||||||
|
"log",
|
||||||
|
"once_cell",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro"
|
||||||
|
version = "0.2.92"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
|
||||||
|
dependencies = [
|
||||||
|
"quote",
|
||||||
|
"wasm-bindgen-macro-support",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-macro-support"
|
||||||
|
version = "0.2.92"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn",
|
||||||
|
"wasm-bindgen-backend",
|
||||||
|
"wasm-bindgen-shared",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wasm-bindgen-shared"
|
||||||
|
version = "0.2.92"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-core"
|
||||||
|
version = "0.52.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
|
||||||
|
dependencies = [
|
||||||
|
"windows-targets",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows-targets"
|
||||||
|
version = "0.52.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b"
|
||||||
|
dependencies = [
|
||||||
|
"windows_aarch64_gnullvm",
|
||||||
|
"windows_aarch64_msvc",
|
||||||
|
"windows_i686_gnu",
|
||||||
|
"windows_i686_msvc",
|
||||||
|
"windows_x86_64_gnu",
|
||||||
|
"windows_x86_64_gnullvm",
|
||||||
|
"windows_x86_64_msvc",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_gnullvm"
|
||||||
|
version = "0.52.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_aarch64_msvc"
|
||||||
|
version = "0.52.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_gnu"
|
||||||
|
version = "0.52.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_i686_msvc"
|
||||||
|
version = "0.52.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnu"
|
||||||
|
version = "0.52.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_gnullvm"
|
||||||
|
version = "0.52.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "windows_x86_64_msvc"
|
||||||
|
version = "0.52.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8"
|
||||||
|
|||||||
16
Cargo.toml
16
Cargo.toml
@ -23,9 +23,21 @@ path = "lib/lib.rs"
|
|||||||
cli-stylize = ["dep:bitflags"]
|
cli-stylize = ["dep:bitflags"]
|
||||||
logger = ["dep:log"]
|
logger = ["dep:log"]
|
||||||
macros = []
|
macros = []
|
||||||
|
sched = ["dep:chrono"]
|
||||||
|
sched-tokio = ["dep:tokio", "tokio?/time", "tokio?/rt"]
|
||||||
|
sched-rule-recurrent = []
|
||||||
|
sched-rule-cron = []
|
||||||
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bitflags = { version = "2.5.0", optional = true }
|
# default
|
||||||
log = { version = "0.4.21", optional = true }
|
|
||||||
eyre = { version = "0.6.12", default-features = false }
|
eyre = { version = "0.6.12", default-features = false }
|
||||||
|
|
||||||
|
# optional
|
||||||
|
bitflags = { version = "2.5.0", optional = true }
|
||||||
|
chrono = { version = "0.4.37", optional = true }
|
||||||
|
log = { version = "0.4.21", optional = true }
|
||||||
|
tokio = { version = "1.37.0", optional = true }
|
||||||
|
num-traits = "0.2.18"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -16,3 +16,4 @@
|
|||||||
- [x] [cli/stylize](lib/cli/stylize)
|
- [x] [cli/stylize](lib/cli/stylize)
|
||||||
- [x] [logging](lib/logger)
|
- [x] [logging](lib/logger)
|
||||||
- [x] [macros](lib/macros)
|
- [x] [macros](lib/macros)
|
||||||
|
- [x] [tokio/sched](lib/tokio/sched)
|
||||||
@ -1,5 +1,8 @@
|
|||||||
pub mod cli;
|
pub mod cli;
|
||||||
|
|
||||||
|
#[cfg(feature = "sched")]
|
||||||
|
pub mod sched;
|
||||||
|
|
||||||
#[cfg(feature = "logger")]
|
#[cfg(feature = "logger")]
|
||||||
pub mod logger;
|
pub mod logger;
|
||||||
|
|
||||||
|
|||||||
19
lib/lutest/mod.rs
Normal file
19
lib/lutest/mod.rs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
#[macro_export]
|
||||||
|
macro_rules! it {
|
||||||
|
// Entry point for the macro, takes the struct definition
|
||||||
|
(
|
||||||
|
$fn_name:tt, $body:expr
|
||||||
|
// $fn_name:ident $($rest:ident)+, $body:expr
|
||||||
|
) => {
|
||||||
|
#[test]
|
||||||
|
fn $fn_name () -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
$body
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// it!(should_do_the_trick, {
|
||||||
|
// assert_eq!(2, 1);
|
||||||
|
|
||||||
|
// Ok(())
|
||||||
|
// });
|
||||||
42
lib/sched/README.md
Normal file
42
lib/sched/README.md
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<p align="center"><img src="../../.github/img/logo-tokio-sched.svg" height="256"></p>
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<b>lool » <code>sched</code></b> is a utility library that provides a way to schedule tasks in various ways. Supports <code>std::thread</code> and the <a href="https://tokio.rs">tokio</a> runtime (as a feature flag).
|
||||||
|
</p>
|
||||||
|
|
||||||
|
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
<br>
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
|
||||||
|
This library is for internal use. And as such, it's only published privately.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cargo add lool --registry=lugit --features sched
|
||||||
|
```
|
||||||
|
|
||||||
|
# Additional Features
|
||||||
|
|
||||||
|
- `sched-tokio`: Enables the `tokio` runtime support.
|
||||||
|
- `sched-rule-recurrent`: Enables the "recurrent-rule" style for scheduling tasks.
|
||||||
|
- `sched-rule-cron`: Enables the "cron-like" style for scheduling tasks
|
||||||
|
|
||||||
|
> [!WARNING] Not implemented warning
|
||||||
|
> although the `sched-rule-cron` feature is available, it's not yet implemented.
|
||||||
|
|
||||||
|
## Planned Features
|
||||||
|
|
||||||
|
- `sched-rule-cron`: Enables the "cron-like" style for scheduling tasks
|
||||||
|
- `sched-rule-pysched`: Enables the "pysched-like" style for scheduling tasks
|
||||||
|
|
||||||
|
# Usage
|
||||||
|
|
||||||
|
<!--
|
||||||
|
TODO
|
||||||
|
-->
|
||||||
4
lib/sched/mod.rs
Normal file
4
lib/sched/mod.rs
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
mod rules;
|
||||||
|
pub mod utils;
|
||||||
|
|
||||||
|
pub use rules::*;
|
||||||
51
lib/sched/rules.rs
Normal file
51
lib/sched/rules.rs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
#[cfg(feature = "sched-rule-cron")]
|
||||||
|
mod cron;
|
||||||
|
#[cfg(feature = "sched-rule-recurrent")]
|
||||||
|
mod recurrent;
|
||||||
|
#[cfg(feature = "sched-rule-recurrent")]
|
||||||
|
pub use self::recurrent::{many, val, range, ranges, ruleset, RecurrenceRuleSet, Rule};
|
||||||
|
|
||||||
|
use chrono::{DateTime, Local};
|
||||||
|
|
||||||
|
/// 🧉 » a scheduling rule
|
||||||
|
///
|
||||||
|
/// can be:
|
||||||
|
/// - `Once`: runs only at a specific `chrono::DateTime`
|
||||||
|
/// - `Repeat`: runs at specific intervals defined by a `RecurrenceRule`
|
||||||
|
/// - `Cron`: runs at specific intervals defined by a cron expression
|
||||||
|
pub enum SchedulingRule {
|
||||||
|
/// 🧉 » a scheduling rule that makes the task run only once at a specific `chrono::DateTime`
|
||||||
|
Once(chrono::DateTime<Local>),
|
||||||
|
|
||||||
|
/// 🧉 » a scheduling rule expressed with a `RecurrenceRule` structure
|
||||||
|
#[cfg(feature = "sched-rule-recurrent")]
|
||||||
|
Repeat(RecurrenceRuleSet),
|
||||||
|
|
||||||
|
/// 🧉 » a scheduling rule expressed in cron format
|
||||||
|
#[cfg(feature = "sched-rule-cron")]
|
||||||
|
Cron(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SchedulingRule {
|
||||||
|
/// 🧉 » get the next execution time from now
|
||||||
|
pub fn next(&self) -> Option<DateTime<Local>> {
|
||||||
|
self.next_from(Local::now())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🧉 » get the next execution time from now
|
||||||
|
pub fn next_from(&self, _date: DateTime<Local>) -> Option<DateTime<Local>> {
|
||||||
|
match self {
|
||||||
|
SchedulingRule::Once(_dt) => {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
#[cfg(feature = "sched-rule-recurrent")]
|
||||||
|
SchedulingRule::Repeat(_rule) => {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
#[cfg(feature = "sched-rule-cron")]
|
||||||
|
SchedulingRule::Cron(_cron) => {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
2
lib/sched/rules/cron.rs
Normal file
2
lib/sched/rules/cron.rs
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
// TODO: cron based scheduling
|
||||||
8
lib/sched/rules/recurrent/mod.rs
Normal file
8
lib/sched/rules/recurrent/mod.rs
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
|
||||||
|
mod ruleset;
|
||||||
|
mod rule_unit;
|
||||||
|
|
||||||
|
pub use ruleset::{RecurrenceRuleSet, builder::ruleset};
|
||||||
|
pub use rule_unit::{Rule, val, range, many, ranges};
|
||||||
85
lib/sched/rules/recurrent/rule_unit.rs
Normal file
85
lib/sched/rules/recurrent/rule_unit.rs
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
use num_traits::PrimInt;
|
||||||
|
|
||||||
|
/// 🧉 » a recurrence rule unit
|
||||||
|
///
|
||||||
|
/// represents a single rule unit that can be used to match a value
|
||||||
|
pub enum Rule<T>
|
||||||
|
where
|
||||||
|
T: PrimInt,
|
||||||
|
{
|
||||||
|
/// a single value
|
||||||
|
Val(T),
|
||||||
|
/// range from `start` to `end` with `step` increment
|
||||||
|
Range(T, T, T),
|
||||||
|
/// a list of single values
|
||||||
|
Many(Vec<T>),
|
||||||
|
/// a list of ranges
|
||||||
|
Ranges(Vec<(T, T, T)>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> Rule<T>
|
||||||
|
where
|
||||||
|
T: PrimInt,
|
||||||
|
{
|
||||||
|
/// 🧉 » check if the value matches this `Rule` Unit
|
||||||
|
pub fn matches(&self, value: T) -> bool {
|
||||||
|
Self::_matches(self, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🚧 internal
|
||||||
|
fn _matches(matcher: &Rule<T>, value: T) -> bool {
|
||||||
|
match matcher {
|
||||||
|
Rule::Val(v) => value == *v,
|
||||||
|
Rule::Range(start, end, step) => {
|
||||||
|
if *step == T::one() {
|
||||||
|
value >= *start && value <= *end
|
||||||
|
} else {
|
||||||
|
let mut current = *start;
|
||||||
|
while current <= *end {
|
||||||
|
if current == value {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Move to the next value based on the step size
|
||||||
|
current = current + *step;
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Rule::Many(matcher) => matcher.iter().any(|v| Self::_matches(&Rule::Val(*v), value)),
|
||||||
|
Rule::Ranges(matcher) => matcher
|
||||||
|
.iter()
|
||||||
|
.any(|(start, end, step)| Self::_matches(&Rule::Range(*start, *end, *step), value)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn value_is_between(&self, min: T, max: T) -> bool {
|
||||||
|
match self {
|
||||||
|
Rule::Val(v) => *v >= min && *v <= max,
|
||||||
|
Rule::Range(start, end, _) => *start >= min && *end <= max,
|
||||||
|
Rule::Many(values) => values.iter().all(|v| *v >= min && *v <= max),
|
||||||
|
Rule::Ranges(ranges) => {
|
||||||
|
ranges.iter().all(|(start, end, _)| *start >= min && *end <= max)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🧉 » create a `Rule` that will match a single value
|
||||||
|
pub fn val<T: PrimInt>(value: T) -> Rule<T> {
|
||||||
|
Rule::Val(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🧉 » create a `Rule` that will match a range of values
|
||||||
|
pub fn range<T: PrimInt>(start: T, end: T, step: T) -> Rule<T> {
|
||||||
|
Rule::Range(start, end, step)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🧉 » create a `Rule` that will match many values
|
||||||
|
pub fn many<T: PrimInt>(values: Vec<T>) -> Rule<T> {
|
||||||
|
Rule::Many(values)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🧉 » create a `Rule` that will match a list of rages
|
||||||
|
pub fn ranges<T: PrimInt>(ranges: Vec<(T, T, T)>) -> Rule<T> {
|
||||||
|
Rule::Ranges(ranges)
|
||||||
|
}
|
||||||
128
lib/sched/rules/recurrent/ruleset.rs
Normal file
128
lib/sched/rules/recurrent/ruleset.rs
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
pub mod builder;
|
||||||
|
|
||||||
|
use {
|
||||||
|
super::Rule,
|
||||||
|
crate::sched::utils::cron_date::LoolDate,
|
||||||
|
chrono::{DateTime, Datelike, Local},
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 🧉 » a recurrence rule-set
|
||||||
|
///
|
||||||
|
/// sets rules that define a certain recurrence behavior
|
||||||
|
///
|
||||||
|
/// use the builder pattern to create a new `RecurrenceRuleSet`
|
||||||
|
pub struct RecurrenceRuleSet {
|
||||||
|
/// second of the minute (0..59)
|
||||||
|
second: Option<Rule<u32>>,
|
||||||
|
/// minute of the hour (0..59)
|
||||||
|
minute: Option<Rule<u32>>,
|
||||||
|
/// hour of the day (0..23)
|
||||||
|
hour: Option<Rule<u32>>,
|
||||||
|
/// day of the week starting from sunday (`0=Sunday`, `1=Monday`, ..., `6=Saturday`)
|
||||||
|
dow: Option<Rule<u32>>,
|
||||||
|
/// day of the month (1..31)
|
||||||
|
day: Option<Rule<u32>>,
|
||||||
|
/// month of the year (1..12)
|
||||||
|
month: Option<Rule<u32>>,
|
||||||
|
/// year
|
||||||
|
year: Option<Rule<i32>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RecurrenceRuleSet {
|
||||||
|
/// 🧉 » returns the next match of the rule set from `now`
|
||||||
|
pub fn next_match(&self) -> Option<DateTime<Local>> {
|
||||||
|
self.next_match_from(Local::now())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🧉 » returns the next match of the rule set from a given `DateTime`
|
||||||
|
pub fn next_match_from(&self, from: DateTime<Local>) -> Option<DateTime<Local>> {
|
||||||
|
let next = self._next_match(from);
|
||||||
|
match next {
|
||||||
|
Some(date) => Some(date.date()),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🚧 internal
|
||||||
|
fn _next_match(&self, from: DateTime<Local>) -> Option<LoolDate<Local>> {
|
||||||
|
if !self.is_valid() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check year
|
||||||
|
if let Some(Rule::Val(year)) = self.year {
|
||||||
|
if year < from.year().into() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut next = LoolDate::new(from.clone());
|
||||||
|
next.add_second();
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// check other possible year values
|
||||||
|
if let Some(year_unit) = &self.year {
|
||||||
|
if let Rule::Val(year) = year_unit {
|
||||||
|
if *year < next.year() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !year_unit.matches(next.year()) {
|
||||||
|
next.add_year();
|
||||||
|
next.set_md(1, 1);
|
||||||
|
next.set_hms(0, 0, 0);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(month_unit) = &self.month {
|
||||||
|
if !month_unit.matches(next.month()) {
|
||||||
|
next.add_month();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(day_unit) = &self.day {
|
||||||
|
if !day_unit.matches(next.day()) {
|
||||||
|
next.add_day();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(dow_unit) = &self.dow {
|
||||||
|
if !dow_unit.matches(next.weekday_from_sunday()) {
|
||||||
|
next.add_day();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(hour_unit) = &self.hour {
|
||||||
|
if !hour_unit.matches(next.hour()) {
|
||||||
|
next.add_hour();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(minute_unit) = &self.minute {
|
||||||
|
if !minute_unit.matches(next.minute()) {
|
||||||
|
next.add_minute();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(second_unit) = &self.second {
|
||||||
|
if !second_unit.matches(next.second()) {
|
||||||
|
next.add_second();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// finally, everything matches, so we get out of the loop
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(next)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
198
lib/sched/rules/recurrent/ruleset/builder.rs
Normal file
198
lib/sched/rules/recurrent/ruleset/builder.rs
Normal file
@ -0,0 +1,198 @@
|
|||||||
|
use chrono::Weekday;
|
||||||
|
|
||||||
|
use super::RecurrenceRuleSet;
|
||||||
|
use crate::sched::rules::Rule;
|
||||||
|
|
||||||
|
pub fn ruleset() -> RecurrenceRuleSet {
|
||||||
|
let ruleset = RecurrenceRuleSet::recurring();
|
||||||
|
ruleset
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RecurrenceRuleSet {
|
||||||
|
/// 🧉 » create a new `RecurrenceRuleSet
|
||||||
|
pub fn recurring() -> Self {
|
||||||
|
Self {
|
||||||
|
second: None,
|
||||||
|
minute: None,
|
||||||
|
hour: None,
|
||||||
|
dow: None,
|
||||||
|
day: None,
|
||||||
|
month: None,
|
||||||
|
year: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🧉 » set the second rule
|
||||||
|
pub fn seconds_rule(&mut self, rule: Rule<u32>) -> &mut Self {
|
||||||
|
self.second = Some(rule);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🧉 » set the minute rule
|
||||||
|
pub fn minutes_rule(&mut self, rule: Rule<u32>) -> &mut Self {
|
||||||
|
self.minute = Some(rule);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🧉 » set the hour rule
|
||||||
|
pub fn hours_rule(&mut self, rule: Rule<u32>) -> &mut Self {
|
||||||
|
self.hour = Some(rule);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🧉 » set the full time rule
|
||||||
|
pub fn time_rule(
|
||||||
|
&mut self,
|
||||||
|
hour: Rule<u32>,
|
||||||
|
minute: Rule<u32>,
|
||||||
|
second: Rule<u32>,
|
||||||
|
) -> &mut Self {
|
||||||
|
self.hours_rule(hour).minutes_rule(minute).seconds_rule(second)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🧉 » set the day of the week rule
|
||||||
|
pub fn dow_rule(&mut self, rule: Rule<u32>) -> &mut Self {
|
||||||
|
self.dow = Some(rule);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🧉 » set the day of the month rule
|
||||||
|
pub fn day_ryle(&mut self, rule: Rule<u32>) -> &mut Self {
|
||||||
|
self.day = Some(rule);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🧉 » set the month rule
|
||||||
|
pub fn month_rule(&mut self, rule: Rule<u32>) -> &mut Self {
|
||||||
|
self.month = Some(rule);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🧉 » set the year rule
|
||||||
|
pub fn year_rule(&mut self, rule: Rule<i32>) -> &mut Self {
|
||||||
|
self.year = Some(rule);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🧉 » set the second rule as a single value from primitive
|
||||||
|
pub fn at_second(&mut self, value: u32) -> &mut Self {
|
||||||
|
self.seconds_rule(Rule::Val(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🧉 » set the minute rule as a single value from primitive
|
||||||
|
pub fn at_minute(&mut self, value: u32) -> &mut Self {
|
||||||
|
self.minutes_rule(Rule::Val(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🧉 » set the hour rule as a single value from primitive
|
||||||
|
pub fn at_hour(&mut self, value: u32) -> &mut Self {
|
||||||
|
self.hours_rule(Rule::Val(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🧉 » set the full time rule as a single value from primitives
|
||||||
|
pub fn at_time(&mut self, hour: u32, minute: u32, second: u32) -> &mut Self {
|
||||||
|
self.time_rule(Rule::Val(hour), Rule::Val(minute), Rule::Val(second))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🧉 » set the day of the week rule as a single value from primitive
|
||||||
|
pub fn on_dow(&mut self, value: Weekday) -> &mut Self {
|
||||||
|
self.dow_rule(Rule::Val(value.num_days_from_sunday()))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🧉 » set the day of the month rule as a single value from primitive
|
||||||
|
pub fn on_day(&mut self, value: u32) -> &mut Self {
|
||||||
|
self.day_ryle(Rule::Val(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🧉 » set the month rule as a single value from primitive
|
||||||
|
pub fn in_month(&mut self, value: u32) -> &mut Self {
|
||||||
|
self.month_rule(Rule::Val(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🧉 » set the year rule as a single value from primitive
|
||||||
|
pub fn in_year(&mut self, value: i32) -> &mut Self {
|
||||||
|
self.year_rule(Rule::Val(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🧉 » set the full date as single values from primitives
|
||||||
|
pub fn on_date(&mut self, year: i32, month: u32, day: u32) -> &mut Self {
|
||||||
|
self.in_year(year).in_month(month).on_day(day)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🧉 » set the full datetime as single values from primitives
|
||||||
|
pub fn on_datetime(
|
||||||
|
&mut self,
|
||||||
|
year: i32,
|
||||||
|
month: u32,
|
||||||
|
day: u32,
|
||||||
|
hour: u32,
|
||||||
|
minute: u32,
|
||||||
|
second: u32,
|
||||||
|
) -> &mut Self {
|
||||||
|
self.on_date(year, month, day).at_time(hour, minute, second)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🧉 » check if the rule set is valid
|
||||||
|
pub fn is_valid(&self) -> bool {
|
||||||
|
// at least one of the rules must be set
|
||||||
|
let mut valid = self.second.is_some()
|
||||||
|
|| self.minute.is_some()
|
||||||
|
|| self.hour.is_some()
|
||||||
|
|| self.dow.is_some()
|
||||||
|
|| self.day.is_some()
|
||||||
|
|| self.month.is_some()
|
||||||
|
|| self.year.is_some();
|
||||||
|
|
||||||
|
// month/s should be between 1 and 12
|
||||||
|
if let Some(month) = &self.month {
|
||||||
|
valid = valid && month.value_is_between(1, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
// day/s of week should be between 0 and 6
|
||||||
|
if let Some(dow) = &self.dow {
|
||||||
|
valid = valid && dow.value_is_between(0, 6);
|
||||||
|
}
|
||||||
|
|
||||||
|
// day/s of month should be between 1 and 31
|
||||||
|
if let Some(day) = &self.day {
|
||||||
|
// check month overflows if month is also set
|
||||||
|
// HACK: I'm already handling day overflows and even leap years at `CronDate`
|
||||||
|
// this might not be necessary anymore... we should test it and see how it goes
|
||||||
|
// without this check (we might want to check for 1..31 as a minimum and that's
|
||||||
|
// all)
|
||||||
|
match &self.month {
|
||||||
|
Some(month) => {
|
||||||
|
if month.matches(2) {
|
||||||
|
valid = valid && day.value_is_between(1, 29);
|
||||||
|
} else if month.matches(4)
|
||||||
|
|| month.matches(6)
|
||||||
|
|| month.matches(9)
|
||||||
|
|| month.matches(11)
|
||||||
|
{
|
||||||
|
valid = valid && day.value_is_between(1, 30);
|
||||||
|
} else {
|
||||||
|
valid = valid && day.value_is_between(1, 31);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => valid = valid && day.value_is_between(1, 31),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// hour/s should be between 0 and 23
|
||||||
|
if let Some(hour) = &self.hour {
|
||||||
|
valid = valid && hour.value_is_between(0, 23);
|
||||||
|
}
|
||||||
|
|
||||||
|
// minute/s should be between 0 and 59
|
||||||
|
if let Some(minute) = &self.minute {
|
||||||
|
valid = valid && minute.value_is_between(0, 59);
|
||||||
|
}
|
||||||
|
|
||||||
|
// second/s should be between 0 and 59
|
||||||
|
if let Some(second) = &self.second {
|
||||||
|
valid = valid && second.value_is_between(0, 59);
|
||||||
|
}
|
||||||
|
|
||||||
|
valid
|
||||||
|
}
|
||||||
|
}
|
||||||
3
lib/sched/rules/recurrent/tests/mod.rs
Normal file
3
lib/sched/rules/recurrent/tests/mod.rs
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
mod recurrence_rules_by_val;
|
||||||
|
mod recurrence_rules_by_range;
|
||||||
|
mod recurrence_rules_by_many;
|
||||||
52
lib/sched/rules/recurrent/tests/recurrence_rules_by_many.rs
Normal file
52
lib/sched/rules/recurrent/tests/recurrence_rules_by_many.rs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
use chrono::{Datelike, Local, TimeZone, Utc};
|
||||||
|
|
||||||
|
use crate::sched::rules::{many, range, ruleset};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn every_day_at_12_and_15() {
|
||||||
|
let date = Local.with_ymd_and_hms(2024, 4, 7, 13, 15, 05).unwrap();
|
||||||
|
|
||||||
|
let mut rules = ruleset();
|
||||||
|
rules.hours_rule(many(vec![12, 15])).at_minute(0).at_second(0);
|
||||||
|
|
||||||
|
let next = rules.next_match_from(date).unwrap();
|
||||||
|
assert_eq!(next, Local.with_ymd_and_hms(2024, 4, 7, 15, 0, 0).unwrap());
|
||||||
|
|
||||||
|
let next = rules.next_match_from(next).unwrap();
|
||||||
|
assert_eq!(next, Local.with_ymd_and_hms(2024, 4, 8, 12, 0, 0).unwrap());
|
||||||
|
|
||||||
|
let next = rules.next_match_from(next).unwrap();
|
||||||
|
assert_eq!(next, Local.with_ymd_and_hms(2024, 4, 8, 15, 0, 0).unwrap());
|
||||||
|
|
||||||
|
let next = rules.next_match_from(next).unwrap();
|
||||||
|
assert_eq!(next, Local.with_ymd_and_hms(2024, 4, 9, 12, 0, 0).unwrap());
|
||||||
|
|
||||||
|
let next = rules.next_match_from(next).unwrap();
|
||||||
|
assert_eq!(next, Local.with_ymd_and_hms(2024, 4, 9, 15, 0, 0).unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn each_day_between_9_and_17_at_hour_start() {
|
||||||
|
// will start next day because it's already > 17:00:00
|
||||||
|
let date = Local.with_ymd_and_hms(2024, 4, 25, 19, 15, 05).unwrap();
|
||||||
|
|
||||||
|
let mut rules = ruleset();
|
||||||
|
rules.hours_rule(range(9, 17, 1)).at_minute(0).at_second(0);
|
||||||
|
|
||||||
|
let mut next = date.clone();
|
||||||
|
let initial_day = date.day() + 1;
|
||||||
|
|
||||||
|
for i in 0..18 {
|
||||||
|
let day = if i < 9 { initial_day } else { initial_day + 1 };
|
||||||
|
let addend = if i < 9 { i } else { i - 9 };
|
||||||
|
|
||||||
|
next = rules.next_match_from(next).unwrap();
|
||||||
|
|
||||||
|
println!("[{i}]: {:?}", next);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
next,
|
||||||
|
Local.with_ymd_and_hms(2024, 4, day, 9 + addend, 0, 0).unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
57
lib/sched/rules/recurrent/tests/recurrence_rules_by_range.rs
Normal file
57
lib/sched/rules/recurrent/tests/recurrence_rules_by_range.rs
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
use chrono::{Datelike, Local, TimeZone, Timelike};
|
||||||
|
|
||||||
|
use crate::sched::rules::{range, ruleset};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn between_10_and_20_seconds() {
|
||||||
|
let date = Local.with_ymd_and_hms(2024, 4, 7, 16, 15, 05).unwrap();
|
||||||
|
|
||||||
|
let mut rules = ruleset();
|
||||||
|
rules.seconds_rule(range(10, 20, 1));
|
||||||
|
|
||||||
|
let mut next = date.clone();
|
||||||
|
let initial_minute = date.minute();
|
||||||
|
|
||||||
|
for i in 0..20 {
|
||||||
|
let addend = if i < 11 { i } else { i - 10 - 1 };
|
||||||
|
let minute = if i < 11 {
|
||||||
|
initial_minute
|
||||||
|
} else {
|
||||||
|
initial_minute + 1
|
||||||
|
};
|
||||||
|
|
||||||
|
next = rules.next_match_from(next).unwrap();
|
||||||
|
println!("[{i}]: {:?}", next);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
next,
|
||||||
|
Local.with_ymd_and_hms(2024, 4, 7, 16, minute, 10 + addend).unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn each_day_between_9_and_17_at_hour_start() {
|
||||||
|
// will start next day because it's already > 17:00:00
|
||||||
|
let date = Local.with_ymd_and_hms(2024, 4, 25, 19, 15, 05).unwrap();
|
||||||
|
|
||||||
|
let mut rules = ruleset();
|
||||||
|
rules.hours_rule(range(9, 17, 1)).at_minute(0).at_second(0);
|
||||||
|
|
||||||
|
let mut next = date.clone();
|
||||||
|
let initial_day = date.day() + 1;
|
||||||
|
|
||||||
|
for i in 0..18 {
|
||||||
|
let day = if i < 9 { initial_day } else { initial_day + 1 };
|
||||||
|
let addend = if i < 9 { i } else { i - 9 };
|
||||||
|
|
||||||
|
next = rules.next_match_from(next).unwrap();
|
||||||
|
|
||||||
|
println!("[{i}]: {:?}", next);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
next,
|
||||||
|
Local.with_ymd_and_hms(2024, 4, day, 9 + addend, 0, 0).unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
179
lib/sched/rules/recurrent/tests/recurrence_rules_by_val.rs
Normal file
179
lib/sched/rules/recurrent/tests/recurrence_rules_by_val.rs
Normal file
@ -0,0 +1,179 @@
|
|||||||
|
use chrono::{Datelike, Local, TimeZone, Timelike, Weekday};
|
||||||
|
|
||||||
|
use crate::sched::rules::ruleset;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn at_second_1_of_each_minute() {
|
||||||
|
// we have passed the second 1 of the minute
|
||||||
|
// so it should go to the next minute
|
||||||
|
let date = Local.with_ymd_and_hms(2024, 4, 7, 16, 15, 05).unwrap();
|
||||||
|
|
||||||
|
let mut rules = ruleset();
|
||||||
|
rules.at_second(1);
|
||||||
|
|
||||||
|
let mut next = date.clone();
|
||||||
|
let initial_minute = date.minute();
|
||||||
|
|
||||||
|
for i in 0..10 {
|
||||||
|
next = rules.next_match_from(next).unwrap();
|
||||||
|
println!("next: {:?}", next);
|
||||||
|
// should match 16:16:01, 16:17:01, 16:18:01, ...
|
||||||
|
assert_eq!(
|
||||||
|
next,
|
||||||
|
Local.with_ymd_and_hms(2024, 4, 7, 16, initial_minute + i + 1, 01).unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn at_hour_1_of_each_day() {
|
||||||
|
// we have passed the hour 1 of the day
|
||||||
|
// so it should go to the next day
|
||||||
|
let date = Local.with_ymd_and_hms(2024, 4, 25, 16, 15, 05).unwrap();
|
||||||
|
|
||||||
|
let mut rules = ruleset();
|
||||||
|
rules.at_time(1, 0, 0);
|
||||||
|
|
||||||
|
let mut next = date.clone();
|
||||||
|
let mut initial_day = date.day() as i32;
|
||||||
|
let mut initial_month = date.month();
|
||||||
|
|
||||||
|
for i in 0..10 {
|
||||||
|
next = rules.next_match_from(next).unwrap();
|
||||||
|
println!("next: {:?}", next);
|
||||||
|
|
||||||
|
if initial_day + i + 1 == 31 {
|
||||||
|
initial_day = -i;
|
||||||
|
initial_month += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// should match 2024-04-08 01:00:00, 2024-04-09 01:00:00, 2024-04-10 01:00:00, ...
|
||||||
|
assert_eq!(
|
||||||
|
next,
|
||||||
|
Local
|
||||||
|
.with_ymd_and_hms(
|
||||||
|
2024,
|
||||||
|
initial_month,
|
||||||
|
(initial_day + i + 1) as u32,
|
||||||
|
01,
|
||||||
|
00,
|
||||||
|
00
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn each_1st_of_month() {
|
||||||
|
// we have passed the 1st of the month
|
||||||
|
// so it should go to the next month
|
||||||
|
let date = Local.with_ymd_and_hms(2024, 5, 7, 16, 15, 05).unwrap();
|
||||||
|
|
||||||
|
let mut rules = ruleset();
|
||||||
|
rules.on_day(1).at_time(0, 0, 0);
|
||||||
|
|
||||||
|
let mut next = date.clone();
|
||||||
|
let mut initial_month = date.month() as i32;
|
||||||
|
let mut initial_year = date.year();
|
||||||
|
|
||||||
|
for i in 0..10 {
|
||||||
|
next = rules.next_match_from(next).unwrap();
|
||||||
|
println!("next: {:?}", next);
|
||||||
|
|
||||||
|
if initial_month + i + 1 == 13 {
|
||||||
|
initial_month = -i;
|
||||||
|
initial_year += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// should match 2024-05-01 00:00:00, 2024-06-01 00:00:00, 2024-07-01 00:00:00, ...
|
||||||
|
assert_eq!(
|
||||||
|
next,
|
||||||
|
Local
|
||||||
|
.with_ymd_and_hms(initial_year, (initial_month + i + 1) as u32, 01, 00, 00, 00)
|
||||||
|
.unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn each_wednesday() {
|
||||||
|
// we have passed the Wednesday
|
||||||
|
// so it should go to the next Wednesday
|
||||||
|
let date = Local.with_ymd_and_hms(2024, 4, 7, 16, 15, 05).unwrap();
|
||||||
|
let mut next_wednesday = Local.with_ymd_and_hms(2024, 4, 10, 0, 0, 0).unwrap();
|
||||||
|
|
||||||
|
let mut rules = ruleset();
|
||||||
|
rules.on_dow(Weekday::Wed).at_time(0, 0, 0);
|
||||||
|
|
||||||
|
let mut next = date.clone();
|
||||||
|
|
||||||
|
for _ in 0..10 {
|
||||||
|
next = rules.next_match_from(next).unwrap();
|
||||||
|
println!("next: {:?}", next);
|
||||||
|
|
||||||
|
assert_eq!(next, next_wednesday);
|
||||||
|
next_wednesday = next_wednesday + chrono::Duration::days(7);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_31th_may_schedule_first_of_each_june() {
|
||||||
|
// we have passed the 31th of May
|
||||||
|
// so it should go to the 1st of June
|
||||||
|
let date = Local.with_ymd_and_hms(2024, 5, 31, 16, 15, 05).unwrap();
|
||||||
|
|
||||||
|
let mut rules = ruleset();
|
||||||
|
rules.in_month(6).on_day(1).at_time(0, 0, 0);
|
||||||
|
|
||||||
|
let mut next = date.clone();
|
||||||
|
|
||||||
|
let initial_year = date.year();
|
||||||
|
let initial_month = date.month();
|
||||||
|
|
||||||
|
for i in 0..10 {
|
||||||
|
next = rules.next_match_from(next).unwrap();
|
||||||
|
println!("next: {:?}", next);
|
||||||
|
|
||||||
|
// should match 2024-06-01 00:00:00, 2024-07-01 00:00:00, 2024-08-01 00:00:00, ...
|
||||||
|
assert_eq!(
|
||||||
|
next,
|
||||||
|
Local.with_ymd_and_hms(initial_year + i, initial_month + 1, 01, 00, 00, 00).unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn from_1st_may_schedule_first_of_each_june() {
|
||||||
|
let date = Local.with_ymd_and_hms(2024, 6, 1, 0, 0, 0).unwrap();
|
||||||
|
|
||||||
|
let mut rules = ruleset();
|
||||||
|
rules.in_month(6).on_day(1).at_time(0, 0, 0);
|
||||||
|
|
||||||
|
let mut next = date.clone();
|
||||||
|
let initial_year = date.year();
|
||||||
|
|
||||||
|
for i in 0..10 {
|
||||||
|
next = rules.next_match_from(next).unwrap();
|
||||||
|
println!("next: {:?}", next);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
next,
|
||||||
|
Local.with_ymd_and_hms(initial_year + i + 1, 06, 01, 00, 00, 00).unwrap()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn if_full_date_set_should_return_only_one_match_then_null() {
|
||||||
|
let date = Local.with_ymd_and_hms(2024, 5, 14, 19, 44, 15).unwrap();
|
||||||
|
|
||||||
|
let mut rules = ruleset();
|
||||||
|
rules.on_datetime(2024, 6, 1, 0, 0, 0);
|
||||||
|
|
||||||
|
let next = rules.next_match_from(date).unwrap();
|
||||||
|
assert_eq!(next, Local.with_ymd_and_hms(2024, 6, 1, 0, 0, 0).unwrap());
|
||||||
|
|
||||||
|
let next = rules.next_match_from(next);
|
||||||
|
assert!(next.is_none());
|
||||||
|
}
|
||||||
547
lib/sched/utils/cron_date.rs
Normal file
547
lib/sched/utils/cron_date.rs
Normal file
@ -0,0 +1,547 @@
|
|||||||
|
use chrono::{
|
||||||
|
DateTime, Datelike, Duration, Local, Months, NaiveDate, TimeZone, Timelike, Utc, Weekday,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 🧉 » `LoolDate`
|
||||||
|
/// --
|
||||||
|
///
|
||||||
|
/// wrapper around `chrono::DateTime` to provide more date manipulation methods
|
||||||
|
pub struct LoolDate<Tz: TimeZone> {
|
||||||
|
date: DateTime<Tz>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum TimeUnit {
|
||||||
|
Year,
|
||||||
|
Month,
|
||||||
|
Day,
|
||||||
|
Hour,
|
||||||
|
Minute,
|
||||||
|
Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<Tz: TimeZone> LoolDate<Tz> {
|
||||||
|
/// creates a new `LoolDate` from a `DateTime`
|
||||||
|
pub fn new(date: DateTime<Tz>) -> Self {
|
||||||
|
Self { date }
|
||||||
|
}
|
||||||
|
|
||||||
|
// creates a new `LoolDate` in the `Local` timezone
|
||||||
|
pub fn now() -> LoolDate<Local> {
|
||||||
|
LoolDate { date: Local::now() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// creates a new `LoolDate` in the `Utc` timezone
|
||||||
|
pub fn utc_now() -> LoolDate<Utc> {
|
||||||
|
LoolDate { date: Utc::now() }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns a clone of the inner `DateTime`
|
||||||
|
pub fn date(&self) -> DateTime<Tz> {
|
||||||
|
self.date.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// adds `1` year to the current date
|
||||||
|
pub fn add_year(&mut self) {
|
||||||
|
self.date = self.date.clone() + Months::new(12);
|
||||||
|
self.set_start_of(TimeUnit::Year);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// adds `1` month to the current date
|
||||||
|
pub fn add_month(&mut self) {
|
||||||
|
self.date = self.date.clone() + Months::new(1);
|
||||||
|
self.set_start_of(TimeUnit::Month);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// adds `1` day to the current date
|
||||||
|
pub fn add_day(&mut self) {
|
||||||
|
self.date = self.date.clone() + Duration::days(1);
|
||||||
|
self.set_start_of(TimeUnit::Day);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// adds `1` hour to the current date
|
||||||
|
pub fn add_hour(&mut self) {
|
||||||
|
self.date = self.date.clone() + Duration::hours(1);
|
||||||
|
self.set_start_of(TimeUnit::Hour);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// adds `1` minute to the current date
|
||||||
|
pub fn add_minute(&mut self) {
|
||||||
|
self.date = self.date.clone() + Duration::minutes(1);
|
||||||
|
self.set_start_of(TimeUnit::Minute);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// adds `1` second to the current date
|
||||||
|
pub fn add_second(&mut self) {
|
||||||
|
self.date = self.date.clone() + Duration::seconds(1);
|
||||||
|
self.set_start_of(TimeUnit::Second);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// subtracts `1` year from the current date
|
||||||
|
pub fn subs_year(&mut self) {
|
||||||
|
self.date = self.date.clone() - Months::new(12);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// subtracts `1` month from the current date
|
||||||
|
pub fn subs_month(&mut self) {
|
||||||
|
self.date = self.date.clone() - Months::new(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// subtracts `1` day from the current date
|
||||||
|
pub fn subs_day(&mut self) {
|
||||||
|
self.date = self.date.clone() - Duration::days(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// subtracts `1` hour from the current date
|
||||||
|
pub fn subs_hour(&mut self) {
|
||||||
|
self.date = self.date.clone() - Duration::hours(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// subtracts `1` minute from the current date
|
||||||
|
pub fn subs_minute(&mut self) {
|
||||||
|
self.date = self.date.clone() - Duration::minutes(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// subtracts `1` second from the current date
|
||||||
|
pub fn subs_second(&mut self) {
|
||||||
|
self.date = self.date.clone() - Duration::seconds(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns the day of the month starting from `1`
|
||||||
|
pub fn day(&self) -> u32 {
|
||||||
|
self.date.day()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns the day of the week
|
||||||
|
pub fn weekday(&self) -> Weekday {
|
||||||
|
self.date.weekday()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns the day of the week starting from `0` (monday)
|
||||||
|
pub fn weekday_from_monday(&self) -> u32 {
|
||||||
|
self.date.weekday().num_days_from_monday()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns the day of the week starting from `0` (sunday)
|
||||||
|
pub fn weekday_from_sunday(&self) -> u32 {
|
||||||
|
self.date.weekday().num_days_from_sunday()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns the month starting from 1
|
||||||
|
pub fn month(&self) -> u32 {
|
||||||
|
self.date.month()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns the year number in the [calendar date](./naive/struct.NaiveDate.html#calendar-date).
|
||||||
|
pub fn year(&self) -> i32 {
|
||||||
|
self.date.year()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns the hour `(0..23)`
|
||||||
|
pub fn hour(&self) -> u32 {
|
||||||
|
self.date.hour()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns the minute `(0..59)`
|
||||||
|
pub fn minute(&self) -> u32 {
|
||||||
|
self.date.minute()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns the second number `(0..59)`
|
||||||
|
pub fn second(&self) -> u32 {
|
||||||
|
self.date.second()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns the number of milliseconds since the last second boundary
|
||||||
|
///
|
||||||
|
/// In event of a [leap second](https://en.wikipedia.org/wiki/Leap_second) this may exceed
|
||||||
|
/// `999`.
|
||||||
|
pub fn millis(&self) -> u64 {
|
||||||
|
self.date.timestamp_subsec_millis() as u64
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the number of microseconds since the last second boundary.
|
||||||
|
///
|
||||||
|
/// In event of a [leap second](https://en.wikipedia.org/wiki/Leap_second) this may exceed
|
||||||
|
/// `999999`.
|
||||||
|
pub fn micros(&self) -> u32 {
|
||||||
|
self.date.timestamp_subsec_micros()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the number of nanoseconds since the last second boundary.
|
||||||
|
///
|
||||||
|
/// In event of a [leap second](https://en.wikipedia.org/wiki/Leap_second) this may exceed
|
||||||
|
/// `999999999`.
|
||||||
|
pub fn nanos(&self) -> u32 {
|
||||||
|
self.date.timestamp_subsec_nanos()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// sets the nanoseconds since the last second change
|
||||||
|
///
|
||||||
|
/// values greater than `2000,000,000` will be clamped to `1999,999,999`
|
||||||
|
pub fn set_nanos(&mut self, nanos: u32) {
|
||||||
|
// avoid `whith_nanosecond` returning None for > 2_000_000_000 values
|
||||||
|
// so we can safely unwrap the result
|
||||||
|
let nanos = if nanos > 2_000_000_000 { 1_999_999_999 } else { nanos };
|
||||||
|
self.date = self.date.with_nanosecond(nanos).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// sets the microseconds since the last second change
|
||||||
|
///
|
||||||
|
/// values greater than `2,000,000` will be clamped to `1,999,999`
|
||||||
|
pub fn set_micros(&mut self, micros: u32) {
|
||||||
|
let micros = if micros > 2_000_000 { 1_999_999 } else { micros };
|
||||||
|
self.date = self.date.with_nanosecond(micros as u32 * 1_000).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// sets the milliseconds since the last second change
|
||||||
|
///
|
||||||
|
/// values greater than `2,000` will be clamped to `1,999`
|
||||||
|
pub fn set_millis(&mut self, millis: u64) {
|
||||||
|
let millis = if millis > 2_000 { 1_999 } else { millis };
|
||||||
|
self.date = self.date.with_nanosecond(millis as u32 * 1_000_000).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// sets the second number of the date
|
||||||
|
///
|
||||||
|
/// values >= `60` will be clamped to `0`
|
||||||
|
pub fn set_second(&mut self, second: u32) {
|
||||||
|
let second = if second >= 60 { 0 } else { second };
|
||||||
|
self.date = self.date.with_second(second).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// sets the minute number of the date
|
||||||
|
///
|
||||||
|
/// values >= `60` will be clamped to `0`
|
||||||
|
pub fn set_minute(&mut self, minute: u32) {
|
||||||
|
let minute = if minute >= 60 { 0 } else { minute };
|
||||||
|
self.date = self.date.with_minute(minute).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// sets the hour number of the date `(0..23)`
|
||||||
|
///
|
||||||
|
/// values >= `24` will be clamped to `0`
|
||||||
|
pub fn set_hour(&mut self, hour: u32) {
|
||||||
|
let hour = if hour >= 24 { 0 } else { hour };
|
||||||
|
self.date = self.date.with_hour(hour).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// sets the hour, minute and second of the date at once
|
||||||
|
///
|
||||||
|
/// - `hour` must be in the range `(0..23)`
|
||||||
|
/// - `minute` must be in the range `(0..59)`
|
||||||
|
/// - `second` must be in the range `(0..59)`
|
||||||
|
///
|
||||||
|
/// values greater than the maximum will be clamped (see `set_hour`, `set_minute`, `set_second`)
|
||||||
|
/// documentation for more information
|
||||||
|
pub fn set_hms(&mut self, hour: u32, minute: u32, second: u32) {
|
||||||
|
self.set_hour(hour);
|
||||||
|
self.set_minute(minute);
|
||||||
|
self.set_second(second);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// sets the day of the month `(1..31)`
|
||||||
|
///
|
||||||
|
/// values greater than the maximum day of the month will be clamped to the last day of the
|
||||||
|
/// month, depending on the current year and month and taking leap years into account.
|
||||||
|
pub fn set_day(&mut self, day: u32) {
|
||||||
|
let day = if day == 0 { 1 } else { day };
|
||||||
|
let days_in_month = get_days_from_month(self.date.year(), self.date.month());
|
||||||
|
|
||||||
|
if day > days_in_month {
|
||||||
|
self.date = self.date.with_day(days_in_month).unwrap();
|
||||||
|
} else {
|
||||||
|
self.date = self.date.with_day(day).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// sets the month of the year `(1..12)`
|
||||||
|
///
|
||||||
|
/// - values greater than the maximum month will be clamped to `12`
|
||||||
|
/// - values less than `1` will be clamped to `1`
|
||||||
|
///
|
||||||
|
/// if the current day is greater than the last day of the new month, the day will be clamped
|
||||||
|
/// to the last day of the month, depending on the current year and month and taking leap years
|
||||||
|
/// into account. So, be aware that changing the month may imply a change in the day.
|
||||||
|
/// This is done to avoid invalid dates like `2024-02-30` which would cause an error in the
|
||||||
|
/// `chrono` library.
|
||||||
|
pub fn set_month(&mut self, month: u32) {
|
||||||
|
let month = if month > 12 {
|
||||||
|
12
|
||||||
|
} else if month == 0 {
|
||||||
|
1
|
||||||
|
} else {
|
||||||
|
month
|
||||||
|
};
|
||||||
|
|
||||||
|
let day = self.date.day();
|
||||||
|
let days_in_month = get_days_from_month(self.date.year(), month);
|
||||||
|
|
||||||
|
if day > days_in_month {
|
||||||
|
self.date = self.date.with_day(days_in_month).unwrap();
|
||||||
|
self.date = self.date.with_month(month).unwrap();
|
||||||
|
} else {
|
||||||
|
self.date = self.date.with_month(month).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// sets the month and day of the date at once
|
||||||
|
///
|
||||||
|
/// check the `set_month` and `set_day` documentation for more information about the clamping
|
||||||
|
/// behavior.
|
||||||
|
pub fn set_md(&mut self, month: u32, day: u32) {
|
||||||
|
self.set_month(month);
|
||||||
|
self.set_day(day);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// sets the year of the date
|
||||||
|
///
|
||||||
|
/// **warning**: changing the year may imply a change in the day.
|
||||||
|
/// For example, if the current date is `2024-02-29` and you set the year to `2023`, the date
|
||||||
|
/// date `2023-02-29` will be invalid, because `2023` is not a leap year. In this case, the day
|
||||||
|
/// will be clamped to `2023-02-28`.
|
||||||
|
///
|
||||||
|
/// check the `set_day` documentation for more information about the clamping behavior.
|
||||||
|
pub fn set_year(&mut self, year: i32) {
|
||||||
|
let month = self.date.month();
|
||||||
|
let day = self.date.day();
|
||||||
|
let days_in_month = get_days_from_month(year, month);
|
||||||
|
|
||||||
|
if day > days_in_month {
|
||||||
|
self.date = self.date.with_day(days_in_month).unwrap();
|
||||||
|
self.date = self.date.with_year(year).unwrap();
|
||||||
|
} else {
|
||||||
|
self.date = self.date.with_year(year).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// sets the year, month and day of the date at once
|
||||||
|
///
|
||||||
|
/// check the `set_year`, `set_month` and `set_day` documentation for more information about the
|
||||||
|
/// clamping behavior.
|
||||||
|
pub fn set_ymd(&mut self, year: i32, month: u32, day: u32) {
|
||||||
|
self.set_year(year);
|
||||||
|
self.set_month(month);
|
||||||
|
self.set_day(day);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns true when the current date is the last day of the month.
|
||||||
|
pub fn is_last_day_of_month(&self) -> bool {
|
||||||
|
self.date.day() == get_days_from_month(self.date.year(), self.date.month())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// returns true when the current weekday is the last occurrence of this weekday
|
||||||
|
/// for the present month.
|
||||||
|
pub fn is_last_weekday_of_month(&self) -> bool {
|
||||||
|
// check this by adding 7 days to the current date and seeing if it's
|
||||||
|
// a different month
|
||||||
|
let next_weekday = self.date.clone() + Duration::days(7);
|
||||||
|
self.date.month() != next_weekday.month()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_start_of(&mut self, unit: TimeUnit) {
|
||||||
|
match unit {
|
||||||
|
TimeUnit::Year => {
|
||||||
|
self.set_month(1);
|
||||||
|
self.set_day(1);
|
||||||
|
self.set_hour(0);
|
||||||
|
self.set_minute(0);
|
||||||
|
self.set_second(0);
|
||||||
|
self.set_nanos(0);
|
||||||
|
},
|
||||||
|
TimeUnit::Month => {
|
||||||
|
self.set_day(1);
|
||||||
|
self.set_hour(0);
|
||||||
|
self.set_minute(0);
|
||||||
|
self.set_second(0);
|
||||||
|
self.set_nanos(0);
|
||||||
|
},
|
||||||
|
TimeUnit::Day => {
|
||||||
|
self.set_hour(0);
|
||||||
|
self.set_minute(0);
|
||||||
|
self.set_second(0);
|
||||||
|
self.set_nanos(0);
|
||||||
|
},
|
||||||
|
TimeUnit::Hour => {
|
||||||
|
self.set_minute(0);
|
||||||
|
self.set_second(0);
|
||||||
|
self.set_nanos(0);
|
||||||
|
},
|
||||||
|
TimeUnit::Minute => {
|
||||||
|
self.set_second(0);
|
||||||
|
self.set_nanos(0);
|
||||||
|
},
|
||||||
|
TimeUnit::Second => {
|
||||||
|
self.set_nanos(0);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_days_from_month(year: i32, month: u32) -> u32 {
|
||||||
|
NaiveDate::from_ymd_opt(
|
||||||
|
match month {
|
||||||
|
12 => year + 1,
|
||||||
|
_ => year,
|
||||||
|
},
|
||||||
|
match month {
|
||||||
|
12 => 1,
|
||||||
|
_ => month + 1,
|
||||||
|
},
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.signed_duration_since(NaiveDate::from_ymd_opt(year, month, 1).unwrap())
|
||||||
|
.num_days() as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use {super::*, chrono::Utc};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_get_days_from_month() {
|
||||||
|
assert_eq!(get_days_from_month(2024, 1), 31);
|
||||||
|
assert_eq!(get_days_from_month(2024, 2), 29);
|
||||||
|
assert_eq!(get_days_from_month(2024, 3), 31);
|
||||||
|
assert_eq!(get_days_from_month(2024, 4), 30);
|
||||||
|
assert_eq!(get_days_from_month(2024, 5), 31);
|
||||||
|
assert_eq!(get_days_from_month(2024, 6), 30);
|
||||||
|
assert_eq!(get_days_from_month(2024, 7), 31);
|
||||||
|
assert_eq!(get_days_from_month(2024, 8), 31);
|
||||||
|
assert_eq!(get_days_from_month(2024, 9), 30);
|
||||||
|
assert_eq!(get_days_from_month(2024, 10), 31);
|
||||||
|
assert_eq!(get_days_from_month(2024, 11), 30);
|
||||||
|
assert_eq!(get_days_from_month(2024, 12), 31);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_set_day() {
|
||||||
|
let mut date = LoolDate::new(Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap());
|
||||||
|
date.set_day(31);
|
||||||
|
assert_eq!(date.day(), 31);
|
||||||
|
assert_eq!(date.month(), 1);
|
||||||
|
assert_eq!(date.year(), 2024);
|
||||||
|
|
||||||
|
date.set_day(1);
|
||||||
|
assert_eq!(date.day(), 1);
|
||||||
|
assert_eq!(date.month(), 1);
|
||||||
|
assert_eq!(date.year(), 2024);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_set_day_invalid_values() {
|
||||||
|
let mut date = LoolDate::new(Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap());
|
||||||
|
date.set_day(35);
|
||||||
|
assert_eq!(date.day(), 31);
|
||||||
|
assert_eq!(date.month(), 1);
|
||||||
|
assert_eq!(date.year(), 2024);
|
||||||
|
|
||||||
|
date.set_day(0);
|
||||||
|
assert_eq!(date.day(), 1);
|
||||||
|
assert_eq!(date.month(), 1);
|
||||||
|
assert_eq!(date.year(), 2024);
|
||||||
|
|
||||||
|
let mut date = LoolDate::new(Utc.with_ymd_and_hms(2024, 2, 1, 0, 0, 0).unwrap());
|
||||||
|
date.set_day(30);
|
||||||
|
assert_eq!(date.day(), 29);
|
||||||
|
assert_eq!(date.month(), 2);
|
||||||
|
assert_eq!(date.year(), 2024);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_set_month() {
|
||||||
|
let mut date = LoolDate::new(Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap());
|
||||||
|
date.set_month(12);
|
||||||
|
assert_eq!(date.month(), 12);
|
||||||
|
|
||||||
|
date.set_month(13);
|
||||||
|
assert_eq!(date.month(), 12);
|
||||||
|
|
||||||
|
date.set_month(1);
|
||||||
|
assert_eq!(date.month(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_set_month_invalid_values() {
|
||||||
|
let mut date = LoolDate::new(Utc.with_ymd_and_hms(2024, 1, 10, 0, 0, 0).unwrap());
|
||||||
|
date.set_month(13);
|
||||||
|
assert_eq!(date.month(), 12);
|
||||||
|
assert_eq!(date.year(), 2024);
|
||||||
|
assert_eq!(date.day(), 10);
|
||||||
|
|
||||||
|
date.set_month(0);
|
||||||
|
assert_eq!(date.month(), 1);
|
||||||
|
assert_eq!(date.year(), 2024);
|
||||||
|
assert_eq!(date.day(), 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_set_month_leap_years() {
|
||||||
|
let mut date = LoolDate::new(Utc.with_ymd_and_hms(2023, 1, 29, 0, 0, 0).unwrap());
|
||||||
|
date.set_month(2);
|
||||||
|
assert_eq!(date.month(), 2);
|
||||||
|
assert_eq!(date.year(), 2023);
|
||||||
|
assert_eq!(date.day(), 28); // 2023 is not a leap year so, can't keep the original 29
|
||||||
|
|
||||||
|
let mut date = LoolDate::new(Utc.with_ymd_and_hms(2024, 1, 29, 0, 0, 0).unwrap());
|
||||||
|
date.set_month(2);
|
||||||
|
assert_eq!(date.month(), 2);
|
||||||
|
assert_eq!(date.year(), 2024);
|
||||||
|
assert_eq!(date.day(), 29); // 2024 is a leap year so, 29 is a valid day
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_set_month_month_overflow() {
|
||||||
|
let mut date = LoolDate::new(Utc.with_ymd_and_hms(2024, 1, 31, 0, 0, 0).unwrap());
|
||||||
|
date.set_month(4);
|
||||||
|
assert_eq!(date.month(), 4);
|
||||||
|
assert_eq!(date.year(), 2024);
|
||||||
|
assert_eq!(date.day(), 30); // since april has 30 days, it cant keep the original 31
|
||||||
|
|
||||||
|
let mut date = LoolDate::new(Utc.with_ymd_and_hms(2024, 1, 31, 0, 0, 0).unwrap());
|
||||||
|
date.set_month(8);
|
||||||
|
assert_eq!(date.month(), 8);
|
||||||
|
assert_eq!(date.year(), 2024);
|
||||||
|
assert_eq!(date.day(), 31); // august has 31 days, so it can keep the original 31
|
||||||
|
|
||||||
|
let mut date = LoolDate::new(Utc.with_ymd_and_hms(2024, 1, 31, 0, 0, 0).unwrap());
|
||||||
|
date.set_month(11);
|
||||||
|
assert_eq!(date.month(), 11);
|
||||||
|
assert_eq!(date.year(), 2024);
|
||||||
|
assert_eq!(date.day(), 30); // november has 30 days, so it cant keep the original 31
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_set_year() {
|
||||||
|
let mut date = LoolDate::new(Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap());
|
||||||
|
date.set_year(2021);
|
||||||
|
assert_eq!(date.year(), 2021);
|
||||||
|
assert_eq!(date.month(), 1);
|
||||||
|
assert_eq!(date.day(), 1);
|
||||||
|
|
||||||
|
date.set_year(2050);
|
||||||
|
assert_eq!(date.year(), 2050);
|
||||||
|
assert_eq!(date.month(), 1);
|
||||||
|
assert_eq!(date.day(), 1);
|
||||||
|
|
||||||
|
date.set_year(-5);
|
||||||
|
assert_eq!(date.year(), -5);
|
||||||
|
assert_eq!(date.month(), 1);
|
||||||
|
assert_eq!(date.day(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_set_year_leap_years() {
|
||||||
|
let mut date = LoolDate::new(Utc.with_ymd_and_hms(2024, 2, 29, 0, 0, 0).unwrap());
|
||||||
|
date.set_year(2023);
|
||||||
|
assert_eq!(date.year(), 2023);
|
||||||
|
assert_eq!(date.month(), 2);
|
||||||
|
assert_eq!(date.day(), 28); // 2023 is not a leap year so, can't keep the original 29
|
||||||
|
|
||||||
|
let mut date = LoolDate::new(Utc.with_ymd_and_hms(2023, 2, 28, 0, 0, 0).unwrap());
|
||||||
|
date.set_year(2024);
|
||||||
|
assert_eq!(date.year(), 2024);
|
||||||
|
assert_eq!(date.month(), 2);
|
||||||
|
assert_eq!(date.day(), 28);
|
||||||
|
}
|
||||||
|
}
|
||||||
117
lib/sched/utils/mod.rs
Normal file
117
lib/sched/utils/mod.rs
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
use eyre::{ensure, eyre, Result};
|
||||||
|
|
||||||
|
pub mod cron_date;
|
||||||
|
|
||||||
|
const NO_SIGN_ERR: &str = "invalid timezone offset";
|
||||||
|
const INVALID_OFFSET_ERR: &str = "invalid timezone offset (format should be `+hh{{:mm}}?`)";
|
||||||
|
const INVALID_OFFSET_HS_ERR: &str = "invalid timezone offset (hour should be between 0 and 14)";
|
||||||
|
const INVALID_OFFSET_MIN_ERR: &str = "invalid timezone offset (minute should be between 0 and 59)";
|
||||||
|
|
||||||
|
/// 🧉 » converts `hours` and `minutes` durations to total seconds
|
||||||
|
///
|
||||||
|
/// e.g. `h=1, m=30` should return `5400`
|
||||||
|
///
|
||||||
|
/// meaning `1 hour and 30 minutes` is `5400` seconds
|
||||||
|
pub fn hm_to_s(h: i32, m: i32) -> i32 {
|
||||||
|
h * 3600 + m * 60
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🧉 » converts timezone offset to seconds
|
||||||
|
///
|
||||||
|
/// eg:
|
||||||
|
/// - `+01:00` -> `3600`
|
||||||
|
/// - `-03:00` -> `-10800`
|
||||||
|
/// - `+03` -> `10800`
|
||||||
|
/// - `+00:00` -> `0`
|
||||||
|
/// - `-03:30` -> `-12600`
|
||||||
|
///
|
||||||
|
/// returns an error if the offset is invalid or badly formatted
|
||||||
|
pub fn tz_to_s(offset: &str) -> Result<i32> {
|
||||||
|
// if it doesn't start with '+' or '-', it's invalid
|
||||||
|
ensure!(
|
||||||
|
offset.starts_with('+') || offset.starts_with('-'),
|
||||||
|
NO_SIGN_ERR
|
||||||
|
);
|
||||||
|
|
||||||
|
let sign = if offset.starts_with('+') { 1 } else { -1 };
|
||||||
|
let parts: Vec<&str> = offset[1..].split(':').collect();
|
||||||
|
|
||||||
|
// it should have at least one part and at most two parts
|
||||||
|
ensure!(!parts.is_empty(), INVALID_OFFSET_ERR);
|
||||||
|
ensure!(parts.len() <= 2, INVALID_OFFSET_ERR);
|
||||||
|
|
||||||
|
let hours = parts[0].parse::<u32>().map_err(|_| eyre!(INVALID_OFFSET_ERR))?;
|
||||||
|
|
||||||
|
let minutes = if parts.len() == 2 {
|
||||||
|
parts[1].parse::<u32>().map_err(|_| eyre!(INVALID_OFFSET_ERR))?
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
};
|
||||||
|
|
||||||
|
// offset hours cannot be greater than 14, minutes cannot be greater than 59 and
|
||||||
|
// seconds cannot be greater than 59
|
||||||
|
ensure!(hours <= 14, INVALID_OFFSET_HS_ERR);
|
||||||
|
ensure!(minutes <= 59, INVALID_OFFSET_MIN_ERR);
|
||||||
|
|
||||||
|
Ok(sign * hm_to_s(hours as i32, minutes as i32))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use eyre::{set_hook, DefaultHandler};
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn setup_eyre() {
|
||||||
|
let _ = set_hook(Box::new(DefaultHandler::default_with));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_h_m_to_seconds() {
|
||||||
|
assert_eq!(hm_to_s(1, 30), 5400);
|
||||||
|
assert_eq!(hm_to_s(2, 0), 7200);
|
||||||
|
assert_eq!(hm_to_s(3, 0), 10800);
|
||||||
|
assert_eq!(hm_to_s(0, 0), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_timezone_offset_to_seconds() -> Result<()> {
|
||||||
|
assert_eq!(tz_to_s("+01:00")?, 3600);
|
||||||
|
assert_eq!(tz_to_s("-03:00")?, -10800);
|
||||||
|
assert_eq!(tz_to_s("+03")?, 10800);
|
||||||
|
assert_eq!(tz_to_s("+00:00")?, 0);
|
||||||
|
assert_eq!(tz_to_s("-00:00")?, 0);
|
||||||
|
assert_eq!(tz_to_s("-3")?, -10800);
|
||||||
|
assert_eq!(tz_to_s("-3:30")?, -12600);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_timezone_offset_to_seconds_missing_sign_err() {
|
||||||
|
setup_eyre();
|
||||||
|
let err = tz_to_s("01:00").unwrap_err().to_string();
|
||||||
|
assert_eq!(err, NO_SIGN_ERR);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_timezone_offset_to_seconds_invalid_format_errs() {
|
||||||
|
setup_eyre();
|
||||||
|
let err = tz_to_s("+01:00:00").unwrap_err().to_string();
|
||||||
|
assert_eq!(err, INVALID_OFFSET_ERR);
|
||||||
|
|
||||||
|
let err = tz_to_s("+01:").unwrap_err().to_string();
|
||||||
|
assert_eq!(err, INVALID_OFFSET_ERR);
|
||||||
|
|
||||||
|
let err = tz_to_s("--01:00").unwrap_err().to_string();
|
||||||
|
assert_eq!(err, INVALID_OFFSET_ERR);
|
||||||
|
|
||||||
|
let err = tz_to_s("+01:-01").unwrap_err().to_string();
|
||||||
|
assert_eq!(err, INVALID_OFFSET_ERR);
|
||||||
|
|
||||||
|
let err = tz_to_s("+15:01").unwrap_err().to_string();
|
||||||
|
assert_eq!(err, INVALID_OFFSET_HS_ERR);
|
||||||
|
|
||||||
|
let err = tz_to_s("+01:60").unwrap_err().to_string();
|
||||||
|
assert_eq!(err, INVALID_OFFSET_MIN_ERR);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,11 +12,38 @@
|
|||||||
"lucodear-icons.activeIconPack": "rust_ferris",
|
"lucodear-icons.activeIconPack": "rust_ferris",
|
||||||
"lucodear-icons.folders.associations": {
|
"lucodear-icons.folders.associations": {
|
||||||
".cargo": "rust",
|
".cargo": "rust",
|
||||||
"stylize": "theme"
|
"stylize": "theme",
|
||||||
|
"ruleset": "rules",
|
||||||
|
"recurrent": "generator"
|
||||||
},
|
},
|
||||||
"lucodear-icons.files.associations": {
|
"lucodear-icons.files.associations": {
|
||||||
|
|
||||||
},
|
},
|
||||||
"rust-analyzer.cargo.features": "all",
|
"rust-analyzer.cargo.features": "all",
|
||||||
|
"files.exclude": {
|
||||||
|
"**/.git": false,
|
||||||
|
"**/.svn": false,
|
||||||
|
"**/.hg": false,
|
||||||
|
"**/CVS": false,
|
||||||
|
"**/.DS_Store": false,
|
||||||
|
"**/Thumbs.db": true,
|
||||||
|
"**/.classpath": false,
|
||||||
|
"**/.factorypath": true,
|
||||||
|
"**/.project": false,
|
||||||
|
"**/.settings": true,
|
||||||
|
"*.ids": false,
|
||||||
|
"*.iml": false,
|
||||||
|
"*.ipr": false,
|
||||||
|
"*.iws": false,
|
||||||
|
"*.orig": false,
|
||||||
|
".gradle": false,
|
||||||
|
".idea/": false,
|
||||||
|
".out/": false,
|
||||||
|
".settings": false,
|
||||||
|
".vscode": false,
|
||||||
|
"bin/": false,
|
||||||
|
"build/": false,
|
||||||
|
"out/": false
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user