The problem
Need to schedule tasks using cron syntax expressions
The solution
class CronScheduler {
constructor() {
this.jobs = new Map();
this.timers = new Map();
this.running = false;
}
// Parse cron expression
parseCronExpression(expression) {
const parts = expression.trim().split(/s+/);
if (parts.length !== 5 && parts.length !== 6) {
throw new Error('Invalid cron expression format');
}
const [minute, hour, dayOfMonth, month, dayOfWeek, ...rest] = parts;
const year = parts.length === 6 ? rest[0] : '*';
return {
minute: this.parseField(minute, 0, 59),
hour: this.parseField(hour, 0, 23),
dayOfMonth: this.parseField(dayOfMonth, 1, 31),
month: this.parseField(month, 1, 12),
dayOfWeek: this.parseField(dayOfWeek, 0, 6), // 0 = Sunday
year: year === '*' ? '*' : this.parseField(year, 1970, 2099)
};
}
parseField(field, min, max) {
if (field === '*') {
return { type: 'any' };
}
// Handle ranges (1-5)
if (field.includes('-')) {
const [start, end] = field.split('-').map(Number);
return { type: 'range', start, end };
}
// Handle steps (*/15)
if (field.includes('/')) {
const [range, step] = field.split('/');
const stepNum = Number(step);
if (range === '*') {
return { type: 'step', step: stepNum };
}
// Handle ranges with steps (1-30/5)
if (range.includes('-')) {
const [start, end] = range.split('-').map(Number);
return { type: 'rangeStep', start, end, step: stepNum };
}
}
// Handle lists (1,3,5)
if (field.includes(',')) {
const values = field.split(',').map(Number);
return { type: 'list', values };
}
// Single value
const value = Number(field);
return { type: 'value', value };
}
matchesCron(cron, date) {
const fields = [
{ value: date.getMinutes(), cron: cron.minute },
{ value: date.getHours(), cron: cron.hour },
{ value: date.getDate(), cron: cron.dayOfMonth },
{ value: date.getMonth() + 1, cron: cron.month }, // JS months are 0-indexed
{ value: date.getDay(), cron: cron.dayOfWeek },
{ value: date.getFullYear(), cron: cron.year }
];
return fields.every(({ value, cron }) => {
if (cron.type === 'any') return true;
if (cron.type === 'value') return value === cron.value;
if (cron.type === 'range') return value >= cron.start && value <= cron.end;
if (cron.type === 'list') return cron.values.includes(value);
if (cron.type === 'step') return value % cron.step === 0;
if (cron.type === 'rangeStep') {
return value >= cron.start &&
value <= cron.end &&
(value - cron.start) % cron.step === 0;
}
return false;
});
}
schedule(name, cronExpression, task) {
const cron = this.parseCronExpression(cronExpression);
this.jobs.set(name, { cron, task, expression: cronExpression });
if (this.running) {
this.scheduleNextExecution(name);
}
return this;
}
unschedule(name) {
const timer = this.timers.get(name);
if (timer) {
clearTimeout(timer);
this.timers.delete(name);
}
this.jobs.delete(name);
return this;
}
scheduleNextExecution(name) {
const job = this.jobs.get(name);
if (!job) return;
const now = new Date();
let next = new Date(now.getTime() + 60000); // Start checking from next minute
// Find next matching time (brute force approach)
const maxAttempts = 10000; // Safety limit
let attempts = 0;
while (attempts < maxAttempts) {
if (this.matchesCron(job.cron, next)) {
const delay = next.getTime() - now.getTime();
const timer = setTimeout(() => {
try {
job.task();
} catch (error) {
console.error(`Error executing job "${name}":`, error);
}
// Schedule next execution
this.scheduleNextExecution(name);
}, delay);
this.timers.set(name, timer);
return;
}
// Move to next minute
next = new Date(next.getTime() + 60000);
attempts++;
}
console.warn(`Could not find next execution time for job "${name}"`);
}
start() {
if (this.running) return;
this.running = true;
for (const name of this.jobs.keys()) {
this.scheduleNextExecution(name);
}
return this;
}
stop() {
this.running = false;
for (const timer of this.timers.values()) {
clearTimeout(timer);
}
this.timers.clear();
return this;
}
listJobs() {
return Array.from(this.jobs.entries()).map(([name, job]) => ({
name,
expression: job.expression,
nextExecution: this.getNextExecution(name)
}));
}
getNextExecution(name) {
const timer = this.timers.get(name);
if (!timer) return null;
// This is simplified - in reality we'd need to store the scheduled time
return 'Scheduled';
}
}Put it to work
const scheduler = new CronScheduler();
// Schedule jobs
scheduler.schedule('dailyBackup', '0 2 * * *', () => {
console.log('Running daily backup at 2 AM');
});
scheduler.schedule('every5Minutes', '*/5 * * * *', () => {
console.log('Running every 5 minutes');
});
scheduler.schedule('weekdaysAt9', '0 9 * * 1-5', () => {
console.log('Running weekdays at 9 AM');
});
scheduler.schedule('firstOfMonth', '0 0 1 * *', () => {
console.log('Running on the first day of each month');
});
// Start scheduler
scheduler.start();
// List all jobs
console.log(scheduler.listJobs());
// Stop after 10 minutes
setTimeout(() => {
scheduler.stop();
console.log('Scheduler stopped');
}, 600000);
// Unschedule a job
scheduler.unschedule('every5Minutes');