From cd7a1ffca78e8c9a81e4084b9bf91e9a6824980f Mon Sep 17 00:00:00 2001 From: Hexagon Date: Thu, 9 Nov 2023 19:44:47 +0000 Subject: [PATCH] Perf: Short circuit search loop --- Cargo.toml | 7 +- LICENSE.md | 23 ++- README.md | 135 ++++++++----- benches/croner_bench.rs | 20 ++ benches/deno/deno_bench.ts | 10 + src/lib.rs | 384 ++++++++++++++++++++++++++----------- src/pattern.rs | 41 ++++ 7 files changed, 446 insertions(+), 174 deletions(-) create mode 100644 benches/croner_bench.rs create mode 100644 benches/deno/deno_bench.ts diff --git a/Cargo.toml b/Cargo.toml index b1b5207..69073e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "croner" -version = "0.0.5" +version = "0.0.6" edition = "2021" license = "MIT" description = "A cron parser and job scheduler library for Rust" @@ -20,3 +20,8 @@ chrono = "0.4" [dev-dependencies] chrono-tz = "0.8.4" +criterion = "0.3" + +[[bench]] +name = "croner_bench" +harness = false \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md index a6a77aa..bf21578 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -2,20 +2,19 @@ The MIT License (MIT) Copyright (c) 2023 Hexagon -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 17c0336..d15abdf 100644 --- a/README.md +++ b/README.md @@ -1,32 +1,45 @@ # Croner -Croner is a lightweight, efficient Rust library for parsing and handling cron patterns. Designed with simplicity and performance in mind, it provides Rust developers with a tool to schedule tasks efficiently, following the familiar cron syntax. +Croner is a lightweight, efficient Rust library for parsing and handling cron +patterns. Designed with simplicity and performance in mind, it provides Rust +developers with a tool to schedule tasks efficiently, following the familiar +cron syntax. -This is the **Work in progress** Rust flavor of the popular JavaScript/TypeScript cron scheduler [croner](https://github.com/hexagon/croner). +This is the **Work in progress** Rust flavor of the popular +JavaScript/TypeScript cron scheduler +[croner](https://github.com/hexagon/croner). ## Features -* Parse and evaluate [cron](https://en.wikipedia.org/wiki/Cron#CRON_expression) expressions to calculate upcoming execution times. -* Supports extended Vixie-cron patterns with additional specifiers such as `L` for the last day and weekday of the month, and `#` for the nth weekday of the month. -* Evaulate cron extpressions across different time zones. -* Compatible with `chrono` and (optionally) `chrono-tz`. -* Includes overrun protection to prevent jobs from overlapping in a concurrent environment. -* Robust error handling. -* Control execution flow with the ability to pause, resume, or stop scheduled tasks. -* Operates in-memory without the need for persistent storage or configuration files. -* Highly optimized method of finding future/past matches. +- Parse and evaluate [cron](https://en.wikipedia.org/wiki/Cron#CRON_expression) + expressions to calculate upcoming execution times. +- Supports extended Vixie-cron patterns with additional specifiers such as `L` + for the last day and weekday of the month, and `#` for the nth weekday of the + month. +- Evaulate cron extpressions across different time zones. +- Compatible with `chrono` and (optionally) `chrono-tz`. +- Includes overrun protection to prevent jobs from overlapping in a concurrent + environment. +- Robust error handling. +- Control execution flow with the ability to pause, resume, or stop scheduled + tasks. +- Operates in-memory without the need for persistent storage or configuration + files. +- Highly optimized method of finding future/past matches. ## Getting Started ### Prerequisites -Ensure you have Rust installed on your machine. If not, you can get it from [the official Rust website](https://www.rust-lang.org/). +Ensure you have Rust installed on your machine. If not, you can get it from +[the official Rust website](https://www.rust-lang.org/). ### Installation Add `croner` to your `Cargo.toml` dependencies: -**Please note that croner for Rust is work in progress, and not production ready** +**Please note that croner for Rust is work in progress, and not production +ready** ```toml [dependencies] @@ -35,7 +48,8 @@ croner = "0.0.4" # Adjust the version as necessary ### Usage -Here's a quick example to get you started with matching current time, and finding the next occurrence. `is_time_matching` takes a `chrono` `DateTime`: +Here's a quick example to get you started with matching current time, and +finding the next occurrence. `is_time_matching` takes a `chrono` `DateTime`: ```rust use croner::Cron; @@ -61,7 +75,8 @@ fn main() { } ``` -To match against a non local timezone, croner supports zoned chrono DateTime's `DateTime`. To use a named time zone, you can utilize the `chrono-tz` crate. +To match against a non local timezone, croner supports zoned chrono DateTime's +`DateTime`. To use a named time zone, you can utilize the `chrono-tz` crate. ```rust use croner::Cron; @@ -91,7 +106,8 @@ fn main() { ### Pattern -The expressions used by Croner are very similar to those of Vixie Cron, but with a few additions and changes as outlined below: +The expressions used by Croner are very similar to those of Vixie Cron, but with +a few additions and changes as outlined below: ```javascript // ┌──────────────── (optional) second (0 - 59) @@ -99,48 +115,61 @@ The expressions used by Croner are very similar to those of Vixie Cron, but with // │ │ ┌──────────── hour (0 - 23) // │ │ │ ┌────────── day of month (1 - 31) // │ │ │ │ ┌──────── month (1 - 12, JAN-DEC) -// │ │ │ │ │ ┌────── day of week (0 - 6, SUN-Mon) +// │ │ │ │ │ ┌────── day of week (0 - 6, SUN-Mon) // │ │ │ │ │ │ (0 to 6 are Sunday to Saturday; 7 is Sunday, the same as 0) // │ │ │ │ │ │ // * * * * * * ``` -* Croner expressions have the following additional modifiers: - - *?*: In the Rust version of croner, a questionmark behaves just as *, to allow for legacy cron patterns to be used. - - *L*: The letter 'L' can be used in the day of the month field to indicate the last day of the month. When used in the day of the week field in conjunction with the # character, it denotes the last specific weekday of the month. For example, `5#L` represents the last Friday of the month. - - *#*: The # character specifies the "nth" occurrence of a particular day within a month. For example, supplying - `5#2` in the day of week field signifies the second Friday of the month. This can be combined with ranges and supports day names. For instance, MON-FRI#2 would match the Monday through Friday of the second week of the month. - -* Croner allows you to pass a JavaScript Date object or an ISO 8601 formatted string as a pattern. The scheduled function will trigger at the specified date/time and only once. If you use a timezone different from the local timezone, you should pass the ISO 8601 local time in the target location and specify the timezone using the options (2nd parameter). - -| Field | Required | Allowed values | Allowed special characters | Remarks | -|--------------|----------|----------------|----------------------------|---------------------------------------| -| Seconds | Optional | 0-59 | * , - / ? | | -| Minutes | Yes | 0-59 | * , - / ? | | -| Hours | Yes | 0-23 | * , - / ? | | -| Day of Month | Yes | 1-31 | * , - / ? L | | -| Month | Yes | 1-12 or JAN-DEC| * , - / ? | | -| Day of Week | Yes | 0-7 or SUN-MON | * , - / ? # | 0 to 6 are Sunday to Saturday
7 is Sunday, the same as 0
# is used to specify nth occurrence of a weekday | - -> **Note** -> Weekday and month names are case-insensitive. Both `MON` and `mon` work. -> When using `L` in the Day of Week field, it affects all specified weekdays. For example, `5-6#L` means the last Friday and Saturday in the month." -> The # character can be used to specify the "nth" weekday of the month. For example, 5#2 represents the second Friday of the month. +- Croner expressions have the following additional modifiers: + - _?_: In the Rust version of croner, a questionmark behaves just as *, to + allow for legacy cron patterns to be used. + - _L_: The letter 'L' can be used in the day of the month field to indicate + the last day of the month. When used in the day of the week field in + conjunction with the # character, it denotes the last specific weekday of + the month. For example, `5#L` represents the last Friday of the month. + - _#_: The # character specifies the "nth" occurrence of a particular day + within a month. For example, supplying `5#2` in the day of week field + signifies the second Friday of the month. This can be combined with ranges + and supports day names. For instance, MON-FRI#2 would match the Monday + through Friday of the second week of the month. + +- Croner allows you to pass a JavaScript Date object or an ISO 8601 formatted + string as a pattern. The scheduled function will trigger at the specified + date/time and only once. If you use a timezone different from the local + timezone, you should pass the ISO 8601 local time in the target location and + specify the timezone using the options (2nd parameter). + +| Field | Required | Allowed values | Allowed special characters | Remarks | +| ------------ | -------- | --------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------- | +| Seconds | Optional | 0-59 | * , - / ? | | +| Minutes | Yes | 0-59 | * , - / ? | | +| Hours | Yes | 0-23 | * , - / ? | | +| Day of Month | Yes | 1-31 | * , - / ? L | | +| Month | Yes | 1-12 or JAN-DEC | * , - / ? | | +| Day of Week | Yes | 0-7 or SUN-MON | * , - / ? # | 0 to 6 are Sunday to Saturday
7 is Sunday, the same as 0
# is used to specify nth occurrence of a weekday | + +> **Note** Weekday and month names are case-insensitive. Both `MON` and `mon` +> work. When using `L` in the Day of Week field, it affects all specified +> weekdays. For example, `5-6#L` means the last Friday and Saturday in the +> month." The # character can be used to specify the "nth" weekday of the month. +> For example, 5#2 represents the second Friday of the month. It is also possible to use the following "nicknames" as pattern. -| Nickname | Description | -| -------- | ----------- | -| \@yearly | Run once a year, ie. "0 0 1 1 *". | -| \@annually | Run once a year, ie. "0 0 1 1 *". | -| \@monthly | Run once a month, ie. "0 0 1 * *". | -| \@weekly | Run once a week, ie. "0 0 * * 0". | -| \@daily | Run once a day, ie. "0 0 * * *". | -| \@hourly | Run once an hour, ie. "0 * * * *". | +| Nickname | Description | +| ---------- | ---------------------------------- | +| \@yearly | Run once a year, ie. "0 0 1 1 *". | +| \@annually | Run once a year, ie. "0 0 1 1 *". | +| \@monthly | Run once a month, ie. "0 0 1 * *". | +| \@weekly | Run once a week, ie. "0 0 * * 0". | +| \@daily | Run once a day, ie. "0 0 * * *". | +| \@hourly | Run once an hour, ie. "0 * * * *". | ### Documentation -For detailed usage and API documentation, visit [Croner on docs.rs](https://docs.rs/croner/). +For detailed usage and API documentation, visit +[Croner on docs.rs](https://docs.rs/croner/). ## Development @@ -154,15 +183,18 @@ To start developing in the Croner project: ## Contributing -We welcome contributions! Please feel free to submit a pull request or open an issue. +We welcome contributions! Please feel free to submit a pull request or open an +issue. ## License -This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details. +This project is licensed under the MIT License - see the +[LICENSE.md](LICENSE.md) file for details. ## Acknowledgments -- Thanks to the `chrono` crate for providing robust date and time handling in Rust. +- Thanks to the `chrono` crate for providing robust date and time handling in + Rust. - This project adheres to Semantic Versioning. ## Disclaimer @@ -171,4 +203,5 @@ This is an early version of Croner, and the API is subject to change. ## Contact -If you have any questions or feedback, please open an issue in the repository and we'll get back to you as soon as possible. \ No newline at end of file +If you have any questions or feedback, please open an issue in the repository +and we'll get back to you as soon as possible. diff --git a/benches/croner_bench.rs b/benches/croner_bench.rs new file mode 100644 index 0000000..f329f70 --- /dev/null +++ b/benches/croner_bench.rs @@ -0,0 +1,20 @@ +use chrono::Local; +use criterion::{black_box, criterion_group, criterion_main, Criterion}; +use croner::Cron; + +fn parse_take_100(_n: u64) { + let cron: Cron = "15 15 15 L 3 *" + .parse() + .expect("Couldn't parse cron string"); + let time = Local::now(); + for _time in cron.clone().iter_after(time).take(100) {} +} + +pub fn criterion_benchmark(c: &mut Criterion) { + c.bench_function("parse_take_100", |b| { + b.iter(|| parse_take_100(black_box(20))) + }); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); diff --git a/benches/deno/deno_bench.ts b/benches/deno/deno_bench.ts new file mode 100644 index 0000000..b8979ac --- /dev/null +++ b/benches/deno/deno_bench.ts @@ -0,0 +1,10 @@ +import { Cron } from "https://deno.land/x/croner@7.0.5-dev.0/dist/croner.js"; + +function workload() { + new Cron("15 15 15 L 3 *").nextRuns(100); +} + +// url_bench.ts +Deno.bench("parse take 100", () => { + workload(); +}); diff --git a/src/lib.rs b/src/lib.rs index b42878a..ead9bc4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,10 +4,10 @@ mod component; mod errors; use errors::CronError; -use pattern::CronPattern; +use pattern::{CronPattern, NO_MATCH}; use std::str::FromStr; -use chrono::{DateTime, Datelike, Duration, TimeZone, Timelike}; +use chrono::{DateTime, Datelike, Duration, LocalResult, TimeZone, Timelike}; pub struct CronIterator where @@ -56,6 +56,203 @@ where } } +enum TimeComponent { + Second = 1, + Minute, + Hour, + Day, + Month, + Year, +} + +// Recursive function to handle setting the time and managing overflows. +fn set_time( + current_time: &mut DateTime, + year: i32, + month: u32, + day: u32, + hour: u32, + minute: u32, + second: u32, + component: TimeComponent, + tz: &Tz, +) -> Result<(), CronError> { + match tz.with_ymd_and_hms(year, month, day, hour, minute, second) { + LocalResult::Single(new_time) => { + *current_time = new_time; + Ok(()) + } + LocalResult::None => { + // Handle overflow by incrementing the next higher component. + match component { + TimeComponent::Second => set_time( + current_time, + year, + month, + day, + hour, + minute + 1, + 0, + TimeComponent::Minute, + tz, + ), + TimeComponent::Minute => set_time( + current_time, + year, + month, + day, + hour + 1, + 0, + 0, + TimeComponent::Hour, + tz, + ), + TimeComponent::Hour => set_time( + current_time, + year, + month, + day + 1, + 0, + 0, + 0, + TimeComponent::Day, + tz, + ), + TimeComponent::Day => set_time( + current_time, + year, + month + 1, + 1, + 0, + 0, + 0, + TimeComponent::Month, + tz, + ), + TimeComponent::Month => set_time( + current_time, + year + 1, + 1, + 1, + 0, + 0, + 0, + TimeComponent::Year, + tz, + ), + TimeComponent::Year => Err(CronError::InvalidDate), + } + } + LocalResult::Ambiguous(..) => Err(CronError::InvalidDate), + } +} + +fn set_time_component( + current_time: &mut DateTime, + component: TimeComponent, + set_to: u32, +) -> Result<(), CronError> { + let tz = current_time.timezone(); + + // Extract all parts + let (year, month, day, hour, minute, _second) = ( + current_time.year(), + current_time.month(), + current_time.day(), + current_time.hour(), + current_time.minute(), + current_time.second(), + ); + + match component { + TimeComponent::Year => set_time(current_time, set_to as i32, 0, 0, 0, 0, 0, component, &tz), + TimeComponent::Month => set_time(current_time, year, set_to, 0, 0, 0, 0, component, &tz), + TimeComponent::Day => set_time(current_time, year, month, set_to, 0, 0, 0, component, &tz), + TimeComponent::Hour => { + set_time(current_time, year, month, day, set_to, 0, 0, component, &tz) + } + TimeComponent::Minute => set_time( + current_time, + year, + month, + day, + hour, + set_to, + 0, + component, + &tz, + ), + TimeComponent::Second => set_time( + current_time, + year, + month, + day, + hour, + minute, + set_to, + component, + &tz, + ), + } +} + +fn increment_time_component( + current_time: &mut DateTime, + component: TimeComponent, +) -> Result<(), CronError> { + let tz = current_time.timezone(); + + // Extract all parts + let (year, month, day, hour, minute, second) = ( + current_time.year(), + current_time.month(), + current_time.day(), + current_time.hour(), + current_time.minute(), + current_time.second(), + ); + + // Increment the component and try to set the new time. + match component { + TimeComponent::Year => set_time(current_time, year + 1, 1, 1, 0, 0, 0, component, &tz), + TimeComponent::Month => set_time(current_time, year, month + 1, 1, 0, 0, 0, component, &tz), + TimeComponent::Day => set_time(current_time, year, month, day + 1, 0, 0, 0, component, &tz), + TimeComponent::Hour => set_time( + current_time, + year, + month, + day, + hour + 1, + 0, + 0, + component, + &tz, + ), + TimeComponent::Minute => set_time( + current_time, + year, + month, + day, + hour, + minute + 1, + 0, + component, + &tz, + ), + TimeComponent::Second => set_time( + current_time, + year, + month, + day, + hour, + minute, + second + 1, + component, + &tz, + ), + } +} + // The Cron struct represents a cron schedule and provides methods to parse cron strings, // check if a datetime matches the cron pattern, and find the next occurrence. #[derive(Clone)] @@ -177,17 +374,26 @@ impl Cron { start_time: &DateTime, inclusive: bool, ) -> Result, CronError> { - let mut current_time = if inclusive { - start_time.clone() - } else { - self.increment_time(start_time, Duration::seconds(1))? - }; + let mut current_time: DateTime = start_time.clone(); + if !inclusive { + increment_time_component(&mut current_time, TimeComponent::Second)?; + } loop { - current_time = self.find_next_matching_month(¤t_time)?; - current_time = self.find_next_matching_day(¤t_time)?; - current_time = self.find_next_matching_hour(¤t_time)?; - current_time = self.find_next_matching_minute(¤t_time)?; - current_time = self.find_next_matching_second(¤t_time)?; + if self.find_next_matching_month(&mut current_time)? { + continue; + }; + if self.find_next_matching_day(&mut current_time)? { + continue; + }; + if self.find_next_matching_hour(&mut current_time)? { + continue; + }; + if self.find_next_matching_minute(&mut current_time)? { + continue; + }; + if self.find_next_matching_second(&mut current_time)? { + continue; + }; if self.is_time_matching(¤t_time)? { return Ok(current_time); } @@ -281,122 +487,83 @@ impl Cron { CronIterator::new(self.clone(), start_from) } - // Internal function to increment time with a set duration, and give CronError::InvalidTime on failure - fn increment_time( - &self, - time: &DateTime, - duration: Duration, - ) -> Result, CronError> { - time.clone() - .checked_add_signed(duration) - .ok_or(CronError::InvalidTime) - } - // Internal functions to check for the next matching month/day/hour/minute/second and return the updated time. fn find_next_matching_month( &self, - current_time: &DateTime, - ) -> Result, CronError> { - let tz = current_time.timezone(); - let mut year = current_time.year(); - let mut month = current_time.month(); - let mut day = current_time.day(); - let mut hour = current_time.hour(); - let mut minute = current_time.minute(); - let mut second = current_time.second(); - while !self.pattern.month_match(month)? { - month += 1; - day = 1; - hour = 0; - minute = 0; - second = 0; - if month > 12 { - month = 1; - year += 1; - if year > 9999 { - return Err(CronError::TimeSearchLimitExceeded); - } - } + current_time: &mut DateTime, + ) -> Result { + let mut incremented = false; + while !self.pattern.month_match(current_time.month())? { + increment_time_component(current_time, TimeComponent::Month)?; + incremented = true; } - tz.with_ymd_and_hms(year, month, day, hour, minute, second) - .single() - .ok_or(CronError::InvalidDate) + Ok(incremented) } fn find_next_matching_day( &self, - current_time: &DateTime, - ) -> Result, CronError> { - let tz = current_time.timezone(); - let mut date = current_time.clone(); - - while !self - .pattern - .day_match(date.year(), date.month(), date.day())? - { - date = self.increment_time(&date, Duration::days(1))?; - // When the minute changes, reset time - date = tz - .with_ymd_and_hms(date.year(), date.month(), date.day(), 0, 0, 0) - .single() - .ok_or(CronError::InvalidTime)?; + current_time: &mut DateTime, + ) -> Result { + let mut incremented = false; + while !self.pattern.day_match( + current_time.year(), + current_time.month(), + current_time.day(), + )? { + increment_time_component(current_time, TimeComponent::Day)?; + incremented = true; } - Ok(date) + Ok(incremented) } fn find_next_matching_hour( &self, - current_time: &DateTime, - ) -> Result, CronError> { - let tz = current_time.timezone(); - let mut time = current_time.clone(); - - while !self.pattern.hour_match(time.hour())? { - time = self.increment_time(&time, Duration::hours(1))?; - // When the hour changes, reset minutes and seconds to 0 - time = tz - .with_ymd_and_hms(time.year(), time.month(), time.day(), time.hour(), 0, 0) - .single() - .ok_or(CronError::InvalidTime)?; + current_time: &mut DateTime, + ) -> Result { + let mut incremented = false; + let next_match = self.pattern.next_hour_match(current_time.hour()).unwrap(); + if next_match == NO_MATCH { + increment_time_component(current_time, TimeComponent::Day)?; + incremented = true; + } else if next_match != current_time.hour() { + incremented = true; + set_time_component(current_time, TimeComponent::Hour, next_match)?; } - - Ok(time) + Ok(incremented) } fn find_next_matching_minute( &self, - current_time: &DateTime, - ) -> Result, CronError> { - let tz = current_time.timezone(); - let mut time = current_time.clone(); - - while !self.pattern.minute_match(time.minute())? { - time = self.increment_time(&time, Duration::minutes(1))?; - // When the minute changes, reset seconds to 0 - time = tz - .with_ymd_and_hms( - time.year(), - time.month(), - time.day(), - time.hour(), - time.minute(), - 0, - ) - .single() - .ok_or(CronError::InvalidTime)?; + current_time: &mut DateTime, + ) -> Result { + let mut incremented = false; + let next_match = self + .pattern + .next_minute_match(current_time.minute()) + .unwrap(); + if next_match == NO_MATCH { + increment_time_component(current_time, TimeComponent::Hour)?; + incremented = true; + } else if next_match != current_time.minute() { + incremented = true; + set_time_component(current_time, TimeComponent::Minute, next_match)?; } - - Ok(time) + Ok(incremented) } fn find_next_matching_second( &self, - current_time: &DateTime, - ) -> Result, CronError> { - let mut time = current_time.clone(); - - while !self.pattern.second_match(time.second())? { - time = self.increment_time(&time, Duration::seconds(1))?; + current_time: &mut DateTime, + ) -> Result { + let mut incremented = false; + let next_match = self + .pattern + .next_second_match(current_time.second()) + .unwrap(); + if next_match == NO_MATCH { + increment_time_component(current_time, TimeComponent::Minute)?; + incremented = true; + } else { + set_time_component(current_time, TimeComponent::Second, next_match)?; } - - Ok(time) + Ok(incremented) } } @@ -483,7 +650,6 @@ mod tests { // Verify that the next occurrence is at the expected time. let expected_time = Local.with_ymd_and_hms(2023, 1, 1, 0, 0, 30).unwrap(); - println!("{} {} {}", start_time, next_occurrence, expected_time); assert_eq!(next_occurrence, expected_time); Ok(()) @@ -501,7 +667,6 @@ mod tests { // Verify that the next occurrence is at the expected time. let expected_time = Local.with_ymd_and_hms(2023, 1, 1, 0, 1, 0).unwrap(); - println!("{} {} {}", start_time, next_occurrence, expected_time); assert_eq!(next_occurrence, expected_time); Ok(()) @@ -519,7 +684,6 @@ mod tests { // Verify that the next occurrence is at the expected time. let expected_time = Local.with_ymd_and_hms(2024, 1, 1, 15, 0, 0).unwrap(); - println!("{} {} {}", start_time, next_occurrence, expected_time); assert_eq!(next_occurrence, expected_time); Ok(()) diff --git a/src/pattern.rs b/src/pattern.rs index 9689603..639d133 100644 --- a/src/pattern.rs +++ b/src/pattern.rs @@ -5,6 +5,8 @@ use crate::component::{ use crate::errors::CronError; use chrono::{Datelike, Duration, NaiveDate, Weekday}; +pub const NO_MATCH: u32 = 99999; + // This struct is used for representing and validating cron pattern strings. // It supports parsing cron patterns with optional seconds field and provides functionality to check pattern matching against specific datetime. #[derive(Debug, Clone)] @@ -347,6 +349,45 @@ impl CronPattern { } self.seconds.is_bit_set(second as u8, ALL_BIT) } + + // Finds the next hour that matches the hour part of the cron pattern. + pub fn next_hour_match(&self, hour: u32) -> Result { + if hour > 23 { + return Err(CronError::InvalidTime); + } + for next_hour in hour..=23 { + if self.hours.is_bit_set(next_hour as u8, ALL_BIT)? { + return Ok(next_hour); + } + } + Ok(NO_MATCH) // No match found within the current range + } + + // Finds the next minute that matches the minute part of the cron pattern. + pub fn next_minute_match(&self, minute: u32) -> Result { + if minute > 59 { + return Err(CronError::InvalidTime); + } + for next_minute in minute..=59 { + if self.minutes.is_bit_set(next_minute as u8, ALL_BIT)? { + return Ok(next_minute); + } + } + Ok(NO_MATCH) // No match found within the current range + } + + // Finds the next second that matches the second part of the cron pattern. + pub fn next_second_match(&self, second: u32) -> Result { + if second > 59 { + return Err(CronError::InvalidTime); + } + for next_second in second..=59 { + if self.seconds.is_bit_set(next_second as u8, ALL_BIT)? { + return Ok(next_second); + } + } + Ok(NO_MATCH) // No match found within the current range + } } impl ToString for CronPattern {