NAME
Cron::Toolkit - Quartz-compatible cron parser with unique extensions and over 400 tests
SYNOPSIS
use Cron::Toolkit;
use feature qw(say);
my $c = Cron::Toolkit->new(
expression => "0 30 14 ? * 6-2 *",
time_zone => "Europe/London",
);
say $c->describe;
# 2:30 PM every day from Saturday to Tuesday of every month
# next occurence in epoch seconds
say $c->next;
# previous occurence in epoch seconds
say $c->previous;
# Question: when does February 29th next land on a Monday?
say Cron::Toolkit->new(expression => "0 0 0 29 2 1 *")->next;
# Mon Feb 29 00:00:00 2044
# See exactly what was parsed
$c->dump_tree;
# ┌─ second: 0
# ├─ minute: 30
# ├─ hour: 14
# ├─ dom: ?
# ├─ month: *
# ├─ dow: 6-2
# └─ year: *
DESCRIPTION
Cron::Toolkit implements a complete, rigorously-tested cron expression parser that supports the full Quartz Scheduler syntax plus several useful extensions not found in other implementations.
Notable features include:
- Full 7-field Quartz syntax (seconds and year fields)
- Both day-of-month and day-of-week may be specified simultaneously (AND logic)
- Wrapped day-of-week ranges (e.g.
6-2= Saturday through Tuesday) - Proper Quartz-compatible DST handling
- Time-zone support via IANA names or fixed UTC offsets
- Natural-language English descriptions
- Complete crontab parsing with environment variable expansion
- Full abstract syntax tree and
dump_tree()for debugging
RELIABILITY
The distribution ships with over 400 data-driven tests covering every supported token, leap years, DST transitions, all time zones from UTC−12 to UTC+14, and every edge case discovered during development.
If it parses, the result is correct.
UNIQUE EXTENSIONS
-
DOM + DOW = AND logic
Allows queries such as "next February 29 that falls on a Monday".
-
Wrapped day-of-week ranges
6-2 matches Saturday, Sunday, Monday, Tuesday
-
Internal day-of-week: 1–7 = Monday–Sunday
Matches Time::Moment and DateTime.
as_quartz_string()converts back to Quartz's 1=Sunday convention.
FIELD REFERENCE & ALLOWED VALUES
Field Allowed values Allowed special characters
-------------------------------------------------------------------
Second 0–59 *,/,-
Minute 0–59 *,/,-,
Hour 0–23 *,/,-,
Day of month 1–31 *,/,-,?,L,LW,W
Month 1–12 or JAN–DEC *,/,-
Day of week 1–7 or MON-SUN *,/,-,?,L,#
Year (optional) 1970–2099 *,/,-
Legend:
* wildcard
, list
- range
/ step
? no specific value (DOM or DOW only)
L last (day or day-of-week)
L-n n to last day of the month
nL last n-day of the month
LW last weekday of month
nW nearest weekday to n
# nth day-of-week (e.g. 3#2 = 2nd Wednesday)
@aliases: @yearly @annually @monthly @weekly @daily @hourly (Quartz standard)
METHODS
-
Cron::Toolkit->new( expression => $expr, %options )Main constructor; auto-detects Unix vs Quartz format.
-
Cron::Toolkit->new_from_unix( expression => $expr, %options )Force traditional 5-field Unix interpretation.
-
Cron::Toolkit->new_from_quartz( expression => $expr, %options )Force Quartz interpretation.
-
Cron::Toolkit->new_from_crontab( $string )Parse a full crontab; returns a list of
Cron::Toolkitobjects. Supports$VARexpansion, user field, and comments. -
$c->as_stringNormalized 7-field representation (DOW 1–7 = Mon–Sun).
-
$c->as_quartz_stringQuartz-compatible string (DOW 1=Sunday).
-
$c->describeHuman-readable English description.
-
$c->next( [$from_epoch] )Next occurrence after
$from_epochortime. -
$c->previous( [$from_epoch] )Previous occurrence before
$from_epochortime. -
$c->is_match( $epoch )Returns true if
$epochmatches the expression. -
$c->dump_treePretty-printed abstract syntax tree (invaluable for debugging).
-
$c->to_jsonJSON representation of the object (expression, description, bounds, etc.).
-
Accessors
$c->time_zone("Europe/Berlin") $c->utc_offset(+180) # minutes $c->begin_epoch($epoch) $c->end_epoch($epoch) # undef = no limit
TIME ZONES AND DST
All calculations are performed in the configured time zone. DST transitions follow Quartz Scheduler rules exactly:
- Spring forward — times that do not exist are skipped
- Fall back — repeated local times fire twice
BUGS AND CONTRIBUTIONS
The test suite currently contains over 400 data-driven tests covering every supported token, DST transitions, leap years, all time zones, and many edge cases — but real-world cron expressions can be surprisingly creative.
If you find:
- an expression that should be valid but dies or is rejected
- a next/previous occurrence that is wrong
- a description that is misleading or unclear
- any behaviour that differs from Quartz Scheduler (when using Quartz syntax)
...please file a bug report at https://github.com/nathanielgraham/cron-toolkit-perl/issues
Pull requests with failing test cases are especially welcome — they are the fastest way to get a fix merged.
Feature requests (e.g. more natural-language locales, RRULE export, etc.) are also very much appreciated.
Thank you!
AUTHOR
Nathaniel Graham
COPYRIGHT AND LICENSE
Copyright 2025 Nathaniel Graham
This library is free software; you may redistribute it and/or modify it under the same terms as Perl itself.