NAME
Punk::Queue::Cron - the cron expression parser and occurrence walk
SYNOPSIS
use Punk::Queue;
# validate at boot - croaks with the reason, names the spec
Punk::Queue::Cron->check('0 3 * * *');
Punk::Queue::Cron->check('30 2 * * *', 'local');
# the next occurrence strictly after an epoch, or undef
my $at = Punk::Queue::Cron->next_after('*/15 * * * *', time);
my $ny = Punk::Queue::Cron->next_after('0 3 * * *', time, '+0530');
# from the shell, no database required:
# punk-queue cron next '*/15 * * * *' --count 10
DESCRIPTION
Five fields - minute, hour, day-of-month, month, day-of-week - compiled once in C into bitmasks, then walked structurally (months skipped by month, days by day, hours by hour) to the next occurrence. The walk is pure arithmetic over epoch seconds; nothing here touches the database. The scheduler that turns occurrences into jobs lives in the supervisor and is documented in Punk::Queue.
Supported syntax
*, ranges a-b, steps */n and a-b/n, lists a,b,c, case-insensitive three-letter month and day names (jan, mon), day-of-week 0-7 with both 0 and 7 meaning Sunday, the @yearly, @annually, @monthly, @weekly, @daily, @midnight and @hourly aliases, and @every <n>(s|m|h|d) - a plain interval from the last occurrence, not a wall-clock grid.
Deliberately excluded, and rejected by name rather than mis-parsed: L, W, #, ?, and second or year fields.
The vixie day rule
When BOTH day fields are restricted, a day matching EITHER fires (0 12 8 * 0 runs on the 8th AND on Sundays). When only one is restricted, it alone gates. This is the single most common cron implementation bug; it is implemented faithfully and tested by name.
Unmatchable expressions
An expression with no occurrence within five years (0 0 30 2 *) croaks at check - and therefore at boot, at upsert_cron, and at the cron keyword - instead of becoming a cron that silently never fires.
Timezones
tz is UTC (the default), local, or a fixed +HHMM/-HHMM offset. There is no timezone database: UTC and fixed offsets use bundled civil-date arithmetic; only local touches libc, because only local has DST. The DST consequences, stated plainly: an occurrence in the spring-forward skipped hour does not fire that day; in the fall-back doubled hour a pinned time fires once, at the first (DST) instance.
METHODS
check
Punk::Queue::Cron->check($expr);
Punk::Queue::Cron->check($expr, $tz);
Parses and validates. Croaks with the reason - bad field, unsupported token by name, unknown alias, bad tz, or an expression that can never fire. Returns true. Every path that stores an expression calls this.
next_after
my $epoch = Punk::Queue::Cron->next_after($expr, $from);
my $epoch = Punk::Queue::Cron->next_after($expr, $from, $tz);
The next occurrence strictly after epoch $from, as an epoch, or undef when none exists within the five-year horizon. $from itself is never returned: feeding a result back in walks the schedule.
SEE ALSO
Punk::Queue for the scheduler that fires these occurrences, Punk::Plugin::Queue for the cron keyword, and the punk-queue crons and punk-queue cron subcommands.
AUTHOR
LNATION, <email at lnation.org>
LICENSE AND COPYRIGHT
This software is Copyright (c) 2026 by LNATION.
This is free software, licensed under:
The Artistic License 2.0 (GPL Compatible)