typescript 145 lines · 3 tabs

Cron-Expression Scheduler with Field Parser and a Tick Loop in TypeScript

Shared by codesnips Aug 2026
3 tabs
export interface CronFields {
  minute: Set<number>;
  hour: Set<number>;
  dayOfMonth: Set<number>;
  month: Set<number>;
  dayOfWeek: Set<number>;
}

function parseField(field: string, min: number, max: number): Set<number> {
  const result = new Set<number>();
  for (const part of field.split(",")) {
    let step = 1;
    let range = part;
    const slash = part.indexOf("/");
    if (slash !== -1) {
      step = parseInt(part.slice(slash + 1), 10);
      range = part.slice(0, slash);
    }
    let start = min;
    let end = max;
    if (range !== "*") {
      const [a, b] = range.split("-");
      start = parseInt(a, 10);
      end = b !== undefined ? parseInt(b, 10) : start;
    }
    if (isNaN(start) || isNaN(end) || isNaN(step) || step < 1) {
      throw new Error(`Invalid cron field: "${field}"`);
    }
    for (let v = start; v <= end; v += step) {
      if (v < min || v > max) throw new Error(`Value ${v} out of range in "${field}"`);
      result.add(v);
    }
  }
  return result;
}

export function parseExpression(expr: string): CronFields {
  const parts = expr.trim().split(/\s+/);
  if (parts.length !== 5) {
    throw new Error(`Cron expression must have 5 fields, got ${parts.length}`);
  }
  const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
  return {
    minute: parseField(minute, 0, 59),
    hour: parseField(hour, 0, 23),
    dayOfMonth: parseField(dayOfMonth, 1, 31),
    month: parseField(month, 1, 12),
    dayOfWeek: parseField(dayOfWeek, 0, 6),
  };
}

export function matches(fields: CronFields, date: Date): boolean {
  return (
    fields.minute.has(date.getMinutes()) &&
    fields.hour.has(date.getHours()) &&
    fields.dayOfMonth.has(date.getDate()) &&
    fields.month.has(date.getMonth() + 1) &&
    fields.dayOfWeek.has(date.getDay())
  );
}
3 files · typescript Explain with highlit

This snippet builds a small recurring-task scheduler around two collaborating pieces: a cron-expression parser that turns strings like */5 * * * * into matchable sets, and a scheduler that ticks once per minute and runs whatever jobs are due. It is the sort of self-contained implementation used when a full dependency like node-cron is overkill but the standard setInterval alone is too blunt.

The cron-parser tab focuses on one job: converting each of the five cron fields into an explicit Set<number> of allowed values. parseField handles the four forms that make up almost all real cron usage — the wildcard *, step values */n, ranges a-b, and comma lists — by normalizing everything into ranges and then filling a set. Expanding to sets up front trades a little memory for very cheap matching later: checking whether a given minute is due becomes a Set.has lookup rather than repeated arithmetic. The matches function ties the fields to a concrete Date, and note the intentional detail that day-of-week uses getDay() while day-of-month uses getDate(), mirroring standard cron semantics. Invalid input throws early from parseExpression, so a bad schedule fails at registration time rather than silently never firing.

The CronScheduler tab drives everything from a single setInterval aligned to minute boundaries. Rather than one timer per task, it keeps a Map of registered ScheduledTasks and, on each tick, filters them with the parser's matches. This one-timer design scales cleanly to many tasks and keeps them evaluated against the same clock instant. A running guard set prevents overlapping executions of a slow task, and each run is wrapped so a thrown error is caught and reported through onError instead of killing the loop. The lastRun bookkeeping guards against a tick firing twice within the same minute.

The usage tab shows registration and graceful shutdown. Tasks are added with register, the loop starts with start, and stop clears the interval. The main trade-off to understand is resolution: this scheduler is minute-granular by design, so it is ideal for periodic maintenance, report generation, or polling, but not for sub-second timing. Its strengths are predictability, no external dependencies, and error isolation between tasks.


Related snips

Share this code

Here's the card — post it anywhere.

Cron-Expression Scheduler with Field Parser and a Tick Loop in TypeScript — share card
Link copied