Developer

CRON PARSER

Parse cron expressions in plain English. See next 10 execution times and build schedules from common presets.

Presets
Minute
Hour
Day (M)
Month
Day (W)
—
Next 10 Executions
Developer Reference

Core Algorithm & Standalone Script

Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.

const MONTHS = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
  const DAYS = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
  const MONTH_NAMES = ['January','February','March','April','May','June','July','August','September','October','November','December'];
  const DAY_NAMES = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];

  function parseField(field, min, max) {
    if (field === '*') return { all: true, values: null };
    const values = new Set();
    for (const part of field.split(',')) {
      if (part.includes('/')) {
        const [range, step] = part.split('/');
        const s = parseInt(step);
        const [rMin, rMax] = range === '*' ? [min, max] : range.split('-').map(Number);
        for (let i = rMin; i <= (rMax || max); i += s) values.add(i);
      } else if (part.includes('-')) {
        const [lo, hi] = part.split('-').map(Number);
        for (let i = lo; i <= hi; i++) values.add(i);
      } else {
        values.add(parseInt(part));
      }
    }
    return { all: false, values };
  }

  function matchesField(val, parsed) {
    if (parsed.all) return true;
    return parsed.values.has(val);
  }

  function nextRuns(expr, count = 10) {
    const parts = expr.trim().split(/\s+/);
    if (parts.length < 5) return null;
    const [mF, hF, domF, monF, dowF] = parts;
    try {
      const fields = [parseField(mF,0,59), parseField(hF,0,23), parseField(domF,1,31), parseField(monF,1,12), parseField(dowF,0,6)];
      const runs = [];
      let d = new Date();
      d.setSeconds(0); d.setMilliseconds(0);
      d = new Date(d.getTime() + 60000); // start from next minute
      for (let iter = 0; iter < 500000 && runs.length < count; iter++) {
        if (matchesField(d.getMonth()+1, fields[3]) &&
            matchesField(d.getDate(), fields[2]) &&
            matchesField(d.getDay(), fields[4]) &&
            matchesField(d.getHours(), fields[1]) &&
            matchesField(d.getMinutes(), fields[0])) {
          runs.push(new Date(d));
        }
        d = new Date(d.getTime() + 60000);
      }
      return runs;
    } catch { return null; }
  }

  function describeExpr(expr) {
    const parts = expr.trim().split(/\s+/);
    if (parts.length < 5) return 'Invalid cron expression (need 5 fields: min hour dom mon dow)';
    const [min, hour, dom, mon, dow] = parts;

    const descMin = min === '*' ? 'every minute' : min.startsWith('*/') ? `every ${min.slice(2)} minutes` : `at minute ${min}`;
    const descHour = hour === '*' ? '' : hour.startsWith('*/') ? ` every ${hour.slice(2)} hours` : ` at ${hour.padStart(2,'0')}:${min === '*' ? '00' : min.padStart(2,'0')}`;
    const descDom = dom === '*' ? '' : ` on day ${dom} of the month`;
    const descMon = mon === '*' ? '' : ` in ${mon.split(',').map(m => MONTH_NAMES[parseInt(m)-1] || m).join(', ')}`;
    const descDow = dow === '*' ? '' : dow === '1-5' ? ' on weekdays (Mon–Fri)' : dow === '0,6' || dow === '6,0' ? ' on weekends' : ` on ${dow.split(',').map(d => DAY_NAMES[parseInt(d)] || d).join(', ')}`;

    if (expr === '* * * * *') return 'Runs every minute';
    if (expr === '0 * * * *') return 'Runs at the start of every hour';
    if (expr === '0 0 * * *') return 'Runs once a day at midnight';
    if (expr === '0 0 * * 0') return 'Runs at midnight every Sunday';
    if (expr === '0 0 1 * *') return 'Runs at midnight on the 1st of every month';
    if (expr === '0 0 1 1 *') return 'Runs at midnight on January 1st (yearly)';

    return `Runs ${descMin}${descHour}${descDom}${descMon}${descDow}`;
  }

  function fieldDesc(field, type) {
    if (field === '*') return 'every ' + type;
    if (field.startsWith('*/')) return 'every ' + field.slice(2);
    if (field.includes('-')) return field.replace('-', '–');
    if (field.includes(',')) return field;
    return field;
  }

  function relTime(d) {
    const diff = d - Date.now();
    const s = Math.floor(diff / 1000);
    if (s < 60) return 'in ' + s + 's';
    if (s < 3600) return 'in ' + Math.floor(s/60) + 'm';
    if (s < 86400) return 'in ' + Math.floor(s/3600) + 'h';
    return 'in ' + Math.floor(s/86400) + 'd';
  }

  function parse() {
    const expr = document.getElementById('cronInput').value;
    const errEl = document.getElementById('errMsg');
    errEl.style.display = 'none';
    document.getElementById('cronInput').classList.remove('error');

    const parts = expr.trim().split(/\s+/);
    if (parts.length < 5) {
      document.getElementById('cronDesc').textContent = 'Enter a 5-field cron expression';
      document.getElementById('cronFields').innerHTML = '';
      document.getElementById('nextRuns').innerHTML = '';
      return;
    }

    const [min, hour, dom, mon, dow] = parts;
    document.getElementById('cronDesc').textContent = describeExpr(expr);

    const FIELD_DEFS = [
      { name: 'Minute', val: min, type: 'minute', range: '0–59' },
      { name: 'Hour', val: hour, type: 'hour', range: '0–23' },
      { name: 'Day (Month)', val: dom, type: 'day', range: '1–31' },
      { name: 'Month', val: mon, type: 'month', range: '1–12' },
      { name: 'Day (Week)', val: dow, type: 'weekday', range: '0–6' },
    ];
    document.getElementById('cronFields').innerHTML = FIELD_DEFS.map(f =>
      `<div class="cron-field">
        <div class="cron-field-name">${f.name}</div>
        <div class="cron-field-val">${f.val}</div>
        <div class="cron-field-desc">${fieldDesc(f.val, f.type)}</div>
      </div>`
    ).join('');

    const runs = nextRuns(expr);
    if (!runs) {
      errEl.textContent = 'Could not compute next runs — check your expression.';
      errEl.style.display = '';
      document.getElementById('nextRuns').innerHTML = '';
      return;
    }

    document.getElementById('nextRuns').innerHTML = runs.map((d, i) =>
      `<div class="next-run">
        <div class="next-run-num">${i+1}</div>
        <div class="next-run-date">${d.toLocaleString()}</div>
        <div class="next-run-rel">${relTime(d)}</div>
      </div>`
    ).join('');
  }

  function setExpr(expr) {
    document.getElementById('cronInput').value = expr;
    parse();
  }

  function copyCron() {
    const expr = document.getElementById('cronInput').value;
    navigator.clipboard.writeText(expr).catch(() => {});
    const btn = document.getElementById('copyBtn');
    btn.textContent = 'Copied!'; btn.classList.add('copied');
    setTimeout(() => { btn.textContent = 'Copy Expression'; btn.classList.remove('copied'); }, 2000);
  }

  parse();