├── .gitignore ├── src ├── ordinal.rs ├── error.rs ├── specifier.rs ├── time_unit │ ├── hours.rs │ ├── minutes.rs │ ├── seconds.rs │ ├── days_of_month.rs │ ├── years.rs │ ├── days_of_week.rs │ ├── months.rs │ └── mod.rs ├── lib.rs ├── queries.rs ├── parsing.rs └── schedule.rs ├── .github └── workflows │ └── rust.yml ├── Cargo.toml ├── LICENSE-MIT ├── README.md ├── LICENSE-APACHE └── tests └── lib.rs /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | *.swp 4 | *.iml 5 | .idea 6 | **/*.rs.bk 7 | -------------------------------------------------------------------------------- /src/ordinal.rs: -------------------------------------------------------------------------------- 1 | use std::collections::BTreeSet; 2 | 3 | pub type Ordinal = u32; 4 | // TODO: Make OrdinalSet an enum. 5 | // It should either be a BTreeSet of ordinals or an `All` option to save space. 6 | // `All` can iterate from inclusive_min to inclusive_max and answer membership 7 | // queries 8 | pub type OrdinalSet = BTreeSet; 9 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use std::{error, fmt}; 2 | 3 | /// A cron error 4 | #[derive(Debug)] 5 | pub struct Error { 6 | kind: ErrorKind, 7 | } 8 | 9 | /// The kind of cron error that occurred 10 | #[derive(Debug)] 11 | pub enum ErrorKind { 12 | /// Failed to parse an expression 13 | Expression(String), 14 | } 15 | 16 | impl fmt::Display for Error { 17 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 18 | match self.kind { 19 | ErrorKind::Expression(ref expr) => write!(f, "{expr}"), 20 | } 21 | } 22 | } 23 | 24 | impl error::Error for Error {} 25 | 26 | impl From for Error { 27 | fn from(kind: ErrorKind) -> Error { 28 | Error { kind } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests (default features) 22 | run: cargo test --verbose 23 | - name: Run tests (all features) 24 | run: cargo test --verbose --all-features 25 | - name: Build docs 26 | run: cargo doc --no-deps --verbose 27 | - name: Check formatting 28 | run: cargo fmt -- --check 29 | - name: Check linting 30 | run: cargo clippy --all-targets --all-features -- -D warnings 31 | -------------------------------------------------------------------------------- /src/specifier.rs: -------------------------------------------------------------------------------- 1 | use crate::ordinal::*; 2 | 3 | #[derive(Debug, PartialEq)] 4 | pub enum Specifier { 5 | All, 6 | Point(Ordinal), 7 | Range(Ordinal, Ordinal), 8 | NamedRange(String, String), 9 | } 10 | 11 | // Separating out a root specifier allows for a higher tiered specifier, allowing us to achieve 12 | // periods with base values that are more advanced than an ordinal: 13 | // - all: '*/2' 14 | // - range: '10-2/2' 15 | // - named range: 'Mon-Thurs/2' 16 | // 17 | // Without this separation we would end up with invalid combinations such as 'Mon/2' 18 | #[derive(Debug, PartialEq)] 19 | pub enum RootSpecifier { 20 | Specifier(Specifier), 21 | Period(Specifier, u32), 22 | NamedPoint(String), 23 | } 24 | 25 | impl From for RootSpecifier { 26 | fn from(specifier: Specifier) -> Self { 27 | Self::Specifier(specifier) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cron" 3 | version = "0.15.0" 4 | authors = ["Zack Slayton "] 5 | repository = "https://github.com/zslayton/cron" 6 | documentation = "https://docs.rs/cron" 7 | readme = "README.md" 8 | description = "A cron expression parser and schedule explorer." 9 | keywords = [ "cron", "schedule", "repeat", "periodic", "time" ] 10 | license = "MIT OR Apache-2.0" 11 | edition = "2021" 12 | 13 | [lib] 14 | name = "cron" 15 | 16 | [dependencies] 17 | chrono = { version = "~0.4", default-features = false, features = ["clock"] } 18 | winnow = "0.7.0" 19 | once_cell = "1.10" 20 | serde = {version = "1.0.164", optional = true } 21 | 22 | [dev-dependencies] 23 | chrono-tz = "~0.6" 24 | serde_test = "1.0.164" 25 | 26 | # Dev-dependency for feature "serde". 27 | # Optional dev-dependencies are not supported yet. 28 | # Cargo feature request is available at https://github.com/rust-lang/cargo/issues/1596 29 | postcard = { version = "1.0.10", default-features = false, features = ["use-std"] } 30 | 31 | [features] 32 | serde = ["dep:serde"] 33 | -------------------------------------------------------------------------------- /src/time_unit/hours.rs: -------------------------------------------------------------------------------- 1 | use crate::ordinal::{Ordinal, OrdinalSet}; 2 | use crate::time_unit::TimeUnitField; 3 | use once_cell::sync::Lazy; 4 | use std::borrow::Cow; 5 | 6 | static ALL: Lazy = Lazy::new(Hours::supported_ordinals); 7 | 8 | #[derive(Clone, Debug, Eq)] 9 | pub struct Hours { 10 | ordinals: Option, 11 | } 12 | 13 | impl TimeUnitField for Hours { 14 | fn from_optional_ordinal_set(ordinal_set: Option) -> Self { 15 | Hours { 16 | ordinals: ordinal_set, 17 | } 18 | } 19 | fn name() -> Cow<'static, str> { 20 | Cow::from("Hours") 21 | } 22 | fn inclusive_min() -> Ordinal { 23 | 0 24 | } 25 | fn inclusive_max() -> Ordinal { 26 | 23 27 | } 28 | fn ordinals(&self) -> &OrdinalSet { 29 | match &self.ordinals { 30 | Some(ordinal_set) => ordinal_set, 31 | None => &ALL, 32 | } 33 | } 34 | } 35 | 36 | impl PartialEq for Hours { 37 | fn eq(&self, other: &Hours) -> bool { 38 | self.ordinals() == other.ordinals() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/time_unit/minutes.rs: -------------------------------------------------------------------------------- 1 | use crate::ordinal::{Ordinal, OrdinalSet}; 2 | use crate::time_unit::TimeUnitField; 3 | use once_cell::sync::Lazy; 4 | use std::borrow::Cow; 5 | 6 | static ALL: Lazy = Lazy::new(Minutes::supported_ordinals); 7 | 8 | #[derive(Clone, Debug, Eq)] 9 | pub struct Minutes { 10 | ordinals: Option, 11 | } 12 | 13 | impl TimeUnitField for Minutes { 14 | fn from_optional_ordinal_set(ordinal_set: Option) -> Self { 15 | Minutes { 16 | ordinals: ordinal_set, 17 | } 18 | } 19 | fn name() -> Cow<'static, str> { 20 | Cow::from("Minutes") 21 | } 22 | fn inclusive_min() -> Ordinal { 23 | 0 24 | } 25 | fn inclusive_max() -> Ordinal { 26 | 59 27 | } 28 | fn ordinals(&self) -> &OrdinalSet { 29 | match &self.ordinals { 30 | Some(ordinal_set) => ordinal_set, 31 | None => &ALL, 32 | } 33 | } 34 | } 35 | 36 | impl PartialEq for Minutes { 37 | fn eq(&self, other: &Minutes) -> bool { 38 | self.ordinals() == other.ordinals() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/time_unit/seconds.rs: -------------------------------------------------------------------------------- 1 | use crate::ordinal::{Ordinal, OrdinalSet}; 2 | use crate::time_unit::TimeUnitField; 3 | use once_cell::sync::Lazy; 4 | use std::borrow::Cow; 5 | 6 | static ALL: Lazy = Lazy::new(Seconds::supported_ordinals); 7 | 8 | #[derive(Clone, Debug, Eq)] 9 | pub struct Seconds { 10 | ordinals: Option, 11 | } 12 | 13 | impl TimeUnitField for Seconds { 14 | fn from_optional_ordinal_set(ordinal_set: Option) -> Self { 15 | Seconds { 16 | ordinals: ordinal_set, 17 | } 18 | } 19 | fn name() -> Cow<'static, str> { 20 | Cow::from("Seconds") 21 | } 22 | fn inclusive_min() -> Ordinal { 23 | 0 24 | } 25 | fn inclusive_max() -> Ordinal { 26 | 59 27 | } 28 | fn ordinals(&self) -> &OrdinalSet { 29 | match &self.ordinals { 30 | Some(ordinal_set) => ordinal_set, 31 | None => &ALL, 32 | } 33 | } 34 | } 35 | 36 | impl PartialEq for Seconds { 37 | fn eq(&self, other: &Seconds) -> bool { 38 | self.ordinals() == other.ordinals() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/time_unit/days_of_month.rs: -------------------------------------------------------------------------------- 1 | use crate::ordinal::{Ordinal, OrdinalSet}; 2 | use crate::time_unit::TimeUnitField; 3 | use once_cell::sync::Lazy; 4 | use std::borrow::Cow; 5 | 6 | static ALL: Lazy = Lazy::new(DaysOfMonth::supported_ordinals); 7 | 8 | #[derive(Clone, Debug, Eq)] 9 | pub struct DaysOfMonth { 10 | ordinals: Option, 11 | } 12 | 13 | impl TimeUnitField for DaysOfMonth { 14 | fn from_optional_ordinal_set(ordinal_set: Option) -> Self { 15 | DaysOfMonth { 16 | ordinals: ordinal_set, 17 | } 18 | } 19 | fn name() -> Cow<'static, str> { 20 | Cow::from("Days of Month") 21 | } 22 | fn inclusive_min() -> Ordinal { 23 | 1 24 | } 25 | fn inclusive_max() -> Ordinal { 26 | 31 27 | } 28 | fn ordinals(&self) -> &OrdinalSet { 29 | match &self.ordinals { 30 | Some(ordinal_set) => ordinal_set, 31 | None => &ALL, 32 | } 33 | } 34 | } 35 | 36 | impl PartialEq for DaysOfMonth { 37 | fn eq(&self, other: &DaysOfMonth) -> bool { 38 | self.ordinals() == other.ordinals() 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Zack Slayton 4 | 5 | Permission is hereby granted, free of charge, to any 6 | person obtaining a copy of this software and associated 7 | documentation files (the "Software"), to deal in the 8 | Software without restriction, including without 9 | limitation the rights to use, copy, modify, merge, 10 | publish, distribute, sublicense, and/or sell copies of 11 | the Software, and to permit persons to whom the Software 12 | is furnished to do so, subject to the following 13 | conditions: 14 | 15 | The above copyright notice and this permission notice 16 | shall be included in all copies or substantial portions 17 | of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 20 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 21 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 22 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 23 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 24 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 25 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 26 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 27 | DEALINGS IN THE SOFTWARE. 28 | -------------------------------------------------------------------------------- /src/time_unit/years.rs: -------------------------------------------------------------------------------- 1 | use crate::ordinal::{Ordinal, OrdinalSet}; 2 | use crate::time_unit::TimeUnitField; 3 | use once_cell::sync::Lazy; 4 | use std::borrow::Cow; 5 | 6 | static ALL: Lazy = Lazy::new(Years::supported_ordinals); 7 | 8 | #[derive(Clone, Debug, Eq)] 9 | pub struct Years { 10 | ordinals: Option, 11 | } 12 | 13 | impl TimeUnitField for Years { 14 | fn from_optional_ordinal_set(ordinal_set: Option) -> Self { 15 | Years { 16 | ordinals: ordinal_set, 17 | } 18 | } 19 | fn name() -> Cow<'static, str> { 20 | Cow::from("Years") 21 | } 22 | 23 | // TODO: Using the default impl, this will make a set w/100+ items each time "*" is used. 24 | // This is obviously suboptimal. 25 | fn inclusive_min() -> Ordinal { 26 | 1970 27 | } 28 | fn inclusive_max() -> Ordinal { 29 | 2100 30 | } 31 | fn ordinals(&self) -> &OrdinalSet { 32 | match &self.ordinals { 33 | Some(ordinal_set) => ordinal_set, 34 | None => &ALL, 35 | } 36 | } 37 | } 38 | 39 | impl PartialEq for Years { 40 | fn eq(&self, other: &Years) -> bool { 41 | self.ordinals() == other.ordinals() 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![deny(rust_2018_idioms)] 2 | #![deny(rustdoc::broken_intra_doc_links)] 3 | #![allow(clippy::needless_doctest_main)] 4 | //! A cron expression parser and schedule explorer 5 | //! # Example 6 | //! ``` 7 | //! use cron::Schedule; 8 | //! use chrono::Utc; 9 | //! use std::str::FromStr; 10 | //! 11 | //! fn main() { 12 | //! // sec min hour day of month month day of week year 13 | //! let expression = "0 30 9,12,15 1,15 May-Aug Mon,Wed,Fri 2018/2"; 14 | //! let schedule = Schedule::from_str(expression).unwrap(); 15 | //! println!("Upcoming fire times:"); 16 | //! for datetime in schedule.upcoming(Utc).take(10) { 17 | //! println!("-> {}", datetime); 18 | //! } 19 | //! } 20 | //! 21 | //! /* 22 | //! Upcoming fire times: 23 | //! -> 2018-06-01 09:30:00 UTC 24 | //! -> 2018-06-01 12:30:00 UTC 25 | //! -> 2018-06-01 15:30:00 UTC 26 | //! -> 2018-06-15 09:30:00 UTC 27 | //! -> 2018-06-15 12:30:00 UTC 28 | //! -> 2018-06-15 15:30:00 UTC 29 | //! -> 2018-08-01 09:30:00 UTC 30 | //! -> 2018-08-01 12:30:00 UTC 31 | //! -> 2018-08-01 15:30:00 UTC 32 | //! -> 2018-08-15 09:30:00 UTC 33 | //! */ 34 | //! ``` 35 | 36 | /// Error types used by this crate. 37 | pub mod error; 38 | 39 | mod ordinal; 40 | mod parsing; 41 | mod queries; 42 | mod schedule; 43 | mod specifier; 44 | mod time_unit; 45 | 46 | pub use crate::schedule::{OwnedScheduleIterator, Schedule, ScheduleIterator}; 47 | pub use crate::time_unit::TimeUnitSpec; 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cron [![Rust](https://github.com/zslayton/cron/workflows/Rust/badge.svg)](https://github.com/zslayton/cron/actions) [![](https://img.shields.io/crates/v/cron.svg)](https://crates.io/crates/cron) [![](https://docs.rs/cron/badge.svg)](https://docs.rs/cron) 2 | A cron expression parser. Works with stable Rust v1.28.0. 3 | 4 | ```rust 5 | use cron::Schedule; 6 | use chrono::Utc; 7 | use std::str::FromStr; 8 | 9 | fn main() { 10 | // sec min hour day of month month day of week year 11 | let expression = "0 30 9,12,15 1,15 May-Aug Mon,Wed,Fri 2018/2"; 12 | let schedule = Schedule::from_str(expression).unwrap(); 13 | println!("Upcoming fire times:"); 14 | for datetime in schedule.upcoming(Utc).take(10) { 15 | println!("-> {}", datetime); 16 | } 17 | } 18 | 19 | /* 20 | Upcoming fire times: 21 | -> 2018-06-01 09:30:00 UTC 22 | -> 2018-06-01 12:30:00 UTC 23 | -> 2018-06-01 15:30:00 UTC 24 | -> 2018-06-15 09:30:00 UTC 25 | -> 2018-06-15 12:30:00 UTC 26 | -> 2018-06-15 15:30:00 UTC 27 | -> 2018-08-01 09:30:00 UTC 28 | -> 2018-08-01 12:30:00 UTC 29 | -> 2018-08-01 15:30:00 UTC 30 | -> 2018-08-15 09:30:00 UTC 31 | */ 32 | ``` 33 | 34 | ## License 35 | 36 | Licensed under either of 37 | * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or https://www.apache.org/licenses/LICENSE-2.0) 38 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or https://opensource.org/licenses/MIT) 39 | at your option. 40 | 41 | ### Contribution 42 | 43 | Unless you explicitly state otherwise, any contribution intentionally submitted 44 | for inclusion in the work by you shall be dual licensed as above, without any 45 | additional terms or conditions. 46 | -------------------------------------------------------------------------------- /src/time_unit/days_of_week.rs: -------------------------------------------------------------------------------- 1 | use crate::error::*; 2 | use crate::ordinal::{Ordinal, OrdinalSet}; 3 | use crate::time_unit::TimeUnitField; 4 | use once_cell::sync::Lazy; 5 | use std::borrow::Cow; 6 | 7 | static ALL: Lazy = Lazy::new(DaysOfWeek::supported_ordinals); 8 | 9 | #[derive(Clone, Debug, Eq)] 10 | pub struct DaysOfWeek { 11 | ordinals: Option, 12 | } 13 | 14 | impl TimeUnitField for DaysOfWeek { 15 | fn from_optional_ordinal_set(ordinal_set: Option) -> Self { 16 | DaysOfWeek { 17 | ordinals: ordinal_set, 18 | } 19 | } 20 | fn name() -> Cow<'static, str> { 21 | Cow::from("Days of Week") 22 | } 23 | fn inclusive_min() -> Ordinal { 24 | 1 25 | } 26 | fn inclusive_max() -> Ordinal { 27 | 7 28 | } 29 | fn ordinal_from_name(name: &str) -> Result { 30 | //TODO: Use phf crate 31 | let ordinal = match name.to_lowercase().as_ref() { 32 | "sun" | "sunday" => 1, 33 | "mon" | "monday" => 2, 34 | "tue" | "tues" | "tuesday" => 3, 35 | "wed" | "wednesday" => 4, 36 | "thu" | "thurs" | "thursday" => 5, 37 | "fri" | "friday" => 6, 38 | "sat" | "saturday" => 7, 39 | _ => { 40 | return Err(ErrorKind::Expression(format!( 41 | "'{}' is not a valid day of the week.", 42 | name 43 | )) 44 | .into()) 45 | } 46 | }; 47 | Ok(ordinal) 48 | } 49 | fn ordinals(&self) -> &OrdinalSet { 50 | match &self.ordinals { 51 | Some(ordinal_set) => ordinal_set, 52 | None => &ALL, 53 | } 54 | } 55 | } 56 | 57 | impl PartialEq for DaysOfWeek { 58 | fn eq(&self, other: &DaysOfWeek) -> bool { 59 | self.ordinals() == other.ordinals() 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/time_unit/months.rs: -------------------------------------------------------------------------------- 1 | use crate::error::*; 2 | use crate::ordinal::{Ordinal, OrdinalSet}; 3 | use crate::time_unit::TimeUnitField; 4 | use once_cell::sync::Lazy; 5 | use std::borrow::Cow; 6 | 7 | static ALL: Lazy = Lazy::new(Months::supported_ordinals); 8 | 9 | #[derive(Clone, Debug, Eq)] 10 | pub struct Months { 11 | ordinals: Option, 12 | } 13 | 14 | impl TimeUnitField for Months { 15 | fn from_optional_ordinal_set(ordinal_set: Option) -> Self { 16 | Months { 17 | ordinals: ordinal_set, 18 | } 19 | } 20 | fn name() -> Cow<'static, str> { 21 | Cow::from("Months") 22 | } 23 | fn inclusive_min() -> Ordinal { 24 | 1 25 | } 26 | fn inclusive_max() -> Ordinal { 27 | 12 28 | } 29 | fn ordinal_from_name(name: &str) -> Result { 30 | //TODO: Use phf crate 31 | let ordinal = match name.to_lowercase().as_ref() { 32 | "jan" | "january" => 1, 33 | "feb" | "february" => 2, 34 | "mar" | "march" => 3, 35 | "apr" | "april" => 4, 36 | "may" => 5, 37 | "jun" | "june" => 6, 38 | "jul" | "july" => 7, 39 | "aug" | "august" => 8, 40 | "sep" | "september" => 9, 41 | "oct" | "october" => 10, 42 | "nov" | "november" => 11, 43 | "dec" | "december" => 12, 44 | _ => { 45 | return Err( 46 | ErrorKind::Expression(format!("'{}' is not a valid month name.", name)).into(), 47 | ) 48 | } 49 | }; 50 | Ok(ordinal) 51 | } 52 | fn ordinals(&self) -> &OrdinalSet { 53 | match &self.ordinals { 54 | Some(ordinal_set) => ordinal_set, 55 | None => &ALL, 56 | } 57 | } 58 | } 59 | 60 | impl PartialEq for Months { 61 | fn eq(&self, other: &Months) -> bool { 62 | self.ordinals() == other.ordinals() 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/queries.rs: -------------------------------------------------------------------------------- 1 | use chrono::offset::TimeZone; 2 | use chrono::{DateTime, Datelike, Duration, Timelike}; 3 | 4 | use crate::ordinal::Ordinal; 5 | use crate::time_unit::{DaysOfMonth, Hours, Minutes, Months, Seconds, TimeUnitField}; 6 | 7 | // TODO: Possibility of one query struct? 8 | 9 | pub struct NextAfterQuery 10 | where 11 | Z: TimeZone, 12 | { 13 | initial_datetime: DateTime, 14 | first_month: bool, 15 | first_day_of_month: bool, 16 | first_hour: bool, 17 | first_minute: bool, 18 | first_second: bool, 19 | } 20 | 21 | impl NextAfterQuery 22 | where 23 | Z: TimeZone, 24 | { 25 | pub fn from(after: &DateTime) -> NextAfterQuery { 26 | NextAfterQuery { 27 | initial_datetime: after.clone() + Duration::seconds(1), 28 | first_month: true, 29 | first_day_of_month: true, 30 | first_hour: true, 31 | first_minute: true, 32 | first_second: true, 33 | } 34 | } 35 | 36 | pub fn year_lower_bound(&self) -> Ordinal { 37 | // Unlike the other units, years will never wrap around. 38 | self.initial_datetime.year() as u32 39 | } 40 | 41 | pub fn month_lower_bound(&mut self) -> Ordinal { 42 | if self.first_month { 43 | self.first_month = false; 44 | return self.initial_datetime.month(); 45 | } 46 | Months::inclusive_min() 47 | } 48 | 49 | pub fn reset_month(&mut self) { 50 | self.first_month = false; 51 | self.reset_day_of_month(); 52 | } 53 | 54 | pub fn day_of_month_lower_bound(&mut self) -> Ordinal { 55 | if self.first_day_of_month { 56 | self.first_day_of_month = false; 57 | return self.initial_datetime.day(); 58 | } 59 | DaysOfMonth::inclusive_min() 60 | } 61 | 62 | pub fn reset_day_of_month(&mut self) { 63 | self.first_day_of_month = false; 64 | self.reset_hour(); 65 | } 66 | 67 | pub fn hour_lower_bound(&mut self) -> Ordinal { 68 | if self.first_hour { 69 | self.first_hour = false; 70 | return self.initial_datetime.hour(); 71 | } 72 | Hours::inclusive_min() 73 | } 74 | 75 | pub fn reset_hour(&mut self) { 76 | self.first_hour = false; 77 | self.reset_minute(); 78 | } 79 | 80 | pub fn minute_lower_bound(&mut self) -> Ordinal { 81 | if self.first_minute { 82 | self.first_minute = false; 83 | return self.initial_datetime.minute(); 84 | } 85 | Minutes::inclusive_min() 86 | } 87 | 88 | pub fn reset_minute(&mut self) { 89 | self.first_minute = false; 90 | self.reset_second(); 91 | } 92 | 93 | pub fn second_lower_bound(&mut self) -> Ordinal { 94 | if self.first_second { 95 | self.first_second = false; 96 | return self.initial_datetime.second(); 97 | } 98 | Seconds::inclusive_min() 99 | } 100 | 101 | pub fn reset_second(&mut self) { 102 | self.first_second = false; 103 | } 104 | } // End of impl 105 | 106 | pub struct PrevFromQuery 107 | where 108 | Z: TimeZone, 109 | { 110 | initial_datetime: DateTime, 111 | first_month: bool, 112 | first_day_of_month: bool, 113 | first_hour: bool, 114 | first_minute: bool, 115 | first_second: bool, 116 | } 117 | 118 | impl PrevFromQuery 119 | where 120 | Z: TimeZone, 121 | { 122 | pub fn from(before: &DateTime) -> PrevFromQuery { 123 | let initial_datetime = if before.timestamp_subsec_nanos() > 0 { 124 | before.clone() 125 | } else { 126 | before.clone() - Duration::seconds(1) 127 | }; 128 | PrevFromQuery { 129 | initial_datetime, 130 | first_month: true, 131 | first_day_of_month: true, 132 | first_hour: true, 133 | first_minute: true, 134 | first_second: true, 135 | } 136 | } 137 | 138 | pub fn year_upper_bound(&self) -> Ordinal { 139 | // Unlike the other units, years will never wrap around. 140 | self.initial_datetime.year() as u32 141 | } 142 | 143 | pub fn month_upper_bound(&mut self) -> Ordinal { 144 | if self.first_month { 145 | self.first_month = false; 146 | return self.initial_datetime.month(); 147 | } 148 | Months::inclusive_max() 149 | } 150 | 151 | pub fn reset_month(&mut self) { 152 | self.first_month = false; 153 | self.reset_day_of_month(); 154 | } 155 | 156 | pub fn day_of_month_upper_bound(&mut self) -> Ordinal { 157 | if self.first_day_of_month { 158 | self.first_day_of_month = false; 159 | return self.initial_datetime.day(); 160 | } 161 | DaysOfMonth::inclusive_max() 162 | } 163 | 164 | pub fn reset_day_of_month(&mut self) { 165 | self.first_day_of_month = false; 166 | self.reset_hour(); 167 | } 168 | 169 | pub fn hour_upper_bound(&mut self) -> Ordinal { 170 | if self.first_hour { 171 | self.first_hour = false; 172 | return self.initial_datetime.hour(); 173 | } 174 | Hours::inclusive_max() 175 | } 176 | 177 | pub fn reset_hour(&mut self) { 178 | self.first_hour = false; 179 | self.reset_minute(); 180 | } 181 | 182 | pub fn minute_upper_bound(&mut self) -> Ordinal { 183 | if self.first_minute { 184 | self.first_minute = false; 185 | return self.initial_datetime.minute(); 186 | } 187 | Minutes::inclusive_max() 188 | } 189 | 190 | pub fn reset_minute(&mut self) { 191 | self.first_minute = false; 192 | self.reset_second(); 193 | } 194 | 195 | pub fn second_upper_bound(&mut self) -> Ordinal { 196 | if self.first_second { 197 | self.first_second = false; 198 | return self.initial_datetime.second(); 199 | } 200 | Seconds::inclusive_max() 201 | } 202 | 203 | pub fn reset_second(&mut self) { 204 | self.first_second = false; 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /src/time_unit/mod.rs: -------------------------------------------------------------------------------- 1 | mod days_of_month; 2 | mod days_of_week; 3 | mod hours; 4 | mod minutes; 5 | mod months; 6 | mod seconds; 7 | mod years; 8 | 9 | pub use self::days_of_month::DaysOfMonth; 10 | pub use self::days_of_week::DaysOfWeek; 11 | pub use self::hours::Hours; 12 | pub use self::minutes::Minutes; 13 | pub use self::months::Months; 14 | pub use self::seconds::Seconds; 15 | pub use self::years::Years; 16 | 17 | use crate::error::*; 18 | use crate::ordinal::{Ordinal, OrdinalSet}; 19 | use crate::specifier::{RootSpecifier, Specifier}; 20 | use std::borrow::Cow; 21 | use std::collections::btree_set; 22 | use std::iter; 23 | use std::ops::RangeBounds; 24 | 25 | pub struct OrdinalIter<'a> { 26 | set_iter: btree_set::Iter<'a, Ordinal>, 27 | } 28 | 29 | impl Iterator for OrdinalIter<'_> { 30 | type Item = Ordinal; 31 | fn next(&mut self) -> Option { 32 | self.set_iter.next().copied() 33 | } 34 | } 35 | 36 | impl DoubleEndedIterator for OrdinalIter<'_> { 37 | fn next_back(&mut self) -> Option { 38 | self.set_iter.next_back().copied() 39 | } 40 | } 41 | 42 | pub struct OrdinalRangeIter<'a> { 43 | range_iter: btree_set::Range<'a, Ordinal>, 44 | } 45 | 46 | impl Iterator for OrdinalRangeIter<'_> { 47 | type Item = Ordinal; 48 | fn next(&mut self) -> Option { 49 | self.range_iter.next().copied() 50 | } 51 | } 52 | 53 | impl DoubleEndedIterator for OrdinalRangeIter<'_> { 54 | fn next_back(&mut self) -> Option { 55 | self.range_iter.next_back().copied() 56 | } 57 | } 58 | 59 | /// Methods exposing a schedule's configured ordinals for each individual unit of time. 60 | /// # Example 61 | /// ``` 62 | /// use cron::{Schedule,TimeUnitSpec}; 63 | /// use std::ops::Bound::{Included,Excluded}; 64 | /// use std::str::FromStr; 65 | /// 66 | /// let expression = "* * * * * * 2015-2044"; 67 | /// let schedule = Schedule::from_str(expression).expect("Failed to parse expression."); 68 | /// 69 | /// // Membership 70 | /// assert_eq!(true, schedule.years().includes(2031)); 71 | /// assert_eq!(false, schedule.years().includes(1969)); 72 | /// 73 | /// // Number of years specified 74 | /// assert_eq!(30, schedule.years().count()); 75 | /// 76 | /// // Iterator 77 | /// let mut years_iter = schedule.years().iter(); 78 | /// assert_eq!(Some(2015), years_iter.next()); 79 | /// assert_eq!(Some(2016), years_iter.next()); 80 | /// // ... 81 | /// 82 | /// // Range Iterator 83 | /// let mut five_year_plan = schedule.years().range((Included(2017), Excluded(2017 + 5))); 84 | /// assert_eq!(Some(2017), five_year_plan.next()); 85 | /// assert_eq!(Some(2018), five_year_plan.next()); 86 | /// assert_eq!(Some(2019), five_year_plan.next()); 87 | /// assert_eq!(Some(2020), five_year_plan.next()); 88 | /// assert_eq!(Some(2021), five_year_plan.next()); 89 | /// assert_eq!(None, five_year_plan.next()); 90 | /// ``` 91 | pub trait TimeUnitSpec { 92 | /// Returns true if the provided ordinal was included in the schedule spec for the unit of time 93 | /// being described. 94 | /// # Example 95 | /// ``` 96 | /// use cron::{Schedule,TimeUnitSpec}; 97 | /// use std::str::FromStr; 98 | /// 99 | /// let expression = "* * * * * * 2015-2044"; 100 | /// let schedule = Schedule::from_str(expression).expect("Failed to parse expression."); 101 | /// 102 | /// // Membership 103 | /// assert_eq!(true, schedule.years().includes(2031)); 104 | /// assert_eq!(false, schedule.years().includes(2004)); 105 | /// ``` 106 | fn includes(&self, ordinal: Ordinal) -> bool; 107 | 108 | /// Provides an iterator which will return each included ordinal for this schedule in order from 109 | /// lowest to highest. 110 | /// # Example 111 | /// ``` 112 | /// use cron::{Schedule,TimeUnitSpec}; 113 | /// use std::str::FromStr; 114 | /// 115 | /// let expression = "* * * * 5-8 * *"; 116 | /// let schedule = Schedule::from_str(expression).expect("Failed to parse expression."); 117 | /// 118 | /// // Iterator 119 | /// let mut summer = schedule.months().iter(); 120 | /// assert_eq!(Some(5), summer.next()); 121 | /// assert_eq!(Some(6), summer.next()); 122 | /// assert_eq!(Some(7), summer.next()); 123 | /// assert_eq!(Some(8), summer.next()); 124 | /// assert_eq!(None, summer.next()); 125 | /// ``` 126 | fn iter(&self) -> OrdinalIter<'_>; 127 | 128 | /// Provides an iterator which will return each included ordinal within the specified range. 129 | /// # Example 130 | /// ``` 131 | /// use cron::{Schedule,TimeUnitSpec}; 132 | /// use std::ops::Bound::{Included,Excluded}; 133 | /// use std::str::FromStr; 134 | /// 135 | /// let expression = "* * * 1,15 * * *"; 136 | /// let schedule = Schedule::from_str(expression).expect("Failed to parse expression."); 137 | /// 138 | /// // Range Iterator 139 | /// let mut mid_month_paydays = schedule.days_of_month().range((Included(10), Included(20))); 140 | /// assert_eq!(Some(15), mid_month_paydays.next()); 141 | /// assert_eq!(None, mid_month_paydays.next()); 142 | /// ``` 143 | fn range(&self, range: R) -> OrdinalRangeIter<'_> 144 | where 145 | R: RangeBounds; 146 | 147 | /// Returns the number of ordinals included in the associated schedule 148 | /// # Example 149 | /// ``` 150 | /// use cron::{Schedule,TimeUnitSpec}; 151 | /// use std::str::FromStr; 152 | /// 153 | /// let expression = "* * * 1,15 * * *"; 154 | /// let schedule = Schedule::from_str(expression).expect("Failed to parse expression."); 155 | /// 156 | /// assert_eq!(2, schedule.days_of_month().count()); 157 | /// ``` 158 | fn count(&self) -> u32; 159 | 160 | /// Checks if this TimeUnitSpec is defined as all possibilities (thus created with a '*', '?' or in the case of weekdays '1-7') 161 | /// # Example 162 | /// ``` 163 | /// use cron::{Schedule,TimeUnitSpec}; 164 | /// use std::str::FromStr; 165 | /// 166 | /// let expression = "* * * 1,15 * * *"; 167 | /// let schedule = Schedule::from_str(expression).expect("Failed to parse expression."); 168 | /// 169 | /// assert_eq!(false, schedule.days_of_month().is_all()); 170 | /// assert_eq!(true, schedule.months().is_all()); 171 | /// ``` 172 | fn is_all(&self) -> bool; 173 | } 174 | 175 | impl TimeUnitSpec for T 176 | where 177 | T: TimeUnitField, 178 | { 179 | fn includes(&self, ordinal: Ordinal) -> bool { 180 | self.ordinals().contains(&ordinal) 181 | } 182 | fn iter(&self) -> OrdinalIter<'_> { 183 | OrdinalIter { 184 | set_iter: TimeUnitField::ordinals(self).iter(), 185 | } 186 | } 187 | fn range(&'_ self, range: R) -> OrdinalRangeIter<'_> 188 | where 189 | R: RangeBounds, 190 | { 191 | OrdinalRangeIter { 192 | range_iter: TimeUnitField::ordinals(self).range(range), 193 | } 194 | } 195 | fn count(&self) -> u32 { 196 | self.ordinals().len() as u32 197 | } 198 | 199 | fn is_all(&self) -> bool { 200 | let max_supported_ordinals = Self::inclusive_max() - Self::inclusive_min() + 1; 201 | self.ordinals().len() == max_supported_ordinals as usize 202 | } 203 | } 204 | 205 | pub trait TimeUnitField 206 | where 207 | Self: Sized, 208 | { 209 | fn from_optional_ordinal_set(ordinal_set: Option) -> Self; 210 | fn name() -> Cow<'static, str>; 211 | fn inclusive_min() -> Ordinal; 212 | fn inclusive_max() -> Ordinal; 213 | fn ordinals(&self) -> &OrdinalSet; 214 | 215 | fn from_ordinal(ordinal: Ordinal) -> Self { 216 | Self::from_ordinal_set(iter::once(ordinal).collect()) 217 | } 218 | 219 | fn supported_ordinals() -> OrdinalSet { 220 | (Self::inclusive_min()..Self::inclusive_max() + 1).collect() 221 | } 222 | 223 | fn all() -> Self { 224 | Self::from_optional_ordinal_set(None) 225 | } 226 | 227 | fn from_ordinal_set(ordinal_set: OrdinalSet) -> Self { 228 | Self::from_optional_ordinal_set(Some(ordinal_set)) 229 | } 230 | 231 | fn ordinal_from_name(name: &str) -> Result { 232 | Err(ErrorKind::Expression(format!( 233 | "The '{}' field does not support using names. '{}' \ 234 | specified.", 235 | Self::name(), 236 | name 237 | )) 238 | .into()) 239 | } 240 | fn validate_ordinal(ordinal: Ordinal) -> Result { 241 | //println!("validate_ordinal for {} => {}", Self::name(), ordinal); 242 | match ordinal { 243 | i if i < Self::inclusive_min() => Err(ErrorKind::Expression(format!( 244 | "{} must be greater than or equal to {}. ('{}' \ 245 | specified.)", 246 | Self::name(), 247 | Self::inclusive_min(), 248 | i 249 | )) 250 | .into()), 251 | i if i > Self::inclusive_max() => Err(ErrorKind::Expression(format!( 252 | "{} must be less than {}. ('{}' specified.)", 253 | Self::name(), 254 | Self::inclusive_max(), 255 | i 256 | )) 257 | .into()), 258 | i => Ok(i), 259 | } 260 | } 261 | 262 | fn ordinals_from_specifier(specifier: &Specifier) -> Result { 263 | use self::Specifier::*; 264 | //println!("ordinals_from_specifier for {} => {:?}", Self::name(), specifier); 265 | match *specifier { 266 | All => Ok(Self::supported_ordinals()), 267 | Point(ordinal) => Ok(([ordinal]).iter().cloned().collect()), 268 | Range(start, end) => { 269 | match (Self::validate_ordinal(start), Self::validate_ordinal(end)) { 270 | (Ok(start), Ok(end)) if start <= end => Ok((start..end + 1).collect()), 271 | _ => Err(ErrorKind::Expression(format!( 272 | "Invalid range for {}: {}-{}", 273 | Self::name(), 274 | start, 275 | end 276 | )) 277 | .into()), 278 | } 279 | } 280 | NamedRange(ref start_name, ref end_name) => { 281 | let start = Self::ordinal_from_name(start_name)?; 282 | let end = Self::ordinal_from_name(end_name)?; 283 | match (Self::validate_ordinal(start), Self::validate_ordinal(end)) { 284 | (Ok(start), Ok(end)) if start <= end => Ok((start..end + 1).collect()), 285 | _ => Err(ErrorKind::Expression(format!( 286 | "Invalid named range for {}: {}-{}", 287 | Self::name(), 288 | start_name, 289 | end_name 290 | )) 291 | .into()), 292 | } 293 | } 294 | } 295 | } 296 | 297 | fn ordinals_from_root_specifier(root_specifier: &RootSpecifier) -> Result { 298 | let ordinals = match root_specifier { 299 | RootSpecifier::Specifier(specifier) => Self::ordinals_from_specifier(specifier)?, 300 | RootSpecifier::Period(_, 0) => Err(ErrorKind::Expression( 301 | "range step cannot be zero".to_string(), 302 | ))?, 303 | RootSpecifier::Period(start, step) => { 304 | if *step < 1 || *step > Self::inclusive_max() { 305 | return Err(ErrorKind::Expression(format!( 306 | "{} must be between 1 and {}. ('{}' specified.)", 307 | Self::name(), 308 | Self::inclusive_max(), 309 | step, 310 | )) 311 | .into()); 312 | } 313 | 314 | let base_set = match start { 315 | // A point prior to a period implies a range whose start is the specified 316 | // point and terminating inclusively with the inclusive max 317 | Specifier::Point(start) => { 318 | let start = Self::validate_ordinal(*start)?; 319 | (start..=Self::inclusive_max()).collect() 320 | } 321 | specifier => Self::ordinals_from_specifier(specifier)?, 322 | }; 323 | base_set.into_iter().step_by(*step as usize).collect() 324 | } 325 | RootSpecifier::NamedPoint(ref name) => ([Self::ordinal_from_name(name)?]) 326 | .iter() 327 | .cloned() 328 | .collect::(), 329 | }; 330 | Ok(ordinals) 331 | } 332 | } 333 | -------------------------------------------------------------------------------- /src/parsing.rs: -------------------------------------------------------------------------------- 1 | use winnow::ascii::{alpha1, digit1, multispace0}; 2 | use winnow::combinator::{alt, delimited, eof, opt, separated, separated_pair, terminated}; 3 | use winnow::prelude::*; 4 | 5 | use std::borrow::Cow; 6 | use std::convert::TryFrom; 7 | use std::str::{self, FromStr}; 8 | 9 | use crate::error::{Error, ErrorKind}; 10 | use crate::ordinal::*; 11 | use crate::schedule::{Schedule, ScheduleFields}; 12 | use crate::specifier::*; 13 | use crate::time_unit::*; 14 | 15 | impl TryFrom> for Schedule { 16 | type Error = Error; 17 | 18 | fn try_from(expression: Cow<'_, str>) -> Result { 19 | match schedule.parse(&expression) { 20 | Ok(schedule_fields) => Ok(Schedule::new(expression.into_owned(), schedule_fields)), // Extract from winnow tuple 21 | Err(parse_error) => Err(ErrorKind::Expression(format!("{parse_error}")).into()), 22 | } 23 | } 24 | } 25 | 26 | impl TryFrom for Schedule { 27 | type Error = Error; 28 | 29 | fn try_from(expression: String) -> Result { 30 | Self::try_from(Cow::Owned(expression)) 31 | } 32 | } 33 | 34 | impl TryFrom<&str> for Schedule { 35 | type Error = Error; 36 | 37 | fn try_from(expression: &str) -> Result { 38 | Self::try_from(Cow::Borrowed(expression)) 39 | } 40 | } 41 | 42 | impl FromStr for Schedule { 43 | type Err = Error; 44 | 45 | fn from_str(expression: &str) -> Result { 46 | Self::try_from(Cow::Borrowed(expression)) 47 | } 48 | } 49 | 50 | #[derive(Debug, PartialEq)] 51 | pub struct Field { 52 | pub specifiers: Vec, // TODO: expose iterator? 53 | } 54 | 55 | trait FromField 56 | where 57 | Self: Sized, 58 | { 59 | //TODO: Replace with std::convert::TryFrom when stable 60 | fn from_field(field: Field) -> Result; 61 | } 62 | 63 | impl FromField for T 64 | where 65 | T: TimeUnitField, 66 | { 67 | fn from_field(field: Field) -> Result { 68 | if field.specifiers.len() == 1 69 | && field.specifiers.first().unwrap() == &RootSpecifier::from(Specifier::All) 70 | { 71 | return Ok(T::all()); 72 | } 73 | let mut ordinals = OrdinalSet::new(); 74 | for specifier in field.specifiers { 75 | let specifier_ordinals: OrdinalSet = T::ordinals_from_root_specifier(&specifier)?; 76 | for ordinal in specifier_ordinals { 77 | ordinals.insert(T::validate_ordinal(ordinal)?); 78 | } 79 | } 80 | Ok(T::from_ordinal_set(ordinals)) 81 | } 82 | } 83 | 84 | fn ordinal(i: &mut &str) -> winnow::Result { 85 | delimited(multispace0, digit1, multispace0) 86 | .try_map(u32::from_str) 87 | .parse_next(i) 88 | } 89 | 90 | fn name(i: &mut &str) -> winnow::Result { 91 | delimited(multispace0, alpha1, multispace0) 92 | .map(ToOwned::to_owned) 93 | .parse_next(i) 94 | } 95 | 96 | fn point(i: &mut &str) -> winnow::Result { 97 | ordinal.map(Specifier::Point).parse_next(i) 98 | } 99 | 100 | fn named_point(i: &mut &str) -> winnow::Result { 101 | name.map(RootSpecifier::NamedPoint).parse_next(i) 102 | } 103 | 104 | fn period(i: &mut &str) -> winnow::Result { 105 | separated_pair(specifier, "/", ordinal) 106 | .map(|(start, step)| RootSpecifier::Period(start, step)) 107 | .parse_next(i) 108 | } 109 | 110 | fn period_with_any(i: &mut &str) -> winnow::Result { 111 | separated_pair(specifier_with_any, "/", ordinal) 112 | .map(|(start, step)| RootSpecifier::Period(start, step)) 113 | .parse_next(i) 114 | } 115 | 116 | fn range(i: &mut &str) -> winnow::Result { 117 | separated_pair(ordinal, "-", ordinal) 118 | .map(|(start, end)| Specifier::Range(start, end)) 119 | .parse_next(i) 120 | } 121 | 122 | fn named_range(i: &mut &str) -> winnow::Result { 123 | separated_pair(name, "-", name) 124 | .map(|(start, end)| Specifier::NamedRange(start, end)) 125 | .parse_next(i) 126 | } 127 | 128 | fn all(i: &mut &str) -> winnow::Result { 129 | "*".map(|_| Specifier::All).parse_next(i) 130 | } 131 | 132 | fn any(i: &mut &str) -> winnow::Result { 133 | "?".map(|_| Specifier::All).parse_next(i) 134 | } 135 | 136 | fn specifier(i: &mut &str) -> winnow::Result { 137 | alt((all, range, point, named_range)).parse_next(i) 138 | } 139 | 140 | fn specifier_with_any(i: &mut &str) -> winnow::Result { 141 | alt((any, specifier)).parse_next(i) 142 | } 143 | 144 | fn root_specifier(i: &mut &str) -> winnow::Result { 145 | alt((period, specifier.map(RootSpecifier::from), named_point)).parse_next(i) 146 | } 147 | 148 | fn root_specifier_with_any(i: &mut &str) -> winnow::Result { 149 | alt(( 150 | period_with_any, 151 | specifier_with_any.map(RootSpecifier::from), 152 | named_point, 153 | )) 154 | .parse_next(i) 155 | } 156 | 157 | fn root_specifier_list(i: &mut &str) -> winnow::Result> { 158 | let list = separated(1.., root_specifier, ","); 159 | let single_item = root_specifier.map(|spec| vec![spec]); 160 | delimited(multispace0, alt((list, single_item)), multispace0).parse_next(i) 161 | } 162 | 163 | fn root_specifier_list_with_any(i: &mut &str) -> winnow::Result> { 164 | let list = separated(1.., root_specifier_with_any, ","); 165 | let single_item = root_specifier_with_any.map(|spec| vec![spec]); 166 | delimited(multispace0, alt((list, single_item)), multispace0).parse_next(i) 167 | } 168 | 169 | fn field(i: &mut &str) -> winnow::Result { 170 | let specifiers = root_specifier_list.parse_next(i)?; 171 | Ok(Field { specifiers }) 172 | } 173 | 174 | fn field_with_any(i: &mut &str) -> winnow::Result { 175 | let specifiers = root_specifier_list_with_any.parse_next(i)?; 176 | Ok(Field { specifiers }) 177 | } 178 | 179 | fn shorthand_yearly(i: &mut &str) -> winnow::Result { 180 | "@yearly".parse_next(i)?; 181 | let fields = ScheduleFields::new( 182 | Seconds::from_ordinal(0), 183 | Minutes::from_ordinal(0), 184 | Hours::from_ordinal(0), 185 | DaysOfMonth::from_ordinal(1), 186 | Months::from_ordinal(1), 187 | DaysOfWeek::all(), 188 | Years::all(), 189 | ); 190 | Ok(fields) 191 | } 192 | 193 | fn shorthand_monthly(i: &mut &str) -> winnow::Result { 194 | "@monthly".parse_next(i)?; 195 | let fields = ScheduleFields::new( 196 | Seconds::from_ordinal(0), 197 | Minutes::from_ordinal(0), 198 | Hours::from_ordinal(0), 199 | DaysOfMonth::from_ordinal(1), 200 | Months::all(), 201 | DaysOfWeek::all(), 202 | Years::all(), 203 | ); 204 | Ok(fields) 205 | } 206 | 207 | fn shorthand_weekly(i: &mut &str) -> winnow::Result { 208 | "@weekly".parse_next(i)?; 209 | let fields = ScheduleFields::new( 210 | Seconds::from_ordinal(0), 211 | Minutes::from_ordinal(0), 212 | Hours::from_ordinal(0), 213 | DaysOfMonth::all(), 214 | Months::all(), 215 | DaysOfWeek::from_ordinal(1), 216 | Years::all(), 217 | ); 218 | Ok(fields) 219 | } 220 | 221 | fn shorthand_daily(i: &mut &str) -> winnow::Result { 222 | "@daily".parse_next(i)?; 223 | let fields = ScheduleFields::new( 224 | Seconds::from_ordinal(0), 225 | Minutes::from_ordinal(0), 226 | Hours::from_ordinal(0), 227 | DaysOfMonth::all(), 228 | Months::all(), 229 | DaysOfWeek::all(), 230 | Years::all(), 231 | ); 232 | Ok(fields) 233 | } 234 | 235 | fn shorthand_hourly(i: &mut &str) -> winnow::Result { 236 | "@hourly".parse_next(i)?; 237 | let fields = ScheduleFields::new( 238 | Seconds::from_ordinal(0), 239 | Minutes::from_ordinal(0), 240 | Hours::all(), 241 | DaysOfMonth::all(), 242 | Months::all(), 243 | DaysOfWeek::all(), 244 | Years::all(), 245 | ); 246 | Ok(fields) 247 | } 248 | 249 | fn shorthand(i: &mut &str) -> winnow::Result { 250 | let keywords = alt(( 251 | shorthand_yearly, 252 | shorthand_monthly, 253 | shorthand_weekly, 254 | shorthand_daily, 255 | shorthand_hourly, 256 | )); 257 | delimited(multispace0, keywords, multispace0).parse_next(i) 258 | } 259 | 260 | fn longhand(i: &mut &str) -> winnow::Result { 261 | let seconds = field.try_map(Seconds::from_field); 262 | let minutes = field.try_map(Minutes::from_field); 263 | let hours = field.try_map(Hours::from_field); 264 | let days_of_month = field_with_any.try_map(DaysOfMonth::from_field); 265 | let months = field.try_map(Months::from_field); 266 | let days_of_week = field_with_any.try_map(DaysOfWeek::from_field); 267 | let years = opt(field.try_map(Years::from_field)); 268 | let fields = ( 269 | seconds, 270 | minutes, 271 | hours, 272 | days_of_month, 273 | months, 274 | days_of_week, 275 | years, 276 | ); 277 | 278 | terminated(fields, eof) 279 | .map( 280 | |(seconds, minutes, hours, days_of_month, months, days_of_week, years)| { 281 | let years = years.unwrap_or_else(Years::all); 282 | ScheduleFields::new( 283 | seconds, 284 | minutes, 285 | hours, 286 | days_of_month, 287 | months, 288 | days_of_week, 289 | years, 290 | ) 291 | }, 292 | ) 293 | .parse_next(i) 294 | } 295 | 296 | fn schedule(i: &mut &str) -> winnow::Result { 297 | alt((shorthand, longhand)).parse_next(i) 298 | } 299 | 300 | #[cfg(test)] 301 | mod test { 302 | use super::*; 303 | 304 | #[test] 305 | fn test_nom_valid_number() { 306 | let expression = "1997"; 307 | point.parse(expression).unwrap(); 308 | } 309 | 310 | #[test] 311 | fn test_nom_invalid_point() { 312 | let expression = "a"; 313 | assert!(point.parse(expression).is_err()); 314 | } 315 | 316 | #[test] 317 | fn test_nom_valid_named_point() { 318 | let expression = "WED"; 319 | named_point.parse(expression).unwrap(); 320 | } 321 | 322 | #[test] 323 | fn test_nom_invalid_named_point() { 324 | let expression = "8"; 325 | assert!(named_point.parse(expression).is_err()); 326 | } 327 | 328 | #[test] 329 | fn test_nom_valid_period() { 330 | let expression = "1/2"; 331 | period.parse(expression).unwrap(); 332 | } 333 | 334 | #[test] 335 | fn test_nom_invalid_period() { 336 | let expression = "Wed/4"; 337 | assert!(period.parse(expression).is_err()); 338 | } 339 | 340 | #[test] 341 | fn test_nom_valid_number_list() { 342 | let expression = "1,2"; 343 | field.parse(expression).unwrap(); 344 | field_with_any.parse(expression).unwrap(); 345 | } 346 | 347 | #[test] 348 | fn test_nom_invalid_number_list() { 349 | let expression = ",1,2"; 350 | assert!(field.parse(expression).is_err()); 351 | assert!(field_with_any.parse(expression).is_err()); 352 | } 353 | 354 | #[test] 355 | fn test_nom_field_with_any_valid_any() { 356 | let expression = "?"; 357 | field_with_any.parse(expression).unwrap(); 358 | } 359 | 360 | #[test] 361 | fn test_nom_field_invalid_any() { 362 | let expression = "?"; 363 | assert!(field.parse(expression).is_err()); 364 | } 365 | 366 | #[test] 367 | fn test_nom_valid_range_field() { 368 | let expression = "1-4"; 369 | range.parse(expression).unwrap(); 370 | } 371 | 372 | #[test] 373 | fn test_nom_valid_period_all() { 374 | let expression = "*/2"; 375 | period.parse(expression).unwrap(); 376 | } 377 | 378 | #[test] 379 | fn test_nom_valid_period_range() { 380 | let expression = "10-20/2"; 381 | period.parse(expression).unwrap(); 382 | } 383 | 384 | #[test] 385 | fn test_nom_valid_period_named_range() { 386 | let expression = "Mon-Thurs/2"; 387 | period.parse(expression).unwrap(); 388 | 389 | let expression = "February-November/2"; 390 | period.parse(expression).unwrap(); 391 | } 392 | 393 | #[test] 394 | fn test_nom_valid_period_point() { 395 | let expression = "10/2"; 396 | period.parse(expression).unwrap(); 397 | } 398 | 399 | #[test] 400 | fn test_nom_invalid_period_any() { 401 | let expression = "?/2"; 402 | assert!(period.parse(expression).is_err()); 403 | } 404 | 405 | #[test] 406 | fn test_nom_invalid_period_named_point() { 407 | let expression = "Tues/2"; 408 | assert!(period.parse(expression).is_err()); 409 | 410 | let expression = "February/2"; 411 | assert!(period.parse(expression).is_err()); 412 | } 413 | 414 | #[test] 415 | fn test_nom_invalid_period_specifier_range() { 416 | let expression = "10-12/*"; 417 | assert!(period.parse(expression).is_err()); 418 | } 419 | 420 | #[test] 421 | fn test_nom_valid_period_with_any_all() { 422 | let expression = "*/2"; 423 | period_with_any.parse(expression).unwrap(); 424 | } 425 | 426 | #[test] 427 | fn test_nom_valid_period_with_any_range() { 428 | let expression = "10-20/2"; 429 | period_with_any.parse(expression).unwrap(); 430 | } 431 | 432 | #[test] 433 | fn test_nom_valid_period_with_any_named_range() { 434 | let expression = "Mon-Thurs/2"; 435 | period_with_any.parse(expression).unwrap(); 436 | 437 | let expression = "February-November/2"; 438 | period_with_any.parse(expression).unwrap(); 439 | } 440 | 441 | #[test] 442 | fn test_nom_valid_period_with_any_point() { 443 | let expression = "10/2"; 444 | period_with_any.parse(expression).unwrap(); 445 | } 446 | 447 | #[test] 448 | fn test_nom_valid_period_with_any_any() { 449 | let expression = "?/2"; 450 | period_with_any.parse(expression).unwrap(); 451 | } 452 | 453 | #[test] 454 | fn test_nom_invalid_period_with_any_named_point() { 455 | let expression = "Tues/2"; 456 | assert!(period_with_any.parse(expression).is_err()); 457 | 458 | let expression = "February/2"; 459 | assert!(period_with_any.parse(expression).is_err()); 460 | } 461 | 462 | #[test] 463 | fn test_nom_invalid_period_with_any_specifier_range() { 464 | let expression = "10-12/*"; 465 | assert!(period_with_any.parse(expression).is_err()); 466 | } 467 | 468 | #[test] 469 | fn test_nom_invalid_range_field() { 470 | let expression = "-4"; 471 | assert!(range.parse(expression).is_err()); 472 | } 473 | 474 | #[test] 475 | fn test_nom_valid_named_range_field() { 476 | let expression = "TUES-THURS"; 477 | named_range.parse(expression).unwrap(); 478 | } 479 | 480 | #[test] 481 | fn test_nom_invalid_named_range_field() { 482 | let expression = "3-THURS"; 483 | assert!(named_range.parse(expression).is_err()); 484 | } 485 | 486 | #[test] 487 | fn test_nom_valid_schedule() { 488 | let expression = "* * * * * *"; 489 | schedule.parse(expression).unwrap(); 490 | } 491 | 492 | #[test] 493 | fn test_nom_invalid_schedule() { 494 | let expression = "* * * *"; 495 | assert!(schedule.parse(expression).is_err()); 496 | } 497 | 498 | #[test] 499 | fn test_nom_valid_seconds_list() { 500 | let expression = "0,20,40 * * * * *"; 501 | schedule.parse(expression).unwrap(); 502 | } 503 | 504 | #[test] 505 | fn test_nom_valid_seconds_range() { 506 | let expression = "0-40 * * * * *"; 507 | schedule.parse(expression).unwrap(); 508 | } 509 | 510 | #[test] 511 | fn test_nom_valid_seconds_mix() { 512 | let expression = "0-5,58 * * * * *"; 513 | schedule.parse(expression).unwrap(); 514 | } 515 | 516 | #[test] 517 | fn test_nom_invalid_seconds_range() { 518 | let expression = "0-65 * * * * *"; 519 | assert!(schedule.parse(expression).is_err()); 520 | } 521 | 522 | #[test] 523 | fn test_nom_invalid_seconds_list() { 524 | let expression = "103,12 * * * * *"; 525 | assert!(schedule.parse(expression).is_err()); 526 | } 527 | 528 | #[test] 529 | fn test_nom_invalid_seconds_mix() { 530 | let expression = "0-5,102 * * * * *"; 531 | assert!(schedule.parse(expression).is_err()); 532 | } 533 | 534 | #[test] 535 | fn test_nom_valid_days_of_week_list() { 536 | let expression = "* * * * * MON,WED,FRI"; 537 | schedule.parse(expression).unwrap(); 538 | } 539 | 540 | #[test] 541 | fn test_nom_invalid_days_of_week_list() { 542 | let expression = "* * * * * MON,TURTLE"; 543 | assert!(schedule.parse(expression).is_err()); 544 | } 545 | 546 | #[test] 547 | fn test_nom_valid_days_of_week_range() { 548 | let expression = "* * * * * MON-FRI"; 549 | schedule.parse(expression).unwrap(); 550 | } 551 | 552 | #[test] 553 | fn test_nom_invalid_days_of_week_range() { 554 | let expression = "* * * * * BEAR-OWL"; 555 | assert!(schedule.parse(expression).is_err()); 556 | } 557 | 558 | #[test] 559 | fn test_nom_invalid_period_with_range_specifier() { 560 | let expression = "10-12/10-12 * * * * ?"; 561 | assert!(schedule.parse(expression).is_err()); 562 | } 563 | 564 | #[test] 565 | fn test_nom_valid_days_of_month_any() { 566 | let expression = "* * * ? * *"; 567 | schedule.parse(expression).unwrap(); 568 | } 569 | 570 | #[test] 571 | fn test_nom_valid_days_of_week_any() { 572 | let expression = "* * * * * ?"; 573 | schedule.parse(expression).unwrap(); 574 | } 575 | 576 | #[test] 577 | fn test_nom_valid_days_of_month_any_days_of_week_specific() { 578 | let expression = "* * * ? * Mon,Thu"; 579 | schedule.parse(expression).unwrap(); 580 | } 581 | 582 | #[test] 583 | fn test_nom_valid_days_of_week_any_days_of_month_specific() { 584 | let expression = "* * * 1,2 * ?"; 585 | schedule.parse(expression).unwrap(); 586 | } 587 | 588 | #[test] 589 | fn test_nom_valid_dom_and_dow_any() { 590 | let expression = "* * * ? * ?"; 591 | schedule.parse(expression).unwrap(); 592 | } 593 | 594 | #[test] 595 | fn test_nom_invalid_other_fields_any() { 596 | let expression = "? * * * * *"; 597 | assert!(schedule.parse(expression).is_err()); 598 | 599 | let expression = "* ? * * * *"; 600 | assert!(schedule.parse(expression).is_err()); 601 | 602 | let expression = "* * ? * * *"; 603 | assert!(schedule.parse(expression).is_err()); 604 | 605 | let expression = "* * * * ? *"; 606 | assert!(schedule.parse(expression).is_err()); 607 | } 608 | 609 | #[test] 610 | fn test_nom_invalid_trailing_characters() { 611 | let expression = "* * * * * *foo *"; 612 | assert!(schedule.parse(expression).is_err()); 613 | 614 | let expression = "* * * * * * * foo"; 615 | assert!(schedule.parse(expression).is_err()); 616 | } 617 | 618 | /// Issue #86 619 | #[test] 620 | fn shorthand_must_match_whole_input() { 621 | let expression = "@dailyBla"; 622 | assert!(schedule.parse(expression).is_err()); 623 | let expression = " @dailyBla "; 624 | assert!(schedule.parse(expression).is_err()); 625 | } 626 | 627 | #[test] 628 | fn test_try_from_cow_str_owned() { 629 | let expression = Cow::Owned(String::from("* * * ? * ?")); 630 | Schedule::try_from(expression).unwrap(); 631 | } 632 | 633 | #[test] 634 | fn test_try_from_cow_str_borrowed() { 635 | let expression = Cow::Borrowed("* * * ? * ?"); 636 | Schedule::try_from(expression).unwrap(); 637 | } 638 | 639 | #[test] 640 | fn test_try_from_string() { 641 | let expression = String::from("* * * ? * ?"); 642 | Schedule::try_from(expression).unwrap(); 643 | } 644 | 645 | #[test] 646 | fn test_try_from_str() { 647 | let expression = "* * * ? * ?"; 648 | Schedule::try_from(expression).unwrap(); 649 | } 650 | 651 | #[test] 652 | fn test_from_str() { 653 | let expression = "* * * ? * ?"; 654 | Schedule::from_str(expression).unwrap(); 655 | } 656 | 657 | /// Issue #59 658 | #[test] 659 | fn test_reject_invalid_interval() { 660 | for invalid_expression in [ 661 | "1-5/61 * * * * *", 662 | "*/61 2 3 4 5 6", 663 | "* */61 * * * *", 664 | "* * */25 * * *", 665 | "* * * */32 * *", 666 | "* * * * */13 *", 667 | "1,2,3/60 * * * * *", 668 | "0 0 0 1 1 ? 2020-2040/2200", 669 | ] { 670 | assert!(schedule.parse(invalid_expression).is_err()); 671 | } 672 | 673 | for valid_expression in [ 674 | "1-5/59 * * * * *", 675 | "*/10 2 3 4 5 6", 676 | "* */30 * * * *", 677 | "* * */23 * * *", 678 | "* * * */30 * *", 679 | "* * * * */10 *", 680 | "1,2,3/5 * * * * *", 681 | "0 0 0 1 1 ? 2020-2040/10", 682 | ] { 683 | assert!(schedule.parse(valid_expression).is_ok()); 684 | } 685 | } 686 | } 687 | -------------------------------------------------------------------------------- /tests/lib.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | mod tests { 3 | use chrono::*; 4 | use chrono_tz::Tz; 5 | use cron::{Schedule, TimeUnitSpec}; 6 | use std::ops::Bound::{Excluded, Included}; 7 | use std::str::FromStr; 8 | 9 | #[test] 10 | fn test_readme() { 11 | let expression = "0 30 9,12,15 1,15 May-Aug Mon,Wed,Fri 2018/2"; 12 | let schedule = Schedule::from_str(expression).unwrap(); 13 | println!("README: Upcoming fire times for '{}':", expression); 14 | for datetime in schedule.upcoming(Utc).take(10) { 15 | println!("README: -> {}", datetime); 16 | } 17 | } 18 | 19 | #[test] 20 | fn test_anything_goes() { 21 | let expression = "* * * * * * *"; 22 | let schedule = Schedule::from_str(expression).unwrap(); 23 | println!("All stars: Upcoming fire times for '{}':", expression); 24 | for datetime in schedule.upcoming(Utc).take(10) { 25 | println!("All stars: -> {}", datetime); 26 | } 27 | } 28 | 29 | #[test] 30 | fn test_parse_with_year() { 31 | let expression = "1 2 3 4 5 6 2015"; 32 | assert!(Schedule::from_str(expression).is_ok()); 33 | } 34 | 35 | #[test] 36 | fn test_parse_with_seconds_list() { 37 | let expression = "1,30,40 2 3 4 5 Mon-Fri"; 38 | assert!(Schedule::from_str(expression).is_ok()); 39 | } 40 | 41 | #[test] 42 | fn test_parse_with_lists() { 43 | let expression = "1 2,17,51 1-3,6,9-11 4,29 2,3,7 Tues"; 44 | let schedule = Schedule::from_str(expression).unwrap(); 45 | let mut date = Utc::now(); 46 | println!("Fire times for {}:", expression); 47 | for _ in 0..20 { 48 | date = schedule.after(&date).next().expect("No further dates!"); 49 | println!("-> {}", date); 50 | } 51 | } 52 | 53 | #[test] 54 | fn test_upcoming_iterator() { 55 | let expression = "0 2,17,51 1-3,6,9-11 4,29 2,3,7 Wed"; 56 | let schedule = Schedule::from_str(expression).unwrap(); 57 | println!("Upcoming fire times for '{}':", expression); 58 | for datetime in schedule.upcoming(Utc).take(12) { 59 | println!("-> {}", datetime); 60 | } 61 | } 62 | 63 | #[test] 64 | fn test_parse_without_year() { 65 | let expression = "1 2 3 4 5 6"; 66 | assert!(Schedule::from_str(expression).is_ok()); 67 | } 68 | 69 | #[test] 70 | fn test_parse_too_many_fields() { 71 | let expression = "1 2 3 4 5 6 7 8 9 2019"; 72 | assert!(Schedule::from_str(expression).is_err()); 73 | } 74 | 75 | #[test] 76 | fn test_not_enough_fields() { 77 | let expression = "1 2 3 2019"; 78 | assert!(Schedule::from_str(expression).is_err()); 79 | } 80 | 81 | #[test] 82 | fn test_next_utc() { 83 | let expression = "1 2 3 4 10 Fri"; 84 | let schedule = Schedule::from_str(expression).unwrap(); 85 | let next = schedule 86 | .upcoming(Utc) 87 | .next() 88 | .expect("There was no upcoming fire time."); 89 | println!("Next fire time: {}", next.to_rfc3339()); 90 | } 91 | 92 | #[test] 93 | fn test_prev_utc() { 94 | let expression = "1 2 3 4 10 Fri"; 95 | let schedule = Schedule::from_str(expression).unwrap(); 96 | let prev = schedule 97 | .upcoming(Utc) 98 | .next_back() 99 | .expect("There was no previous upcoming fire time."); 100 | println!("Previous fire time: {}", prev.to_rfc3339()); 101 | } 102 | 103 | #[test] 104 | fn test_yearly() { 105 | let expression = "@yearly"; 106 | let schedule = Schedule::from_str(expression).expect("Failed to parse @yearly."); 107 | let starting_date = Utc.with_ymd_and_hms(2017, 6, 15, 14, 29, 36).unwrap(); 108 | let mut events = schedule.after(&starting_date); 109 | assert_eq!( 110 | Utc.with_ymd_and_hms(2018, 1, 1, 0, 0, 0).unwrap(), 111 | events.next().unwrap() 112 | ); 113 | assert_eq!( 114 | Utc.with_ymd_and_hms(2019, 1, 1, 0, 0, 0).unwrap(), 115 | events.next().unwrap() 116 | ); 117 | assert_eq!( 118 | Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap(), 119 | events.next().unwrap() 120 | ); 121 | } 122 | 123 | #[test] 124 | fn test_monthly() { 125 | let expression = "@monthly"; 126 | let schedule = Schedule::from_str(expression).expect("Failed to parse @monthly."); 127 | let starting_date = Utc.with_ymd_and_hms(2017, 10, 15, 14, 29, 36).unwrap(); 128 | let mut events = schedule.after(&starting_date); 129 | assert_eq!( 130 | Utc.with_ymd_and_hms(2017, 11, 1, 0, 0, 0).unwrap(), 131 | events.next().unwrap() 132 | ); 133 | assert_eq!( 134 | Utc.with_ymd_and_hms(2017, 12, 1, 0, 0, 0).unwrap(), 135 | events.next().unwrap() 136 | ); 137 | assert_eq!( 138 | Utc.with_ymd_and_hms(2018, 1, 1, 0, 0, 0).unwrap(), 139 | events.next().unwrap() 140 | ); 141 | } 142 | 143 | #[test] 144 | fn test_weekly() { 145 | let expression = "@weekly"; 146 | let schedule = Schedule::from_str(expression).expect("Failed to parse @weekly."); 147 | let starting_date = Utc.with_ymd_and_hms(2016, 12, 23, 14, 29, 36).unwrap(); 148 | let mut events = schedule.after(&starting_date); 149 | assert_eq!( 150 | Utc.with_ymd_and_hms(2016, 12, 25, 0, 0, 0).unwrap(), 151 | events.next().unwrap() 152 | ); 153 | assert_eq!( 154 | Utc.with_ymd_and_hms(2017, 1, 1, 0, 0, 0).unwrap(), 155 | events.next().unwrap() 156 | ); 157 | assert_eq!( 158 | Utc.with_ymd_and_hms(2017, 1, 8, 0, 0, 0).unwrap(), 159 | events.next().unwrap() 160 | ); 161 | } 162 | 163 | #[test] 164 | fn test_daily() { 165 | let expression = "@daily"; 166 | let schedule = Schedule::from_str(expression).expect("Failed to parse @daily."); 167 | let starting_date = Utc.with_ymd_and_hms(2016, 12, 29, 14, 29, 36).unwrap(); 168 | let mut events = schedule.after(&starting_date); 169 | assert_eq!( 170 | Utc.with_ymd_and_hms(2016, 12, 30, 0, 0, 0).unwrap(), 171 | events.next().unwrap() 172 | ); 173 | assert_eq!( 174 | Utc.with_ymd_and_hms(2016, 12, 31, 0, 0, 0).unwrap(), 175 | events.next().unwrap() 176 | ); 177 | assert_eq!( 178 | Utc.with_ymd_and_hms(2017, 1, 1, 0, 0, 0).unwrap(), 179 | events.next().unwrap() 180 | ); 181 | } 182 | 183 | #[test] 184 | fn test_hourly() { 185 | let expression = "@hourly"; 186 | let schedule = Schedule::from_str(expression).expect("Failed to parse @hourly."); 187 | let starting_date = Utc.with_ymd_and_hms(2017, 2, 25, 22, 29, 36).unwrap(); 188 | let mut events = schedule.after(&starting_date); 189 | assert_eq!( 190 | Utc.with_ymd_and_hms(2017, 2, 25, 23, 0, 0).unwrap(), 191 | events.next().unwrap() 192 | ); 193 | assert_eq!( 194 | Utc.with_ymd_and_hms(2017, 2, 26, 0, 0, 0).unwrap(), 195 | events.next().unwrap() 196 | ); 197 | assert_eq!( 198 | Utc.with_ymd_and_hms(2017, 2, 26, 1, 0, 0).unwrap(), 199 | events.next().unwrap() 200 | ); 201 | } 202 | 203 | #[test] 204 | fn test_step_schedule() { 205 | let expression = "0/20 0/5 0 1 1 * *"; 206 | let schedule = Schedule::from_str(expression).expect("Failed to parse expression."); 207 | let starting_date = Utc.with_ymd_and_hms(2017, 6, 15, 14, 29, 36).unwrap(); 208 | let mut events = schedule.after(&starting_date); 209 | 210 | assert_eq!( 211 | Utc.with_ymd_and_hms(2018, 1, 1, 0, 0, 0).unwrap(), 212 | events.next().unwrap() 213 | ); 214 | assert_eq!( 215 | Utc.with_ymd_and_hms(2018, 1, 1, 0, 0, 20).unwrap(), 216 | events.next().unwrap() 217 | ); 218 | assert_eq!( 219 | Utc.with_ymd_and_hms(2018, 1, 1, 0, 0, 40).unwrap(), 220 | events.next().unwrap() 221 | ); 222 | 223 | assert_eq!( 224 | Utc.with_ymd_and_hms(2018, 1, 1, 0, 5, 0).unwrap(), 225 | events.next().unwrap() 226 | ); 227 | assert_eq!( 228 | Utc.with_ymd_and_hms(2018, 1, 1, 0, 5, 20).unwrap(), 229 | events.next().unwrap() 230 | ); 231 | assert_eq!( 232 | Utc.with_ymd_and_hms(2018, 1, 1, 0, 5, 40).unwrap(), 233 | events.next().unwrap() 234 | ); 235 | 236 | assert_eq!( 237 | Utc.with_ymd_and_hms(2018, 1, 1, 0, 10, 0).unwrap(), 238 | events.next().unwrap() 239 | ); 240 | assert_eq!( 241 | Utc.with_ymd_and_hms(2018, 1, 1, 0, 10, 20).unwrap(), 242 | events.next().unwrap() 243 | ); 244 | assert_eq!( 245 | Utc.with_ymd_and_hms(2018, 1, 1, 0, 10, 40).unwrap(), 246 | events.next().unwrap() 247 | ); 248 | } 249 | 250 | #[test] 251 | fn test_invalid_step() { 252 | let expression = "0/0 * * * *"; 253 | assert!(Schedule::from_str(expression).is_err()); 254 | } 255 | 256 | #[test] 257 | fn test_time_unit_spec_years() { 258 | let expression = "* * * * * * 2015-2044"; 259 | let schedule = Schedule::from_str(expression).expect("Failed to parse expression."); 260 | 261 | // Membership 262 | assert!(schedule.years().includes(2031)); 263 | assert!(!schedule.years().includes(1969)); 264 | 265 | // Number of years specified 266 | assert_eq!(30, schedule.years().count()); 267 | 268 | // Iterator 269 | let mut years_iter = schedule.years().iter(); 270 | assert_eq!(Some(2015), years_iter.next()); 271 | assert_eq!(Some(2016), years_iter.next()); 272 | // ... 273 | 274 | // Range Iterator 275 | let mut five_year_plan = schedule.years().range((Included(2017), Excluded(2017 + 5))); 276 | assert_eq!(Some(2017), five_year_plan.next()); 277 | assert_eq!(Some(2018), five_year_plan.next()); 278 | assert_eq!(Some(2019), five_year_plan.next()); 279 | assert_eq!(Some(2020), five_year_plan.next()); 280 | assert_eq!(Some(2021), five_year_plan.next()); 281 | assert_eq!(None, five_year_plan.next()); 282 | } 283 | 284 | #[test] 285 | fn test_time_unit_spec_months() { 286 | let expression = "* * * * 5-8 * *"; 287 | let schedule = Schedule::from_str(expression).expect("Failed to parse expression."); 288 | 289 | // Membership 290 | assert!(!schedule.months().includes(4)); 291 | assert!(schedule.months().includes(6)); 292 | 293 | // Iterator 294 | let mut summer = schedule.months().iter(); 295 | assert_eq!(Some(5), summer.next()); 296 | assert_eq!(Some(6), summer.next()); 297 | assert_eq!(Some(7), summer.next()); 298 | assert_eq!(Some(8), summer.next()); 299 | assert_eq!(None, summer.next()); 300 | 301 | // Number of months specified 302 | assert_eq!(4, schedule.months().count()); 303 | 304 | // Range Iterator 305 | let mut first_half_of_summer = schedule.months().range((Included(1), Included(6))); 306 | assert_eq!(Some(5), first_half_of_summer.next()); 307 | assert_eq!(Some(6), first_half_of_summer.next()); 308 | assert_eq!(None, first_half_of_summer.next()); 309 | } 310 | 311 | #[test] 312 | fn test_time_unit_spec_days_of_month() { 313 | let expression = "* * * 1,15 * * *"; 314 | let schedule = Schedule::from_str(expression).expect("Failed to parse expression."); 315 | // Membership 316 | assert!(schedule.days_of_month().includes(1)); 317 | assert!(!schedule.days_of_month().includes(7)); 318 | 319 | // Iterator 320 | let mut paydays = schedule.days_of_month().iter(); 321 | assert_eq!(Some(1), paydays.next()); 322 | assert_eq!(Some(15), paydays.next()); 323 | assert_eq!(None, paydays.next()); 324 | 325 | // Number of years specified 326 | assert_eq!(2, schedule.days_of_month().count()); 327 | 328 | // Range Iterator 329 | let mut mid_month_paydays = schedule.days_of_month().range((Included(5), Included(25))); 330 | assert_eq!(Some(15), mid_month_paydays.next()); 331 | assert_eq!(None, mid_month_paydays.next()); 332 | } 333 | 334 | #[test] 335 | fn test_first_ordinals_not_in_set_1() { 336 | let schedule = "0 0/10 * * * * *".parse::().unwrap(); 337 | let start_time_1 = NaiveDate::from_ymd_opt(2017, 10, 24) 338 | .unwrap() 339 | .and_hms_opt(0, 0, 59) 340 | .unwrap(); 341 | let start_time_1 = Utc.from_utc_datetime(&start_time_1); 342 | let next_time_1 = schedule.after(&start_time_1).next().unwrap(); 343 | 344 | let start_time_2 = NaiveDate::from_ymd_opt(2017, 10, 24) 345 | .unwrap() 346 | .and_hms_opt(0, 1, 0) 347 | .unwrap(); 348 | let start_time_2 = Utc.from_utc_datetime(&start_time_2); 349 | let next_time_2 = schedule.after(&start_time_2).next().unwrap(); 350 | assert_eq!(next_time_1, next_time_2); 351 | } 352 | 353 | #[test] 354 | fn test_first_ordinals_not_in_set_2() { 355 | let schedule_1 = "00 00 23 * * * *".parse::().unwrap(); 356 | let start_time = NaiveDate::from_ymd_opt(2018, 11, 15) 357 | .unwrap() 358 | .and_hms_opt(22, 30, 00) 359 | .unwrap(); 360 | let start_time = Utc.from_utc_datetime(&start_time); 361 | let next_time_1 = schedule_1.after(&start_time).next().unwrap(); 362 | 363 | let schedule_2 = "00 00 * * * * *".parse::().unwrap(); 364 | let next_time_2 = schedule_2.after(&start_time).next().unwrap(); 365 | assert_eq!(next_time_1, next_time_2); 366 | } 367 | 368 | #[test] 369 | fn test_period_values_any_dom() { 370 | let schedule = Schedule::from_str("0 0 0 ? * *").unwrap(); 371 | let schedule_tz: Tz = "Europe/London".parse().unwrap(); 372 | let dt = schedule_tz.with_ymd_and_hms(2020, 9, 17, 0, 0, 0).unwrap(); 373 | let mut schedule_iter = schedule.after(&dt); 374 | assert_eq!( 375 | schedule_tz.with_ymd_and_hms(2020, 9, 18, 0, 0, 0).unwrap(), 376 | schedule_iter.next().unwrap() 377 | ); 378 | } 379 | 380 | #[test] 381 | fn test_period_values_any_dow() { 382 | let schedule = Schedule::from_str("0 0 0 * * ?").unwrap(); 383 | let schedule_tz: Tz = "Europe/London".parse().unwrap(); 384 | let dt = schedule_tz.with_ymd_and_hms(2020, 9, 17, 0, 0, 0).unwrap(); 385 | let mut schedule_iter = schedule.after(&dt); 386 | assert_eq!( 387 | schedule_tz.with_ymd_and_hms(2020, 9, 18, 0, 0, 0).unwrap(), 388 | schedule_iter.next().unwrap() 389 | ); 390 | } 391 | 392 | #[test] 393 | fn test_period_values_all_seconds() { 394 | let schedule = Schedule::from_str("*/17 * * * * ?").unwrap(); 395 | let schedule_tz: Tz = "Europe/London".parse().unwrap(); 396 | let dt = schedule_tz.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap(); 397 | let mut schedule_iter = schedule.after(&dt); 398 | let expected_values = vec![ 399 | schedule_tz.with_ymd_and_hms(2020, 1, 1, 0, 0, 17).unwrap(), 400 | schedule_tz.with_ymd_and_hms(2020, 1, 1, 0, 0, 34).unwrap(), 401 | schedule_tz.with_ymd_and_hms(2020, 1, 1, 0, 0, 51).unwrap(), 402 | schedule_tz.with_ymd_and_hms(2020, 1, 1, 0, 1, 0).unwrap(), 403 | schedule_tz.with_ymd_and_hms(2020, 1, 1, 0, 1, 17).unwrap(), 404 | schedule_tz.with_ymd_and_hms(2020, 1, 1, 0, 1, 34).unwrap(), 405 | ]; 406 | for expected_value in expected_values.iter() { 407 | assert_eq!(*expected_value, schedule_iter.next().unwrap()); 408 | } 409 | } 410 | 411 | #[test] 412 | fn test_period_values_range() { 413 | let schedule = Schedule::from_str("0 0 0 1 1-4/2 ?").unwrap(); 414 | let schedule_tz: Tz = "Europe/London".parse().unwrap(); 415 | let dt = schedule_tz.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap(); 416 | let mut schedule_iter = schedule.after(&dt); 417 | let expected_values = [ 418 | schedule_tz.with_ymd_and_hms(2020, 3, 1, 0, 0, 0).unwrap(), 419 | schedule_tz.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap(), 420 | schedule_tz.with_ymd_and_hms(2021, 3, 1, 0, 0, 0).unwrap(), 421 | schedule_tz.with_ymd_and_hms(2022, 1, 1, 0, 0, 0).unwrap(), 422 | ]; 423 | for expected_value in expected_values.iter() { 424 | assert_eq!(*expected_value, schedule_iter.next().unwrap()); 425 | } 426 | } 427 | 428 | #[test] 429 | fn test_period_values_range_hours() { 430 | let schedule = Schedule::from_str("0 0 10-12/2 * * ?").unwrap(); 431 | let schedule_tz: Tz = "Europe/London".parse().unwrap(); 432 | let dt = schedule_tz.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap(); 433 | let mut schedule_iter = schedule.after(&dt); 434 | let expected_values = [ 435 | schedule_tz.with_ymd_and_hms(2020, 1, 1, 10, 0, 0).unwrap(), 436 | schedule_tz.with_ymd_and_hms(2020, 1, 1, 12, 0, 0).unwrap(), 437 | schedule_tz.with_ymd_and_hms(2020, 1, 2, 10, 0, 0).unwrap(), 438 | schedule_tz.with_ymd_and_hms(2020, 1, 2, 12, 0, 0).unwrap(), 439 | ]; 440 | for expected_value in expected_values.iter() { 441 | assert_eq!(*expected_value, schedule_iter.next().unwrap()); 442 | } 443 | } 444 | 445 | #[test] 446 | fn test_period_values_range_days() { 447 | let schedule = Schedule::from_str("0 0 0 1-31/10 * ?").unwrap(); 448 | let schedule_tz: Tz = "Europe/London".parse().unwrap(); 449 | let dt = schedule_tz.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap(); 450 | let mut schedule_iter = schedule.after(&dt); 451 | let expected_values = vec![ 452 | schedule_tz.with_ymd_and_hms(2020, 1, 11, 0, 0, 0).unwrap(), 453 | schedule_tz.with_ymd_and_hms(2020, 1, 21, 0, 0, 0).unwrap(), 454 | schedule_tz.with_ymd_and_hms(2020, 1, 31, 0, 0, 0).unwrap(), 455 | schedule_tz.with_ymd_and_hms(2020, 2, 1, 0, 0, 0).unwrap(), 456 | schedule_tz.with_ymd_and_hms(2020, 2, 11, 0, 0, 0).unwrap(), 457 | schedule_tz.with_ymd_and_hms(2020, 2, 21, 0, 0, 0).unwrap(), 458 | schedule_tz.with_ymd_and_hms(2020, 3, 1, 0, 0, 0).unwrap(), 459 | ]; 460 | for expected_value in expected_values.iter() { 461 | assert_eq!(*expected_value, schedule_iter.next().unwrap()); 462 | } 463 | } 464 | 465 | #[test] 466 | fn test_period_values_range_months() { 467 | let schedule = Schedule::from_str("0 0 0 1 January-June/1 *").unwrap(); 468 | let schedule_tz: Tz = "Europe/London".parse().unwrap(); 469 | let dt = schedule_tz.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap(); 470 | let mut schedule_iter = schedule.after(&dt); 471 | let expected_values = vec![ 472 | schedule_tz.with_ymd_and_hms(2020, 2, 1, 0, 0, 0).unwrap(), 473 | schedule_tz.with_ymd_and_hms(2020, 3, 1, 0, 0, 0).unwrap(), 474 | schedule_tz.with_ymd_and_hms(2020, 4, 1, 0, 0, 0).unwrap(), 475 | schedule_tz.with_ymd_and_hms(2020, 5, 1, 0, 0, 0).unwrap(), 476 | schedule_tz.with_ymd_and_hms(2020, 6, 1, 0, 0, 0).unwrap(), 477 | schedule_tz.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap(), 478 | ]; 479 | for expected_value in expected_values.iter() { 480 | assert_eq!(*expected_value, schedule_iter.next().unwrap()); 481 | } 482 | } 483 | 484 | #[test] 485 | fn test_period_values_range_years() { 486 | let schedule = Schedule::from_str("0 0 0 1 1 ? 2020-2040/10").unwrap(); 487 | let schedule_tz: Tz = "Europe/London".parse().unwrap(); 488 | let dt = schedule_tz.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap(); 489 | let mut schedule_iter = schedule.after(&dt); 490 | let expected_values = [ 491 | schedule_tz.with_ymd_and_hms(2030, 1, 1, 0, 0, 0).unwrap(), 492 | schedule_tz.with_ymd_and_hms(2040, 1, 1, 0, 0, 0).unwrap(), 493 | ]; 494 | for expected_value in expected_values.iter() { 495 | assert_eq!(*expected_value, schedule_iter.next().unwrap()); 496 | } 497 | } 498 | 499 | #[test] 500 | fn test_period_values_point() { 501 | let schedule = Schedule::from_str("0 */21 * * * ?").unwrap(); 502 | let schedule_tz: Tz = "Europe/London".parse().unwrap(); 503 | let dt = schedule_tz.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap(); 504 | let mut schedule_iter = schedule.after(&dt); 505 | let expected_values = vec![ 506 | schedule_tz.with_ymd_and_hms(2020, 1, 1, 0, 21, 0).unwrap(), 507 | schedule_tz.with_ymd_and_hms(2020, 1, 1, 0, 42, 0).unwrap(), 508 | schedule_tz.with_ymd_and_hms(2020, 1, 1, 1, 0, 0).unwrap(), 509 | schedule_tz.with_ymd_and_hms(2020, 1, 1, 1, 21, 0).unwrap(), 510 | schedule_tz.with_ymd_and_hms(2020, 1, 1, 1, 42, 0).unwrap(), 511 | schedule_tz.with_ymd_and_hms(2020, 1, 1, 2, 0, 0).unwrap(), 512 | schedule_tz.with_ymd_and_hms(2020, 1, 1, 2, 21, 0).unwrap(), 513 | schedule_tz.with_ymd_and_hms(2020, 1, 1, 2, 42, 0).unwrap(), 514 | ]; 515 | for expected_value in expected_values.iter() { 516 | assert_eq!(*expected_value, schedule_iter.next().unwrap()); 517 | } 518 | } 519 | 520 | #[test] 521 | fn test_period_values_named_range() { 522 | let schedule = Schedule::from_str("0 0 0 1 January-April/2 ?").unwrap(); 523 | let schedule_tz: Tz = "Europe/London".parse().unwrap(); 524 | let dt = schedule_tz.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap(); 525 | let mut schedule_iter = schedule.after(&dt); 526 | let expected_values = [ 527 | schedule_tz.with_ymd_and_hms(2020, 3, 1, 0, 0, 0).unwrap(), 528 | schedule_tz.with_ymd_and_hms(2021, 1, 1, 0, 0, 0).unwrap(), 529 | schedule_tz.with_ymd_and_hms(2021, 3, 1, 0, 0, 0).unwrap(), 530 | schedule_tz.with_ymd_and_hms(2022, 1, 1, 0, 0, 0).unwrap(), 531 | ]; 532 | for expected_value in expected_values.iter() { 533 | assert_eq!(*expected_value, schedule_iter.next().unwrap()); 534 | } 535 | } 536 | 537 | #[test] 538 | fn test_is_all() { 539 | let schedule = Schedule::from_str("0-59 * 0-23 ?/2 1,2-4 ? *").unwrap(); 540 | assert!(schedule.years().is_all()); 541 | assert!(!schedule.days_of_month().is_all()); 542 | assert!(schedule.days_of_week().is_all()); 543 | assert!(!schedule.months().is_all()); 544 | assert!(schedule.hours().is_all()); 545 | assert!(schedule.minutes().is_all()); 546 | assert!(schedule.seconds().is_all()); 547 | } 548 | 549 | #[test] 550 | fn test_includes() { 551 | let schedule = Schedule::from_str("0 0 0 2-31/10 * ?").unwrap(); 552 | let schedule_tz: Tz = "Europe/London".parse().unwrap(); 553 | let included = schedule_tz.with_ymd_and_hms(2020, 1, 12, 0, 0, 0).unwrap(); 554 | let not_included = schedule_tz.with_ymd_and_hms(2020, 1, 11, 0, 0, 0).unwrap(); 555 | assert!(schedule.includes(included)); 556 | assert!(!schedule.includes(not_included)); 557 | } 558 | } 559 | -------------------------------------------------------------------------------- /src/schedule.rs: -------------------------------------------------------------------------------- 1 | use chrono::offset::{LocalResult, TimeZone}; 2 | use chrono::{DateTime, Datelike, Timelike, Utc}; 3 | use std::fmt::{Display, Formatter, Result as FmtResult}; 4 | use std::ops::Bound::{Included, Unbounded}; 5 | 6 | #[cfg(feature = "serde")] 7 | use core::fmt; 8 | #[cfg(feature = "serde")] 9 | use serde::{ 10 | de::{self, Visitor}, 11 | Deserialize, Serialize, Serializer, 12 | }; 13 | 14 | use crate::ordinal::*; 15 | use crate::queries::*; 16 | use crate::time_unit::*; 17 | 18 | impl From for String { 19 | fn from(schedule: Schedule) -> String { 20 | schedule.source 21 | } 22 | } 23 | 24 | #[derive(Clone, Debug, Eq)] 25 | pub struct Schedule { 26 | source: String, 27 | fields: ScheduleFields, 28 | } 29 | 30 | impl Schedule { 31 | pub(crate) fn new(source: String, fields: ScheduleFields) -> Schedule { 32 | Schedule { source, fields } 33 | } 34 | 35 | fn next_after(&self, after: &DateTime) -> LocalResult> 36 | where 37 | Z: TimeZone, 38 | { 39 | let mut query = NextAfterQuery::from(after); 40 | for year in self 41 | .fields 42 | .years 43 | .ordinals() 44 | .range((Included(query.year_lower_bound()), Unbounded)) 45 | .cloned() 46 | { 47 | // It's a future year, the current year's range is irrelevant. 48 | if year > after.year() as u32 { 49 | query.reset_month(); 50 | query.reset_day_of_month(); 51 | } 52 | let month_start = query.month_lower_bound(); 53 | if !self.fields.months.ordinals().contains(&month_start) { 54 | query.reset_month(); 55 | } 56 | let month_range = (Included(month_start), Included(Months::inclusive_max())); 57 | for month in self.fields.months.ordinals().range(month_range).cloned() { 58 | let day_of_month_start = query.day_of_month_lower_bound(); 59 | if !self 60 | .fields 61 | .days_of_month 62 | .ordinals() 63 | .contains(&day_of_month_start) 64 | { 65 | query.reset_day_of_month(); 66 | } 67 | let day_of_month_end = days_in_month(month, year); 68 | let day_of_month_range = ( 69 | Included(day_of_month_start.min(day_of_month_end)), 70 | Included(day_of_month_end), 71 | ); 72 | 73 | 'day_loop: for day_of_month in self 74 | .fields 75 | .days_of_month 76 | .ordinals() 77 | .range(day_of_month_range) 78 | .cloned() 79 | { 80 | let hour_start = query.hour_lower_bound(); 81 | if !self.fields.hours.ordinals().contains(&hour_start) { 82 | query.reset_hour(); 83 | } 84 | let hour_range = (Included(hour_start), Included(Hours::inclusive_max())); 85 | 86 | for hour in self.fields.hours.ordinals().range(hour_range).cloned() { 87 | let minute_start = query.minute_lower_bound(); 88 | if !self.fields.minutes.ordinals().contains(&minute_start) { 89 | query.reset_minute(); 90 | } 91 | let minute_range = 92 | (Included(minute_start), Included(Minutes::inclusive_max())); 93 | 94 | for minute in self.fields.minutes.ordinals().range(minute_range).cloned() { 95 | let second_start = query.second_lower_bound(); 96 | if !self.fields.seconds.ordinals().contains(&second_start) { 97 | query.reset_second(); 98 | } 99 | let second_range = 100 | (Included(second_start), Included(Seconds::inclusive_max())); 101 | 102 | for second in 103 | self.fields.seconds.ordinals().range(second_range).cloned() 104 | { 105 | let timezone = after.timezone(); 106 | let candidate = match timezone.with_ymd_and_hms( 107 | year as i32, 108 | month, 109 | day_of_month, 110 | hour, 111 | minute, 112 | second, 113 | ) { 114 | LocalResult::None => continue, 115 | candidate => candidate, 116 | }; 117 | if !self.fields.days_of_week.ordinals().contains( 118 | &candidate 119 | .clone() 120 | .latest() 121 | .unwrap() 122 | .weekday() 123 | .number_from_sunday(), 124 | ) { 125 | continue 'day_loop; 126 | } 127 | return candidate; 128 | } 129 | query.reset_minute(); 130 | } // End of minutes range 131 | query.reset_hour(); 132 | } // End of hours range 133 | query.reset_day_of_month(); 134 | } // End of Day of Month range 135 | query.reset_month(); 136 | } // End of Month range 137 | } 138 | 139 | // We ran out of dates to try. 140 | LocalResult::None 141 | } 142 | 143 | fn prev_from(&self, before: &DateTime) -> LocalResult> 144 | where 145 | Z: TimeZone, 146 | { 147 | let mut query = PrevFromQuery::from(before); 148 | for year in self 149 | .fields 150 | .years 151 | .ordinals() 152 | .range((Unbounded, Included(query.year_upper_bound()))) 153 | .rev() 154 | .cloned() 155 | { 156 | let month_start = query.month_upper_bound(); 157 | 158 | if !self.fields.months.ordinals().contains(&month_start) { 159 | query.reset_month(); 160 | } 161 | let month_range = (Included(Months::inclusive_min()), Included(month_start)); 162 | 163 | for month in self 164 | .fields 165 | .months 166 | .ordinals() 167 | .range(month_range) 168 | .rev() 169 | .cloned() 170 | { 171 | let day_of_month_end = query.day_of_month_upper_bound(); 172 | if !self 173 | .fields 174 | .days_of_month 175 | .ordinals() 176 | .contains(&day_of_month_end) 177 | { 178 | query.reset_day_of_month(); 179 | } 180 | 181 | let day_of_month_end = days_in_month(month, year).min(day_of_month_end); 182 | 183 | let day_of_month_range = ( 184 | Included(DaysOfMonth::inclusive_min()), 185 | Included(day_of_month_end), 186 | ); 187 | 188 | 'day_loop: for day_of_month in self 189 | .fields 190 | .days_of_month 191 | .ordinals() 192 | .range(day_of_month_range) 193 | .rev() 194 | .cloned() 195 | { 196 | let hour_start = query.hour_upper_bound(); 197 | if !self.fields.hours.ordinals().contains(&hour_start) { 198 | query.reset_hour(); 199 | } 200 | let hour_range = (Included(Hours::inclusive_min()), Included(hour_start)); 201 | 202 | for hour in self 203 | .fields 204 | .hours 205 | .ordinals() 206 | .range(hour_range) 207 | .rev() 208 | .cloned() 209 | { 210 | let minute_start = query.minute_upper_bound(); 211 | if !self.fields.minutes.ordinals().contains(&minute_start) { 212 | query.reset_minute(); 213 | } 214 | let minute_range = 215 | (Included(Minutes::inclusive_min()), Included(minute_start)); 216 | 217 | for minute in self 218 | .fields 219 | .minutes 220 | .ordinals() 221 | .range(minute_range) 222 | .rev() 223 | .cloned() 224 | { 225 | let second_start = query.second_upper_bound(); 226 | if !self.fields.seconds.ordinals().contains(&second_start) { 227 | query.reset_second(); 228 | } 229 | let second_range = 230 | (Included(Seconds::inclusive_min()), Included(second_start)); 231 | 232 | for second in self 233 | .fields 234 | .seconds 235 | .ordinals() 236 | .range(second_range) 237 | .rev() 238 | .cloned() 239 | { 240 | let timezone = before.timezone(); 241 | let candidate = match timezone.with_ymd_and_hms( 242 | year as i32, 243 | month, 244 | day_of_month, 245 | hour, 246 | minute, 247 | second, 248 | ) { 249 | LocalResult::None => continue, 250 | some => some, 251 | }; 252 | if !self.fields.days_of_week.ordinals().contains( 253 | &candidate 254 | .clone() 255 | .latest() 256 | .unwrap() 257 | .weekday() 258 | .number_from_sunday(), 259 | ) { 260 | continue 'day_loop; 261 | } 262 | return candidate; 263 | } 264 | query.reset_minute(); 265 | } // End of minutes range 266 | query.reset_hour(); 267 | } // End of hours range 268 | query.reset_day_of_month(); 269 | } // End of Day of Month range 270 | query.reset_month(); 271 | } // End of Month range 272 | } 273 | 274 | // We ran out of dates to try. 275 | LocalResult::None 276 | } 277 | 278 | /// Provides an iterator which will return each DateTime that matches the schedule starting with 279 | /// the current time if applicable. 280 | pub fn upcoming(&self, timezone: Z) -> ScheduleIterator<'_, Z> 281 | where 282 | Z: TimeZone, 283 | { 284 | self.after(&timezone.from_utc_datetime(&Utc::now().naive_utc())) 285 | } 286 | 287 | /// The same, but with an iterator with a static ownership 288 | pub fn upcoming_owned(&self, timezone: Z) -> OwnedScheduleIterator { 289 | self.after_owned(timezone.from_utc_datetime(&Utc::now().naive_utc())) 290 | } 291 | 292 | /// Like the `upcoming` method, but allows you to specify a start time other than the present. 293 | pub fn after(&self, after: &DateTime) -> ScheduleIterator<'_, Z> 294 | where 295 | Z: TimeZone, 296 | { 297 | ScheduleIterator::new(self, after) 298 | } 299 | 300 | /// The same, but with a static ownership. 301 | pub fn after_owned(&self, after: DateTime) -> OwnedScheduleIterator { 302 | OwnedScheduleIterator::new(self.clone(), after) 303 | } 304 | 305 | pub fn includes(&self, date_time: DateTime) -> bool 306 | where 307 | Z: TimeZone, 308 | { 309 | self.fields.years.includes(date_time.year() as Ordinal) 310 | && self.fields.months.includes(date_time.month() as Ordinal) 311 | && self 312 | .fields 313 | .days_of_week 314 | .includes(date_time.weekday().number_from_sunday()) 315 | && self 316 | .fields 317 | .days_of_month 318 | .includes(date_time.day() as Ordinal) 319 | && self.fields.hours.includes(date_time.hour() as Ordinal) 320 | && self.fields.minutes.includes(date_time.minute() as Ordinal) 321 | && self.fields.seconds.includes(date_time.second() as Ordinal) 322 | } 323 | 324 | /// Returns a [TimeUnitSpec] describing the years included in this [Schedule]. 325 | pub fn years(&self) -> &impl TimeUnitSpec { 326 | &self.fields.years 327 | } 328 | 329 | /// Returns a [TimeUnitSpec] describing the months of the year included in this [Schedule]. 330 | pub fn months(&self) -> &impl TimeUnitSpec { 331 | &self.fields.months 332 | } 333 | 334 | /// Returns a [TimeUnitSpec] describing the days of the month included in this [Schedule]. 335 | pub fn days_of_month(&self) -> &impl TimeUnitSpec { 336 | &self.fields.days_of_month 337 | } 338 | 339 | /// Returns a [TimeUnitSpec] describing the days of the week included in this [Schedule]. 340 | pub fn days_of_week(&self) -> &impl TimeUnitSpec { 341 | &self.fields.days_of_week 342 | } 343 | 344 | /// Returns a [TimeUnitSpec] describing the hours of the day included in this [Schedule]. 345 | pub fn hours(&self) -> &impl TimeUnitSpec { 346 | &self.fields.hours 347 | } 348 | 349 | /// Returns a [TimeUnitSpec] describing the minutes of the hour included in this [Schedule]. 350 | pub fn minutes(&self) -> &impl TimeUnitSpec { 351 | &self.fields.minutes 352 | } 353 | 354 | /// Returns a [TimeUnitSpec] describing the seconds of the minute included in this [Schedule]. 355 | pub fn seconds(&self) -> &impl TimeUnitSpec { 356 | &self.fields.seconds 357 | } 358 | 359 | pub fn timeunitspec_eq(&self, other: &Schedule) -> bool { 360 | self.fields == other.fields 361 | } 362 | 363 | /// Returns a reference to the source cron expression. 364 | pub fn source(&self) -> &str { 365 | &self.source 366 | } 367 | } 368 | 369 | impl Display for Schedule { 370 | fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { 371 | write!(f, "{}", self.source) 372 | } 373 | } 374 | 375 | impl PartialEq for Schedule { 376 | fn eq(&self, other: &Schedule) -> bool { 377 | self.source == other.source 378 | } 379 | } 380 | 381 | #[derive(Clone, Debug, PartialEq, Eq)] 382 | pub struct ScheduleFields { 383 | years: Years, 384 | days_of_week: DaysOfWeek, 385 | months: Months, 386 | days_of_month: DaysOfMonth, 387 | hours: Hours, 388 | minutes: Minutes, 389 | seconds: Seconds, 390 | } 391 | 392 | impl ScheduleFields { 393 | pub(crate) fn new( 394 | seconds: Seconds, 395 | minutes: Minutes, 396 | hours: Hours, 397 | days_of_month: DaysOfMonth, 398 | months: Months, 399 | days_of_week: DaysOfWeek, 400 | years: Years, 401 | ) -> ScheduleFields { 402 | ScheduleFields { 403 | years, 404 | days_of_week, 405 | months, 406 | days_of_month, 407 | hours, 408 | minutes, 409 | seconds, 410 | } 411 | } 412 | } 413 | 414 | pub struct ScheduleIterator<'a, Z> 415 | where 416 | Z: TimeZone, 417 | { 418 | schedule: &'a Schedule, 419 | previous_datetime: Option>, 420 | later_datetime: Option>, 421 | earlier_datetime: Option>, 422 | } 423 | //TODO: Cutoff datetime? 424 | 425 | impl<'a, Z> ScheduleIterator<'a, Z> 426 | where 427 | Z: TimeZone, 428 | { 429 | fn new(schedule: &'a Schedule, starting_datetime: &DateTime) -> Self { 430 | ScheduleIterator { 431 | schedule, 432 | previous_datetime: Some(starting_datetime.clone()), 433 | later_datetime: None, 434 | earlier_datetime: None, 435 | } 436 | } 437 | } 438 | 439 | impl Iterator for ScheduleIterator<'_, Z> 440 | where 441 | Z: TimeZone, 442 | { 443 | type Item = DateTime; 444 | 445 | fn next(&mut self) -> Option> { 446 | let previous = self.previous_datetime.take()?; 447 | 448 | if let Some(later) = self.later_datetime.take() { 449 | self.previous_datetime = Some(later.clone()); 450 | Some(later) 451 | } else { 452 | match self.schedule.next_after(&previous) { 453 | LocalResult::Single(next) => { 454 | self.previous_datetime = Some(next.clone()); 455 | Some(next) 456 | } 457 | LocalResult::Ambiguous(earlier, later) => { 458 | self.previous_datetime = Some(earlier.clone()); 459 | self.later_datetime = Some(later); 460 | Some(earlier) 461 | } 462 | LocalResult::None => None, 463 | } 464 | } 465 | } 466 | } 467 | 468 | impl DoubleEndedIterator for ScheduleIterator<'_, Z> 469 | where 470 | Z: TimeZone, 471 | { 472 | fn next_back(&mut self) -> Option { 473 | let previous = self.previous_datetime.take()?; 474 | 475 | if let Some(earlier) = self.earlier_datetime.take() { 476 | self.previous_datetime = Some(earlier.clone()); 477 | Some(earlier) 478 | } else { 479 | match self.schedule.prev_from(&previous) { 480 | LocalResult::Single(prev) => { 481 | self.previous_datetime = Some(prev.clone()); 482 | Some(prev) 483 | } 484 | LocalResult::Ambiguous(earlier, later) => { 485 | self.previous_datetime = Some(later.clone()); 486 | self.earlier_datetime = Some(earlier); 487 | Some(later) 488 | } 489 | LocalResult::None => None, 490 | } 491 | } 492 | } 493 | } 494 | 495 | /// A `ScheduleIterator` with a static lifetime. 496 | pub struct OwnedScheduleIterator 497 | where 498 | Z: TimeZone, 499 | { 500 | schedule: Schedule, 501 | previous_datetime: Option>, 502 | // In the case of the Daylight Savings Time transition where an hour is 503 | // gained, store the time that occurs twice. Depending on which direction 504 | // the iteration goes, this needs to be stored separately to keep the 505 | // direction of time (becoming earlier or later) consistent. 506 | later_datetime: Option>, 507 | earlier_datetime: Option>, 508 | } 509 | 510 | impl OwnedScheduleIterator 511 | where 512 | Z: TimeZone, 513 | { 514 | pub fn new(schedule: Schedule, starting_datetime: DateTime) -> Self { 515 | Self { 516 | schedule, 517 | previous_datetime: Some(starting_datetime), 518 | later_datetime: None, 519 | earlier_datetime: None, 520 | } 521 | } 522 | } 523 | 524 | impl Iterator for OwnedScheduleIterator 525 | where 526 | Z: TimeZone, 527 | { 528 | type Item = DateTime; 529 | 530 | fn next(&mut self) -> Option> { 531 | let previous = self.previous_datetime.take()?; 532 | 533 | if let Some(later) = self.later_datetime.take() { 534 | self.previous_datetime = Some(later.clone()); 535 | Some(later) 536 | } else { 537 | match self.schedule.next_after(&previous) { 538 | LocalResult::Single(next) => { 539 | self.previous_datetime = Some(next.clone()); 540 | Some(next) 541 | } 542 | // Handle an "Ambiguous" time, such as during the end of 543 | // Daylight Savings Time, transitioning from BST to GMT, where 544 | // for example, in London, 2AM occurs twice when the hour is 545 | // moved back during the fall. 546 | LocalResult::Ambiguous(earlier, later) => { 547 | self.previous_datetime = Some(earlier.clone()); 548 | self.later_datetime = Some(later); 549 | Some(earlier) 550 | } 551 | LocalResult::None => None, 552 | } 553 | } 554 | } 555 | } 556 | 557 | impl DoubleEndedIterator for OwnedScheduleIterator { 558 | fn next_back(&mut self) -> Option { 559 | let previous = self.previous_datetime.take()?; 560 | 561 | if let Some(earlier) = self.earlier_datetime.take() { 562 | self.previous_datetime = Some(earlier.clone()); 563 | Some(earlier) 564 | } else { 565 | match self.schedule.prev_from(&previous) { 566 | LocalResult::Single(prev) => { 567 | self.previous_datetime = Some(prev.clone()); 568 | Some(prev) 569 | } 570 | // Handle an "Ambiguous" time, such as during the end of 571 | // Daylight Savings Time, transitioning from BST to GMT, where 572 | // for example, in London, 2AM occurs twice when the hour is 573 | // moved back during the fall. 574 | LocalResult::Ambiguous(earlier, later) => { 575 | self.previous_datetime = Some(later.clone()); 576 | self.earlier_datetime = Some(earlier); 577 | Some(later) 578 | } 579 | LocalResult::None => None, 580 | } 581 | } 582 | } 583 | } 584 | 585 | fn is_leap_year(year: Ordinal) -> bool { 586 | let by_four = year % 4 == 0; 587 | let by_hundred = year % 100 == 0; 588 | let by_four_hundred = year % 400 == 0; 589 | by_four && ((!by_hundred) || by_four_hundred) 590 | } 591 | 592 | fn days_in_month(month: Ordinal, year: Ordinal) -> u32 { 593 | let is_leap_year = is_leap_year(year); 594 | match month { 595 | 9 | 4 | 6 | 11 => 30, 596 | 2 if is_leap_year => 29, 597 | 2 => 28, 598 | _ => 31, 599 | } 600 | } 601 | 602 | #[cfg(feature = "serde")] 603 | struct ScheduleVisitor; 604 | 605 | #[cfg(feature = "serde")] 606 | impl Visitor<'_> for ScheduleVisitor { 607 | type Value = Schedule; 608 | 609 | fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { 610 | formatter.write_str("a valid cron expression") 611 | } 612 | 613 | // Supporting `Deserializer`s shall provide an owned `String`. 614 | // 615 | // The `Schedule` will decode from a `&str` to it, 616 | // then store the owned `String` as `Schedule::source`. 617 | fn visit_string(self, v: String) -> Result 618 | where 619 | E: de::Error, 620 | { 621 | Schedule::try_from(v).map_err(de::Error::custom) 622 | } 623 | 624 | // `Deserializer`s not providing an owned `String` 625 | // shall provide a `&str`. 626 | // 627 | // The `Schedule` will decode from the `&str`, 628 | // then clone into the heap to store as an owned `String` 629 | // as `Schedule::source`. 630 | fn visit_str(self, v: &str) -> Result 631 | where 632 | E: de::Error, 633 | { 634 | Schedule::try_from(v).map_err(de::Error::custom) 635 | } 636 | } 637 | 638 | #[cfg(feature = "serde")] 639 | impl Serialize for Schedule { 640 | fn serialize(&self, serializer: S) -> Result 641 | where 642 | S: Serializer, 643 | { 644 | serializer.serialize_str(self.source()) 645 | } 646 | } 647 | 648 | #[cfg(feature = "serde")] 649 | impl<'de> Deserialize<'de> for Schedule { 650 | fn deserialize(deserializer: D) -> Result 651 | where 652 | D: serde::Deserializer<'de>, 653 | { 654 | // Hint that the `Deserialize` type `Schedule` 655 | // would benefit from taking ownership of 656 | // buffered data owned by the `Deserializer`: 657 | // 658 | // The deserialization "happy path" decodes from a `&str`, 659 | // then stores the source as owned `String`. 660 | // 661 | // Thus, the optimized happy path receives an owned `String` 662 | // if the `Deserializer` in use supports providing one. 663 | deserializer.deserialize_string(ScheduleVisitor) 664 | } 665 | } 666 | 667 | #[cfg(test)] 668 | mod test { 669 | use chrono::Duration; 670 | #[cfg(feature = "serde")] 671 | use serde_test::{assert_tokens, Token}; 672 | 673 | use super::*; 674 | use std::str::FromStr; 675 | 676 | #[cfg(feature = "serde")] 677 | #[test] 678 | fn test_ser_de_schedule_tokens() { 679 | let schedule = Schedule::from_str("* * * * * * *").expect("valid format"); 680 | assert_tokens(&schedule, &[Token::String("* * * * * * *")]) 681 | } 682 | 683 | #[cfg(feature = "serde")] 684 | #[test] 685 | fn test_invalid_ser_de_schedule_tokens() { 686 | use serde_test::assert_de_tokens_error; 687 | 688 | assert_de_tokens_error::( 689 | &[Token::String( 690 | "definitively an invalid value for a cron schedule!", 691 | )], 692 | "definitively an invalid value for a cron schedule!\n\ 693 | ^\n\ 694 | The 'Seconds' field does not support using names. 'definitively' specified.", 695 | ); 696 | } 697 | 698 | #[cfg(feature = "serde")] 699 | #[test] 700 | fn test_ser_de_schedule_shorthand() { 701 | let serialized = postcard::to_stdvec(&Schedule::try_from("@hourly").expect("valid format")) 702 | .expect("serializable schedule"); 703 | 704 | let schedule: Schedule = 705 | postcard::from_bytes(&serialized).expect("deserializable schedule"); 706 | 707 | let starting_date = Utc.with_ymd_and_hms(2017, 2, 25, 22, 29, 36).unwrap(); 708 | assert!([ 709 | Utc.with_ymd_and_hms(2017, 2, 25, 23, 0, 0).unwrap(), 710 | Utc.with_ymd_and_hms(2017, 2, 26, 0, 0, 0).unwrap(), 711 | Utc.with_ymd_and_hms(2017, 2, 26, 1, 0, 0).unwrap(), 712 | ] 713 | .into_iter() 714 | .eq(schedule.after(&starting_date).take(3))); 715 | } 716 | 717 | #[cfg(feature = "serde")] 718 | #[test] 719 | fn test_ser_de_schedule_period_values_range() { 720 | let serialized = 721 | postcard::to_stdvec(&Schedule::try_from("0 0 0 1-31/10 * ?").expect("valid format")) 722 | .expect("serializable schedule"); 723 | 724 | let schedule: Schedule = 725 | postcard::from_bytes(&serialized).expect("deserializable schedule"); 726 | 727 | let starting_date = Utc.with_ymd_and_hms(2020, 1, 1, 0, 0, 0).unwrap(); 728 | assert!([ 729 | Utc.with_ymd_and_hms(2020, 1, 11, 0, 0, 0).unwrap(), 730 | Utc.with_ymd_and_hms(2020, 1, 21, 0, 0, 0).unwrap(), 731 | Utc.with_ymd_and_hms(2020, 1, 31, 0, 0, 0).unwrap(), 732 | Utc.with_ymd_and_hms(2020, 2, 1, 0, 0, 0).unwrap(), 733 | Utc.with_ymd_and_hms(2020, 2, 11, 0, 0, 0).unwrap(), 734 | Utc.with_ymd_and_hms(2020, 2, 21, 0, 0, 0).unwrap(), 735 | Utc.with_ymd_and_hms(2020, 3, 1, 0, 0, 0).unwrap(), 736 | ] 737 | .into_iter() 738 | .eq(schedule.after(&starting_date).take(7))); 739 | } 740 | 741 | #[test] 742 | fn test_next_and_prev_from() { 743 | let expression = "0 5,13,40-42 17 1 Jan *"; 744 | let schedule = Schedule::from_str(expression).unwrap(); 745 | 746 | let next = schedule.next_after(&Utc::now()); 747 | println!("NEXT AFTER for {} {:?}", expression, next); 748 | assert!(next.single().is_some()); 749 | 750 | let next2 = schedule.next_after(&next.unwrap()); 751 | println!("NEXT2 AFTER for {} {:?}", expression, next2); 752 | assert!(next2.single().is_some()); 753 | 754 | let prev = schedule.prev_from(&next2.unwrap()); 755 | println!("PREV FROM for {} {:?}", expression, prev); 756 | assert!(prev.single().is_some()); 757 | assert_eq!(prev, next); 758 | 759 | let prev2 = schedule.prev_from(&(next2.unwrap() + Duration::nanoseconds(100))); 760 | println!("PREV2 FROM for {} {:?}", expression, prev2); 761 | assert!(prev2.single().is_some()); 762 | assert_eq!(prev2, next2); 763 | } 764 | 765 | #[test] 766 | fn test_next_after_past_date_next_year() { 767 | // Schedule after 2021-10-27 768 | let starting_point = Utc.with_ymd_and_hms(2021, 10, 27, 0, 0, 0).unwrap(); 769 | 770 | // Triggers on 2022-06-01. Note that the month and day are smaller than 771 | // the month and day in `starting_point`. 772 | let expression = "0 5 17 1 6 ? 2022".to_string(); 773 | let schedule = Schedule::from_str(&expression).unwrap(); 774 | let next = schedule.next_after(&starting_point); 775 | println!("NEXT AFTER for {} {:?}", expression, next); 776 | assert!(next.single().is_some()); 777 | } 778 | 779 | #[test] 780 | fn test_prev_from() { 781 | let expression = "0 5,13,40-42 17 1 Jan *"; 782 | let schedule = Schedule::from_str(expression).unwrap(); 783 | let prev = schedule.prev_from(&Utc::now()); 784 | println!("PREV FROM for {} {:?}", expression, prev); 785 | assert!(prev.single().is_some()); 786 | } 787 | 788 | #[test] 789 | fn test_next_after() { 790 | let expression = "0 5,13,40-42 17 1 Jan *"; 791 | let schedule = Schedule::from_str(expression).unwrap(); 792 | let next = schedule.next_after(&Utc::now()); 793 | println!("NEXT AFTER for {} {:?}", expression, next); 794 | assert!(next.single().is_some()); 795 | } 796 | 797 | #[test] 798 | fn test_upcoming_utc() { 799 | let expression = "0 0,30 0,6,12,18 1,15 Jan-March Thurs"; 800 | let schedule = Schedule::from_str(expression).unwrap(); 801 | let mut upcoming = schedule.upcoming(Utc); 802 | let next1 = upcoming.next(); 803 | assert!(next1.is_some()); 804 | let next2 = upcoming.next(); 805 | assert!(next2.is_some()); 806 | let next3 = upcoming.next(); 807 | assert!(next3.is_some()); 808 | println!("Upcoming 1 for {} {:?}", expression, next1); 809 | println!("Upcoming 2 for {} {:?}", expression, next2); 810 | println!("Upcoming 3 for {} {:?}", expression, next3); 811 | } 812 | 813 | #[test] 814 | fn test_upcoming_utc_owned() { 815 | let expression = "0 0,30 0,6,12,18 1,15 Jan-March Thurs"; 816 | let schedule = Schedule::from_str(expression).unwrap(); 817 | let mut upcoming = schedule.upcoming_owned(Utc); 818 | let next1 = upcoming.next(); 819 | assert!(next1.is_some()); 820 | let next2 = upcoming.next(); 821 | assert!(next2.is_some()); 822 | let next3 = upcoming.next(); 823 | assert!(next3.is_some()); 824 | println!("Upcoming 1 for {} {:?}", expression, next1); 825 | println!("Upcoming 2 for {} {:?}", expression, next2); 826 | println!("Upcoming 3 for {} {:?}", expression, next3); 827 | } 828 | 829 | #[test] 830 | fn test_upcoming_rev_utc() { 831 | let expression = "0 0,30 0,6,12,18 1,15 Jan-March Thurs"; 832 | let schedule = Schedule::from_str(expression).unwrap(); 833 | let mut upcoming = schedule.upcoming(Utc).rev(); 834 | let prev1 = upcoming.next(); 835 | assert!(prev1.is_some()); 836 | let prev2 = upcoming.next(); 837 | assert!(prev2.is_some()); 838 | let prev3 = upcoming.next(); 839 | assert!(prev3.is_some()); 840 | println!("Prev Upcoming 1 for {} {:?}", expression, prev1); 841 | println!("Prev Upcoming 2 for {} {:?}", expression, prev2); 842 | println!("Prev Upcoming 3 for {} {:?}", expression, prev3); 843 | } 844 | 845 | #[test] 846 | fn test_upcoming_rev_utc_owned() { 847 | let expression = "0 0,30 0,6,12,18 1,15 Jan-March Thurs"; 848 | let schedule = Schedule::from_str(expression).unwrap(); 849 | let mut upcoming = schedule.upcoming_owned(Utc).rev(); 850 | let prev1 = upcoming.next(); 851 | assert!(prev1.is_some()); 852 | let prev2 = upcoming.next(); 853 | assert!(prev2.is_some()); 854 | let prev3 = upcoming.next(); 855 | assert!(prev3.is_some()); 856 | println!("Prev Upcoming 1 for {} {:?}", expression, prev1); 857 | println!("Prev Upcoming 2 for {} {:?}", expression, prev2); 858 | println!("Prev Upcoming 3 for {} {:?}", expression, prev3); 859 | } 860 | 861 | #[test] 862 | fn test_upcoming_local() { 863 | use chrono::Local; 864 | let expression = "0 0,30 0,6,12,18 1,15 Jan-March Thurs"; 865 | let schedule = Schedule::from_str(expression).unwrap(); 866 | let mut upcoming = schedule.upcoming(Local); 867 | let next1 = upcoming.next(); 868 | assert!(next1.is_some()); 869 | let next2 = upcoming.next(); 870 | assert!(next2.is_some()); 871 | let next3 = upcoming.next(); 872 | assert!(next3.is_some()); 873 | println!("Upcoming 1 for {} {:?}", expression, next1); 874 | println!("Upcoming 2 for {} {:?}", expression, next2); 875 | println!("Upcoming 3 for {} {:?}", expression, next3); 876 | } 877 | 878 | #[test] 879 | fn test_schedule_to_string() { 880 | let expression = "* 1,2,3 * * * *"; 881 | let schedule: Schedule = Schedule::from_str(expression).unwrap(); 882 | let result = String::from(schedule); 883 | assert_eq!(expression, result); 884 | } 885 | 886 | #[test] 887 | fn test_display_schedule() { 888 | use std::fmt::Write; 889 | let expression = "@monthly"; 890 | let schedule = Schedule::from_str(expression).unwrap(); 891 | let mut result = String::new(); 892 | write!(result, "{}", schedule).unwrap(); 893 | assert_eq!(expression, result); 894 | } 895 | 896 | #[test] 897 | fn test_valid_from_str() { 898 | let schedule = Schedule::from_str("0 0,30 0,6,12,18 1,15 Jan-March Thurs"); 899 | schedule.unwrap(); 900 | } 901 | 902 | #[test] 903 | fn test_invalid_from_str() { 904 | let schedule = Schedule::from_str("cheesecake 0,30 0,6,12,18 1,15 Jan-March Thurs"); 905 | assert!(schedule.is_err()); 906 | } 907 | 908 | #[test] 909 | fn test_no_panic_on_nonexistent_time_after() { 910 | use chrono::offset::TimeZone; 911 | use chrono_tz::Tz; 912 | 913 | let schedule_tz: Tz = "Europe/London".parse().unwrap(); 914 | let dt = schedule_tz 915 | .with_ymd_and_hms(2019, 10, 27, 0, 3, 29) 916 | .unwrap() 917 | .checked_add_signed(chrono::Duration::hours(1)) // puts it in the middle of the DST transition 918 | .unwrap(); 919 | let schedule = Schedule::from_str("* * * * * Sat,Sun *").unwrap(); 920 | let next = schedule.after(&dt).next().unwrap(); 921 | assert!(next > dt); // test is ensuring line above does not panic 922 | } 923 | 924 | #[test] 925 | fn test_no_panic_on_nonexistent_time_before() { 926 | use chrono::offset::TimeZone; 927 | use chrono_tz::Tz; 928 | 929 | let schedule_tz: Tz = "Europe/London".parse().unwrap(); 930 | let dt = schedule_tz 931 | .with_ymd_and_hms(2019, 10, 27, 0, 3, 29) 932 | .unwrap() 933 | .checked_add_signed(chrono::Duration::hours(1)) // puts it in the middle of the DST transition 934 | .unwrap(); 935 | let schedule = Schedule::from_str("* * * * * Sat,Sun *").unwrap(); 936 | let prev = schedule.after(&dt).nth_back(1).unwrap(); 937 | assert!(prev < dt); // test is ensuring line above does not panic 938 | } 939 | 940 | #[test] 941 | fn test_no_panic_on_leap_day_time_after() { 942 | let dt = chrono::DateTime::parse_from_rfc3339("2024-02-29T10:00:00.000+08:00").unwrap(); 943 | let schedule = Schedule::from_str("0 0 0 * * * 2100").unwrap(); 944 | let next = schedule.after(&dt).next().unwrap(); 945 | assert!(next > dt); // test is ensuring line above does not panic 946 | } 947 | 948 | #[test] 949 | fn test_time_unit_spec_equality() { 950 | let schedule_1 = Schedule::from_str("@weekly").unwrap(); 951 | let schedule_2 = Schedule::from_str("0 0 0 * * 1 *").unwrap(); 952 | let schedule_3 = Schedule::from_str("0 0 0 * * 1-7 *").unwrap(); 953 | let schedule_4 = Schedule::from_str("0 0 0 * * * *").unwrap(); 954 | assert_ne!(schedule_1, schedule_2); 955 | assert!(schedule_1.timeunitspec_eq(&schedule_2)); 956 | assert!(schedule_3.timeunitspec_eq(&schedule_4)); 957 | } 958 | 959 | #[test] 960 | fn test_dst_ambiguous_time_after() { 961 | use chrono_tz::Tz; 962 | 963 | let schedule_tz: Tz = "America/Chicago".parse().unwrap(); 964 | let dt = schedule_tz 965 | .with_ymd_and_hms(2022, 11, 5, 23, 30, 0) 966 | .unwrap(); 967 | let schedule = Schedule::from_str("0 0 * * * * *").unwrap(); 968 | let times = schedule 969 | .after(&dt) 970 | .map(|x| x.to_string()) 971 | .take(5) 972 | .collect::>(); 973 | let expected_times = [ 974 | "2022-11-06 00:00:00 CDT".to_string(), 975 | "2022-11-06 01:00:00 CDT".to_string(), 976 | "2022-11-06 01:00:00 CST".to_string(), // 1 AM happens again 977 | "2022-11-06 02:00:00 CST".to_string(), 978 | "2022-11-06 03:00:00 CST".to_string(), 979 | ]; 980 | 981 | assert_eq!(times.as_slice(), expected_times.as_slice()); 982 | } 983 | 984 | #[test] 985 | fn test_dst_ambiguous_time_before() { 986 | use chrono_tz::Tz; 987 | 988 | let schedule_tz: Tz = "America/Chicago".parse().unwrap(); 989 | let dt = schedule_tz.with_ymd_and_hms(2022, 11, 6, 3, 30, 0).unwrap(); 990 | let schedule = Schedule::from_str("0 0 * * * * *").unwrap(); 991 | let times = schedule 992 | .after(&dt) 993 | .map(|x| x.to_string()) 994 | .rev() 995 | .take(5) 996 | .collect::>(); 997 | let expected_times = [ 998 | "2022-11-06 03:00:00 CST".to_string(), 999 | "2022-11-06 02:00:00 CST".to_string(), 1000 | "2022-11-06 01:00:00 CST".to_string(), 1001 | "2022-11-06 01:00:00 CDT".to_string(), // 1 AM happens again 1002 | "2022-11-06 00:00:00 CDT".to_string(), 1003 | ]; 1004 | 1005 | assert_eq!(times.as_slice(), expected_times.as_slice()); 1006 | } 1007 | } 1008 | --------------------------------------------------------------------------------