croner 4.0.0

Fully-featured, lightweight, and efficient Rust library designed for parsing and evaluating cron patterns
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
# Croner

Croner is a fully-featured, lightweight, and efficient Rust library designed for parsing and evaluating cron patterns.

This is the Rust flavor of the popular JavaScript/TypeScript cron parser
[croner](https://github.com/hexagon/croner).

## Features

- Parse and evaluate [cron]https://en.wikipedia.org/wiki/Cron#CRON_expression
  expressions to calculate upcoming execution times.
- Generates human-readable descriptions of cron patterns.
- Follows POSIX/Vixie-cron standards, while extending it with additional specifiers such as `L`
  for the last day and weekday of the month, `#` for the nth weekday of the
  month, `W` for closest weekday to a day of month.
- Evaluate cron expressions across different time zones.
- Supports optional second-, and year granularity
- Supports optional alternative weekday mode to use Quartz-style weekdays instead of POSIX using `with_alternative_weekdays`
- Allows for flexible combination of DOM and DOW conditions, enabling patterns to match specific days of the week in specific weeks of the month or the closest weekday to a specific day.
- Compatible with `chrono`, (optionally) `chrono-tz`, and `jiff`.
- Robust error handling.

## Crate Features

- `jiff`: Enables support for [`jiff::Zoned`]https://docs.rs/jiff/latest/jiff/struct.Zoned.html and [`jiff::civil::DateTime`]https://docs.rs/jiff/latest/jiff/civil/struct.DateTime.html. Disabled by default.
- `chrono`: Enables support for [`chrono::DateTime`]https://docs.rs/chrono/latest/chrono/struct.DateTime.html and [`chrono::NaiveDateTime`]https://docs.rs/chrono/latest/chrono/struct.NaiveDateTime.html. Enabled by default when default features are left on.
- `serde`: Enables [`serde::Serialize`]https://docs.rs/serde/1/serde/trait.Serialize.html and [`serde::Deserialize`]https://docs.rs/serde/1/serde/trait.Deserialize.html implementations for [`Cron`]https://docs.rs/croner/latest/croner/struct.Cron.html. This feature is disabled by default.

## Why croner instead of cron or saffron?

Croner combines the features of cron and saffron, while following the POSIX/Vixie "standards" for the relevant parts. See this table:

| Feature              | Croner      | Cron      | Saffron |
|----------------------|-------------|-----------|---------|
| Time Zones | X         |    X    |     |
| Ranges (15-25)| X         |    X    |   X   |
| Ranges with stepping (15-25/2)| X         |    X    |   X   |
| `L` - Last day of month | X         |         |   X   |
| `5#L` - Last occurrence of weekday |    X     |   X    |       |
| `5L` - Last occurrence of weekday |    X     |    ?   |   X    |
| `#` - Nth occurrence of weekday |    X     |      |   X    |
| `W` - Closest weekday |    X     |        |  X     |
| `+` - dom-AND-dow through pattern |    X     |        |      |
| "Standards"-compliant weekdays (1 is monday) |   X    |      |       |
| Five part patterns (minute granularity) |  X   |         |    X   |
| Six part patterns (second granularity)|  X   |    X    |       |
| Weekday/Month text representations |  X   |    X    |   X   |
| Aliases (`@hourly` etc.) |  X           |     X      |          |
| chrono `DateTime` compatibility |    X     |     X   |   X    |
| Option to force DOM-and-DOW |    X     |           |         |
| Generate human readable string |    X     |           |    X    |

> [!NOTE]
> Tests carried out at 2023-12-02 using `cron@0.12.0` and `saffron@.0.1.0`

## Getting Started

### Prerequisites

Ensure you have Rust installed on your machine. If not, you can get it from
[the official Rust website](https://www.rust-lang.org/).

### Installation

Add `croner` to your `Cargo.toml` dependencies:

```toml
[dependencies]
croner = "4.0"
```

If you want to use `jiff` instead of the default `chrono` backend:

```toml
[dependencies]
croner = { version = "4.0", default-features = false, features = ["jiff"] }
jiff = "0.2"
```

### Migration guide (3.x -> 4.0)

See [MIGRATION.md](MIGRATION.md) for a detailed guide covering all breaking changes, including chrono becoming optional, step syntax validation, generic `CronDateTime` method signatures, the new `jiff` backend, and more.

### Usage

Here's a quick example to get you started with matching current time, and
finding the next occurrence using `chrono::Local` (the default backend):

```rust
use chrono::Local;
use croner::Cron;
use std::str::FromStr as _;

fn main() {

    // Parse cron expression
    let cron_all = Cron::from_str("18 * * * 5")
      .expect("Couldn't parse cron string");

    // Compare cron pattern with current local time
    let time = Local::now();
    let matches_all = cron_all.is_time_matching(&time).unwrap();

    // Get next match
    let next = cron_all.find_next_occurrence(&time, false).unwrap();

    // Output results
    println!("Description: {}", cron_all.describe());
    println!("Time is: {}", time);
    println!("Pattern \"{}\" does {} time {}", cron_all.pattern, if matches_all { "match" } else { "not match" }, time );
    println!("Pattern \"{}\" will match next time at {}", cron_all.pattern, next);

}
```

If you prefer the `jiff` backend:

```rust
use croner::Cron;
use jiff::Zoned;
use std::str::FromStr as _;

fn main() {

    // Parse cron expression
    let cron_all = Cron::from_str("18 * * * 5")
      .expect("Couldn't parse cron string");

    // Compare cron pattern with current zoned time
    let time = Zoned::now();
    let matches_all = cron_all.is_time_matching(&time).unwrap();

    // Get next match
    let next = cron_all.find_next_occurrence(&time, false).unwrap();

    // Output results
    println!("Description: {}", cron_all.describe());
    println!("Time is: {}", time);
    println!("Pattern \"{}\" does {} time {}", cron_all.pattern, if matches_all { "match" } else { "not match" }, time );
    println!("Pattern \"{}\" will match next time at {}", cron_all.pattern, next);

}
```

To match against a non-local timezone with the `chrono` backend, croner supports
zoned chrono `DateTime<Tz>`. To use a named time zone, you can utilize the
`chrono-tz` crate.

```rust
use croner::Cron;
use chrono::Local;
use chrono_tz::Tz;
use std::str::FromStr as _;

fn main() {
    // Parse cron expression
    let cron = Cron::from_str("18 * * * 5")
      .expect("Couldn't parse cron string");

    // Choose a different time zone, for example America/New_York
    let est_timezone: Tz = "America/New_York".parse().expect("Invalid timezone");

    // Find the next occurrence in EST
    let time_est = Local::now().with_timezone(&est_timezone);
    let next_est = cron.find_next_occurrence(&time_est, false).unwrap();

    // Output results for EST
    println!("EST time is: {}", time_est);
    println!(
        "Pattern \"{}\" will match next time at (EST): {}",
        cron.pattern.to_string(),
        next_est
    );
}
```

This example demonstrates how to calculate the next 5 occurrences of New Year's Eve that fall on a Friday. We'll use a cron expression to match every Friday (`FRI`) in December (`12`) and configure `dom_and_dow` to ensure both day-of-month and day-of-week conditions are met (see [configuration](#configuration) for more details).

```rust
use chrono::Local;
use croner::parser::CronParser;

fn main() {
    // Parse cron expression for Fridays in December
    let cron = CronParser::builder()
        // Include seconds in pattern
        .seconds(croner::parser::Seconds::Optional)
        // Ensure both day of month and day of week conditions are met
        .dom_and_dow(true)
        .build()
        .parse("0 0 0 31 12 FRI")
        .expect("Couldn't parse cron string");

    let time = Local::now();

    println!("Finding the next 5 New Year's Eves on a Friday:");
    for time in cron.iter_from(time).take(5) {
        println!("{time}");
    }
}
```

### Pattern

The expressions used by Croner are very similar to those of Vixie Cron, but with
a few additions and changes as outlined below:

```javascript
// ┌──────────────── (optional) second (0 - 59)
// │ ┌────────────── minute (0 - 59)
// │ │ ┌──────────── hour (0 - 23)
// │ │ │ ┌────────── day of month (1 - 31)
// │ │ │ │ ┌──────── month (1 - 12, JAN-DEC)
// │ │ │ │ │ ┌────── day of week (0 - 6, SUN-Mon)
// │ │ │ │ │ │       (0 to 6 are Sunday to Saturday; 7 is Sunday, the same as 0)
// │ │ │ │ │ │
// * * * * * *
```

- Croner expressions have the following additional modifiers:
  - _?_: In the Rust version of croner, a questionmark in the day-of-month or
    day-of-week field behaves just as `*`. This allow for legacy cron patterns
    to be used.
  - _L_: The letter 'L' can be used in the day of the month field to indicate
    the last day of the month. When used in the day of the week field in
    conjunction with the # character, it denotes the last specific weekday of
    the month. For example, `5#L` represents the last Friday of the month.
  - _#_: The # character specifies the "nth" occurrence of a particular day
    within a month. For example, supplying `5#2` in the day of week field
    signifies the second Friday of the month. This can be combined with ranges
    and supports day names. For instance, MON-FRI#2 would match the Monday
    through Friday of the second week of the month.
  - _W_: The character 'W' is used to specify the closest weekday to a given day
    in the day of the month field. For example, 15W will match the closest
    weekday to the 15th of the month. If the specified day falls on a weekend
    (Saturday or Sunday), the pattern will match the closest weekday before or
    after that date. For instance, if the 15th is a Saturday, 15W will match the
    14th (Friday), and if the 15th is a Sunday, it will match the 16th (Monday).
  - _+_: The plus sign can be used as a prefix to the day-of-week field to create 
    a logical AND between the day-of-month and day-of-week fields. By default, 
    the relationship is a logical OR. For example, `0 0 1 * +MON` will run only
    if the 1st of the month is also a Monday.

| Field        | Required | Allowed values  | Allowed special characters | Remarks                                                                                                         |
| ------------ | -------- | --------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------- |
| Seconds      | Optional | 0-59            | * , - /                    |                                                                                                                 |
| Minutes      | Yes      | 0-59            | * , - /                    |                                                                                                                 |
| Hours        | Yes      | 0-23            | * , - /                    |                                                                                                                 |
| Day of Month | Yes      | 1-31            | * , - / ? L W              |                                                                                                                 |
| Month        | Yes      | 1-12 or JAN-DEC | * , - /                    |                                                                                                                 |
| Day of Week  | Yes      | 0-7 or SUN-MON  | * , - / ? # L +            | 0 to 6 are Sunday to Saturday<br>7 is Sunday, the same as 0<br># is used to specify nth occurrence of a weekday |

> [!NOTE]
> Weekday and month names are case-insensitive. Both `MON` and `mon`
> work. When using `L` in the Day of Week field, it affects all specified
> weekdays. For example, `5-6#L` means the last Friday and Saturday in the
> month." The # character can be used to specify the "nth" weekday of the month.
> For example, 5#2 represents the second Friday of the month.

> [!NOTE]
> The `W` feature is constrained within the given month. The search for
> the closest weekday will not cross into a previous or subsequent month. For
> example, if the 1st of the month is a Saturday, 1W will trigger on Monday
> the 3rd, not the last Friday of the previous month.

It is also possible to use the following "nicknames" as pattern.

| Nickname   | Description                        |
| ---------- | ---------------------------------- |
| \@yearly   | Run once a year, ie. "0 0 1 1 *".  |
| \@annually | Run once a year, ie. "0 0 1 1 *".  |
| \@monthly  | Run once a month, ie. "0 0 1 * *". |
| \@weekly   | Run once a week, ie. "0 0 * * 0".  |
| \@daily    | Run once a day, ie.  "0 0 * * *".  |
| \@hourly   | Run once an hour, ie. "0 * * * *". |

### Configuration

Croner uses `CronParser` to parse the cron expression. Invoking
`Cron::from_str("pattern")` is equivalent to
`CronParser::new().parse("pattern")`. You can customise the parser by creating a
parser builder using `CronParser::builder`.

#### 1. Making seconds optional

This option enables the inclusion of seconds in the cron pattern, but it's not mandatory. By using this option, you can create cron patterns that either include or omit the seconds field. This offers greater flexibility, allowing for more precise scheduling without imposing the strict requirement of defining seconds in every pattern.

**Example Usage**:

```rust
use croner::parser::{CronParser, Seconds};

// Configure the parser to allow seconds.
let parser = CronParser::builder().seconds(Seconds::Optional).build();

let cron = parser
    .parse("*/10 * * * * *") // Every 10 seconds
    .expect("Invalid cron pattern");
```

#### 2. Making seconds optional required

In contrast to `Seconds::Optional`, the `Seconds::Required` variant requires the seconds field in every cron pattern. This enforces a high level of precision in task scheduling, ensuring that every pattern explicitly specifies the second at which the task should run.

**Example Usage**:

```rust
use croner::parser::{CronParser, Seconds};

// Configure the parser to require seconds.
let parser = CronParser::builder().seconds(Seconds::Required).build();

let cron = parser
    .parse("5 */2 * * * *") // At 5 seconds past every 2 minutes
    .expect("Invalid cron pattern");
```

#### 3. `dom_and_dow`

This method forces the combination of Day of Month (DOM) and Day of Week (DOW) conditions in your cron expressions. It's particularly useful for creating schedules that require specificity in terms of both the day of the month and the day of the week, such as running a task when the first of the month is a Monday, or christmas day is on a friday. Certain libraries use this mode by default.

> [!NOTE]
> While this method provides a way to globally enforce AND logic, the recommended approach is to use the `+` modifier directly in the cron pattern (e.g., `0 0 1 * +MON`). This pattern-level configuration gives you more granular control and is enabled by default.

**Example Usage**:

```rust
use croner::parser::CronParser;

// Configure the parser to enable DOM and DOW.
let parser = CronParser::builder().dom_and_dow(true).build();

let cron = parser
    .parse("0 0 25 * FRI") // When christmas day is on a friday
    .expect("Invalid cron pattern");
```

#### 4. `alternative_weekdays` (Quartz mode)

This configuration method switches the weekday mode from the POSIX standard to the Quartz-style, commonly used in Java-based scheduling systems. It's useful for those who are accustomed to Quartz's way of specifying weekdays or for ensuring compatibility with existing Quartz-based schedules.

**Example Usage**:

```rust
use croner::parser::CronParser;

// Configure the parser to use Quartz-style weekday mode.
let parser = CronParser::builder().alternative_weekdays(true).build();

let cron = parser
    .parse("0 0 12 * * 6") // Every Friday (denoted with 6 in Quartz mode) at noon
    .expect("Invalid cron pattern");
```

#### 5. `sloppy_ranges`

This option enables backward compatibility with non-standard step syntax. When enabled, patterns like `0/10` (start at 0, step by 10) and `/10` (same as `*/10`) are accepted. These patterns are not compliant with OCPS/vixie-cron standards, which only allow `*/Z` (wildcard with step) and `X-Y/Z` (range with step).

**Note**: This option is disabled by default to ensure compliance with cron standards. Only enable it if you need to support legacy cron patterns.

**Example Usage**:

```rust
use croner::parser::CronParser;

// Configure the parser to allow sloppy range syntax.
let parser = CronParser::builder().sloppy_ranges(true).build();

let cron = parser
    .parse("0/10 * * * *") // Start at 0, step by 10 (0, 10, 20, 30, 40, 50)
    .expect("Valid with sloppy_ranges enabled");

let cron2 = parser
    .parse("/10 * * * *") // Same as */10 (0, 10, 20, 30, 40, 50)
    .expect("Valid with sloppy_ranges enabled");
```

#### 6. Quartz Compatibility

If you're migrating from Quartz or need croner-rust to behave like the Quartz scheduler, you can configure the parser to match Quartz's behavior. The main differences between Quartz and standard POSIX cron are:

- **Weekday numbering**: Quartz uses 1-7 (1=Sunday, 7=Saturday) instead of 0-6 (0=Sunday, 6=Saturday)
- **Seconds field**: Quartz typically includes a seconds field (6-7 fields instead of 5)
- **Step syntax**: Quartz allows non-standard step patterns like `0/10`

**Example Usage**:

```rust
use croner::parser::{CronParser, Seconds};

// Configure the parser for Quartz compatibility.
let parser = CronParser::builder()
    .alternative_weekdays(true)  // Use Quartz-style weekday numbering (1=SUN, 7=SAT)
    .seconds(Seconds::Required)  // Require seconds field (6 or 7 fields)
    .sloppy_ranges(true)         // Allow non-standard step syntax like 0/10
    .build();

// Parse a Quartz-style cron expression: every Friday at noon
let cron = parser
    .parse("0 0 12 ? * 6")  // Quartz format: sec min hour dom month dow
    .expect("Invalid cron pattern");
```

### Documentation

For detailed usage and API documentation, visit
[Croner on docs.rs](https://docs.rs/croner/).

## Time and Calendar System

Croner uses the `chrono` crate, which operates on a **proleptic Gregorian calendar**. This means it treats all dates, historical or future, as if the Gregorian calendar has always been in effect. Consequently, it does not account for historical calendar reforms (e.g., skipped days during the 1582 Gregorian adoption) and will iterate through all dates uniformly.
For stability and practical use, Croner supports dates from **year 1 AD/CE** up to the beginning of **year 5000**, preventing searches that are too far into the past or future.

### Daylight Saving Time (DST) Handling

Croner-rust provides robust and predictable handling of Daylight Saving Time (DST) transitions, aligning with the Open Cron Pattern Specification (OCPS) and Vixie-cron's time-tested behavior. Jobs are categorized based on their time-unit field specifications:

* **Fixed-Time Jobs**: Jobs with specific numerical values for seconds, minutes, and hours (e.g., `0 30 2 * * *`).
* **Interval/Wildcard Jobs**: Jobs using wildcards (`*`) or step values (`*/N`) in their seconds, minutes, or hours fields (e.g., `*/5 * * * * *`).

During DST transitions, Croner-rust behaves as follows:

* **DST Gap (Spring Forward)**: When a scheduled time falls into a non-existent interval (e.g., 2:00 AM jumps to 3:00 AM):
    * Fixed-Time Jobs: Will execute at the first valid second/minute immediately following the gap on the same calendar day.
    * Interval/Wildcard Jobs: Occurrences within the gap are skipped. Subsequent executions resume at the next regularly scheduled interval relative to the new wall clock time.
* **DST Overlap (Fall Back)**: When a scheduled time interval occurs twice (e.g., 2:00 AM falls back to 1:00 AM):
    * Fixed-Time Jobs: Will execute only once, at its first occurrence in wall clock time.
    * Interval/Wildcard Jobs: Will execute for each occurrence that matches its pattern in wall clock time within the duplicated hour.

Searches and iterators always move in real time, not in wall clock time. Over a
duplicated hour they run through the whole first pass before the whole second
pass, so a forward search never returns a time in the past and a backward search
never returns one in the future. The start time you give decides which pass a
search is on, so pass the exact value the previous call returned.

## Development

To start developing in the Croner project:

1. Clone the repository.
2. Navigate into the project directory.
3. Build the project using `cargo build`.
4. Run tests with `cargo test --all-features`.
5. Run demo with `cargo run --example simple_demo`
6. Run jiff demo with `cargo run --example simple_demo_jiff --no-default-features --features jiff`

## Contributing

We welcome contributions! Please feel free to submit a pull request or open an
issue.

## License

This project is licensed under the MIT License - see the
[LICENSE.md](LICENSE.md) file for details.

## Disclaimer

Please note that Croner is currently in its early stages of development. As
such, the API is subject to change in future releases, adhering to semantic
versioning principles. We recommend keeping this in mind when integrating Croner
into your projects.

## Contact

If you have any questions or feedback, please open an issue in the repository
and we'll get back to you as soon as possible.