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();