Cron expression parser with next run times
Parse a five-field cron expression with lists, ranges and steps, then compute its upcoming run times in plain TypeScript.
React / Next.jsUtilitiescronschedulerparsertypescript
Code
type Spec = { min: number; max: number };
const FIELDS: Spec[] = [{ min: 0, max: 59 }, { min: 0, max: 23 }, { min: 1, max: 31 }, { min: 1, max: 12 }, { min: 0, max: 7 }];
// One field -> lookup table indexed by value.
function parseField(src: string, { min, max }: Spec): boolean[] {
const on = new Array<boolean>(max + 1).fill(false);
for (const part of src.split(',')) {
const [range = '', stepText] = part.split('/');
const step = stepText === undefined ? 1 : Number(stepText);
if (!Number.isInteger(step) || step < 1) throw new Error(`Bad step in "${part}"`);
let from = min;
let to = max;
if (range !== '*') {
const [a, b] = range.split('-');
from = Number(a);
to = b === undefined ? (stepText === undefined ? from : max) : Number(b);
}
if (!Number.isInteger(from) || !Number.isInteger(to) || from < min || to > max || from > to) {
throw new Error(`Value out of range in "${part}"`);
}
for (let v = from; v <= to; v += step) on[v] = true;
}
return on;
}
export function parseCron(expr: string) {
const parts = expr.trim().split(/\s+/);
if (parts.length !== 5) throw new Error('Expected 5 fields: minute hour day month weekday');
const [minute, hour, dom, month, dow] = parts.map((p, i) => parseField(p, FIELDS[i]!)) as boolean[][];
if (dow![7]) dow![0] = true; // 7 is Sunday too
return { minute: minute!, hour: hour!, dom: dom!, month: month!, dow: dow!, domAny: parts[2] === '*', dowAny: parts[4] === '*' };
}
export function nextRuns(expr: string, from = new Date(), count = 5): Date[] {
const c = parseCron(expr);
const out: Date[] = [];
const d = new Date(from);
d.setSeconds(0, 0);
d.setMinutes(d.getMinutes() + 1);
const limit = d.getTime() + 5 * 366 * 864e5;
while (out.length < count && d.getTime() < limit) {
const dayOk = c.domAny && c.dowAny ? true
: c.domAny ? c.dow[d.getDay()]!
: c.dowAny ? c.dom[d.getDate()]!
: c.dom[d.getDate()]! || c.dow[d.getDay()]!; // both restricted: either matches
if (!c.month[d.getMonth() + 1]) { d.setMonth(d.getMonth() + 1, 1); d.setHours(0, 0, 0, 0); continue; }
if (!dayOk) { d.setDate(d.getDate() + 1); d.setHours(0, 0, 0, 0); continue; }
if (!c.hour[d.getHours()]) { d.setHours(d.getHours() + 1, 0, 0, 0); continue; }
if (!c.minute[d.getMinutes()]) { d.setMinutes(d.getMinutes() + 1); continue; }
out.push(new Date(d));
d.setMinutes(d.getMinutes() + 1);
}
return out;
}
How it works
What it solves
Cron strings are compact and easy to get wrong. Showing the next five run times beside an input field lets users confirm what */15 9-17 * * 1-5 really means before saving a schedule, and a dependency-free parser makes that possible in any TypeScript project.
How it works
parseField converts one field into a boolean lookup array. It splits on commas, then handles *, a-b ranges, and /step suffixes, validating every number against the field's minimum and maximum. Day-of-week accepts 7 as Sunday and folds it to 0. parseCron returns the five arrays plus two flags recording whether the day-of-month and day-of-week fields were restricted.
nextRuns walks forward from a start date, but jumps rather than stepping minute by minute: if the month does not match it skips to the first of the next month, if the day does not match it moves to the next midnight, and if the hour does not match it moves to the next hour. Only when everything else matches does it test minutes. Standard cron semantics apply for days: when both day fields are restricted, a date matches if either one does.
A search window of about five years stops impossible expressions such as the 31st of February from looping forever.
Adapting it
- Add
@dailystyle aliases by mapping them to five-field strings first. - Add a seconds field by extending the arrays and the inner loop.
- Use
Intl.DateTimeFormatwith a time zone to display results elsewhere.
Pitfalls
- Calculations use the local time zone of the runtime.
- Names such as MON or JAN are not supported here.
How to use it
- Save as
lib/cron.ts(pure TypeScript, no packages). const runs = nextRuns('*/15 9-17 * * 1-5', new Date(), 5)returns fiveDateobjects.- Wrap
parseCronin a try/catch to show validation messages in your form. - Format the dates with
toLocaleStringin your UI.
Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.