feat(sched): recurrence ruleset

This commit is contained in:
Lucas Colombo 2024-04-07 22:57:02 -03:00
parent 52b6ead7b3
commit 9b0faace04
Signed by: lucas
GPG Key ID: EF34786CFEFFAE35
21 changed files with 1899 additions and 4 deletions

14
.github/img/logo-tokio-sched.svg vendored Normal file
View 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
View File

@ -2,12 +2,101 @@
# It is not intended for manual editing.
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]]
name = "bitflags"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "eyre"
version = "0.6.12"
@ -18,12 +107,56 @@ dependencies = [
"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]]
name = "indenter"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "log"
version = "0.4.21"
@ -35,8 +168,44 @@ name = "lool"
version = "0.0.5"
dependencies = [
"bitflags",
"chrono",
"eyre",
"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]]
@ -44,3 +213,180 @@ name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
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"

View File

@ -23,9 +23,21 @@ path = "lib/lib.rs"
cli-stylize = ["dep:bitflags"]
logger = ["dep:log"]
macros = []
sched = ["dep:chrono"]
sched-tokio = ["dep:tokio", "tokio?/time", "tokio?/rt"]
sched-rule-recurrent = []
sched-rule-cron = []
[dependencies]
bitflags = { version = "2.5.0", optional = true }
log = { version = "0.4.21", optional = true }
# default
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"

View File

@ -16,3 +16,4 @@
- [x] [cli/stylize](lib/cli/stylize)
- [x] [logging](lib/logger)
- [x] [macros](lib/macros)
- [x] [tokio/sched](lib/tokio/sched)

View File

@ -1,5 +1,8 @@
pub mod cli;
#[cfg(feature = "sched")]
pub mod sched;
#[cfg(feature = "logger")]
pub mod logger;

19
lib/lutest/mod.rs Normal file
View 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
View 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
View File

@ -0,0 +1,4 @@
mod rules;
pub mod utils;
pub use rules::*;

51
lib/sched/rules.rs Normal file
View 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
View File

@ -0,0 +1,2 @@
// TODO: cron based scheduling

View 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};

View 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)
}

View 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)
}
}

View 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
}
}

View File

@ -0,0 +1,3 @@
mod recurrence_rules_by_val;
mod recurrence_rules_by_range;
mod recurrence_rules_by_many;

View 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()
);
}
}

View 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()
);
}
}

View 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());
}

View 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
View 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);
}
}

View File

@ -12,11 +12,38 @@
"lucodear-icons.activeIconPack": "rust_ferris",
"lucodear-icons.folders.associations": {
".cargo": "rust",
"stylize": "theme"
"stylize": "theme",
"ruleset": "rules",
"recurrent": "generator"
},
"lucodear-icons.files.associations": {
},
"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
},
}
}