"use client"; import { useState, useMemo, useCallback } from "react"; import ToolLayout from "@/components/ToolLayout"; interface FieldDefinition { name: string; key: string; min: number; max: number; namesMap?: Record; reverseNamesMap?: Record; description: string; } const MONTH_NAMES: Record = { JAN: 1, FEB: 2, MAR: 3, APR: 4, MAY: 5, JUN: 6, JUL: 7, AUG: 8, SEP: 9, OCT: 10, NOV: 11, DEC: 12, }; const MONTH_NAMES_REV: Record = { 1: "January", 2: "February", 3: "March", 4: "April", 5: "May", 6: "June", 7: "July", 8: "August", 9: "September", 10: "October", 11: "November", 12: "December", }; const DOW_NAMES: Record = { SUN: 0, MON: 1, TUE: 2, WED: 3, THU: 4, FRI: 5, SAT: 6, }; const DOW_NAMES_REV: Record = { 0: "Sunday", 1: "Monday", 2: "Tuesday", 3: "Wednesday", 4: "Thursday", 5: "Friday", 6: "Saturday", }; const FIELDS: FieldDefinition[] = [ { name: "Minute", key: "minute", min: 0, max: 59, description: "Minutes past the hour (0 - 59)", }, { name: "Hour", key: "hour", min: 0, max: 23, description: "Hours of the day (0 - 23, 24-hour clock)", }, { name: "Day of Month", key: "dom", min: 1, max: 31, description: "Days of the month (1 - 31)", }, { name: "Month", key: "month", min: 1, max: 12, namesMap: MONTH_NAMES, reverseNamesMap: MONTH_NAMES_REV, description: "Months of the year (1 - 12 or JAN - DEC)", }, { name: "Day of Week", key: "dow", min: 0, max: 7, namesMap: DOW_NAMES, reverseNamesMap: DOW_NAMES_REV, description: "Days of the week (0 - 7 or SUN - SAT, 0 & 7 = Sunday)", }, ]; interface ParsedField { raw: string; fieldDef: FieldDefinition; valid: boolean; error?: string; values: number[]; isWildcard: boolean; isStep: boolean; explanation: string; } interface ParsedCron { valid: boolean; error?: string; fields: ParsedField[]; humanExplanation: string; } // Helper to format hours in 12-hour AM/PM format function formatHour12(h: number): string { const period = h >= 12 ? "PM" : "AM"; const hour12 = h % 12 === 0 ? 12 : h % 12; return `${hour12}:00 ${period}`; } // Helper to format full time in 12-hour format function formatTime12(h: number, m: number): string { const period = h >= 12 ? "PM" : "AM"; const hour12 = h % 12 === 0 ? 12 : h % 12; const padM = m.toString().padStart(2, "0"); return `${hour12}:${padM} ${period}`; } // Helper for relative time description function getRelativeTime(targetDate: Date, now: Date): string { const diffMs = targetDate.getTime() - now.getTime(); const diffSec = Math.floor(diffMs / 1000); if (diffSec <= 0) return "just now"; if (diffSec < 60) return `in ${diffSec}s`; const diffMin = Math.floor(diffSec / 60); if (diffMin < 60) return `in ${diffMin} min${diffMin > 1 ? "s" : ""}`; const diffHours = Math.floor(diffMin / 60); const remainingMins = diffMin % 60; if (diffHours < 24) { return `in ${diffHours}h ${remainingMins > 0 ? `${remainingMins}m` : ""}`; } const diffDays = Math.floor(diffHours / 24); const remainingHours = diffHours % 24; if (diffDays === 1) return `tomorrow in ${diffHours}h`; if (diffDays < 30) return `in ${diffDays} days ${remainingHours > 0 ? `${remainingHours}h` : ""}`; const diffMonths = Math.floor(diffDays / 30); return `in ${diffMonths} month${diffMonths > 1 ? "s" : ""}`; } // Parse a single cron field token function parseCronField(raw: string, fieldDef: FieldDefinition): ParsedField { const trimmed = raw.trim(); if (!trimmed) { return { raw, fieldDef, valid: false, error: `Field "${fieldDef.name}" cannot be empty`, values: [], isWildcard: false, isStep: false, explanation: "", }; } const isWildcard = trimmed === "*"; const isStep = trimmed.includes("/"); const valuesSet = new Set(); const convertTokenToNum = (tok: string): number | null => { const upper = tok.trim().toUpperCase(); if (fieldDef.namesMap && fieldDef.namesMap[upper] !== undefined) { return fieldDef.namesMap[upper]; } const n = Number(tok); if (!isNaN(n) && Number.isInteger(n)) { return n; } return null; }; const parts = trimmed.split(","); for (const part of parts) { const subParts = part.split("/"); if (subParts.length > 2) { return { raw, fieldDef, valid: false, error: `Invalid step expression "${part}" in ${fieldDef.name}`, values: [], isWildcard, isStep, explanation: "", }; } let rangeStart = fieldDef.min; let rangeEnd = fieldDef.max; let step = 1; if (subParts.length === 2) { const stepNum = Number(subParts[1]); if (isNaN(stepNum) || stepNum < 1 || !Number.isInteger(stepNum)) { return { raw, fieldDef, valid: false, error: `Invalid step value "${subParts[1]}" in ${fieldDef.name}`, values: [], isWildcard, isStep, explanation: "", }; } step = stepNum; } const basePart = subParts[0].trim(); if (basePart === "*") { rangeStart = fieldDef.min; rangeEnd = fieldDef.max; } else if (basePart.includes("-")) { const rangeParts = basePart.split("-"); if (rangeParts.length !== 2) { return { raw, fieldDef, valid: false, error: `Invalid range "${basePart}" in ${fieldDef.name}`, values: [], isWildcard, isStep, explanation: "", }; } const startNum = convertTokenToNum(rangeParts[0]); const endNum = convertTokenToNum(rangeParts[1]); if (startNum === null || endNum === null) { return { raw, fieldDef, valid: false, error: `Invalid range bounds "${basePart}" in ${fieldDef.name}`, values: [], isWildcard, isStep, explanation: "", }; } if (startNum < fieldDef.min || endNum > fieldDef.max) { return { raw, fieldDef, valid: false, error: `Range ${startNum}-${endNum} out of bounds (${fieldDef.min}-${fieldDef.max}) in ${fieldDef.name}`, values: [], isWildcard, isStep, explanation: "", }; } if (startNum > endNum) { return { raw, fieldDef, valid: false, error: `Range start ${startNum} is greater than end ${endNum} in ${fieldDef.name}`, values: [], isWildcard, isStep, explanation: "", }; } rangeStart = startNum; rangeEnd = endNum; } else { const singleNum = convertTokenToNum(basePart); if (singleNum === null) { return { raw, fieldDef, valid: false, error: `Invalid token "${basePart}" in ${fieldDef.name}`, values: [], isWildcard, isStep, explanation: "", }; } if (singleNum < fieldDef.min || singleNum > fieldDef.max) { return { raw, fieldDef, valid: false, error: `Value ${singleNum} out of allowed range (${fieldDef.min}-${fieldDef.max}) in ${fieldDef.name}`, values: [], isWildcard, isStep, explanation: "", }; } if (subParts.length === 2) { rangeStart = singleNum; rangeEnd = fieldDef.max; } else { rangeStart = singleNum; rangeEnd = singleNum; } } for (let i = rangeStart; i <= rangeEnd; i += step) { let val = i; // In Day of Week, 7 is also Sunday (0) if (fieldDef.key === "dow" && val === 7) { val = 0; } valuesSet.add(val); } } const values = Array.from(valuesSet).sort((a, b) => a - b); // Build field-level explanation let explanation = ""; if (isWildcard) { explanation = `Every ${fieldDef.name.toLowerCase()}`; } else if (trimmed.startsWith("*/")) { const stepVal = trimmed.substring(2); explanation = `Every ${stepVal} ${fieldDef.name.toLowerCase()}s`; } else if (fieldDef.key === "dow") { const dayNames = values.map((v) => DOW_NAMES_REV[v] || v.toString()); if (values.length === 5 && values.every((v, i) => v === i + 1)) { explanation = "Monday through Friday (weekdays)"; } else if (values.length === 2 && values.includes(0) && values.includes(6)) { explanation = "Saturday and Sunday (weekends)"; } else { explanation = `On ${dayNames.join(", ")}`; } } else if (fieldDef.key === "month") { const monthNames = values.map((v) => MONTH_NAMES_REV[v] || `Month ${v}`); explanation = `In ${monthNames.join(", ")}`; } else if (fieldDef.key === "hour") { if (values.length === 1) { explanation = `At ${formatHour12(values[0])} (${values[0].toString().padStart(2, "0")}:00)`; } else { explanation = `At hours ${values.map((v) => v.toString().padStart(2, "0")).join(", ")}`; } } else if (fieldDef.key === "minute") { if (values.length === 1) { explanation = `At minute ${values[0]}`; } else { explanation = `At minutes ${values.join(", ")}`; } } else if (fieldDef.key === "dom") { if (values.length === 1) { explanation = `On day ${values[0]} of the month`; } else { explanation = `On days ${values.join(", ")} of the month`; } } return { raw, fieldDef, valid: true, values, isWildcard, isStep, explanation, }; } // Generate overall human-readable explanation function generateCronExplanation(fields: ParsedField[]): string { if (fields.some((f) => !f.valid)) return "Invalid cron expression"; const [minF, hourF, domF, monthF, dowF] = fields; const minRaw = minF.raw.trim(); const hourRaw = hourF.raw.trim(); const domRaw = domF.raw.trim(); const monthRaw = monthF.raw.trim(); const dowRaw = dowF.raw.trim(); // 1. Every minute (* * * * *) if (minRaw === "*" && hourRaw === "*" && domRaw === "*" && monthRaw === "*" && dowRaw === "*") { return "At every minute of every day"; } // 2. Every N minutes (*/5 * * * *) if (minRaw.startsWith("*/") && hourRaw === "*" && domRaw === "*" && monthRaw === "*" && dowRaw === "*") { const step = minRaw.replace("*/", ""); return `Every ${step} minutes`; } // 3. Every hour on minute 0 (0 * * * *) if (minRaw === "0" && hourRaw === "*" && domRaw === "*" && monthRaw === "*" && dowRaw === "*") { return "At the start of every hour (minute 0)"; } // 4. Every N hours (0 */2 * * *) if (minRaw === "0" && hourRaw.startsWith("*/") && domRaw === "*" && monthRaw === "*" && dowRaw === "*") { const step = hourRaw.replace("*/", ""); return `Every ${step} hours, at minute 0`; } // Build natural description const timeDesc: string[] = []; if (minF.values.length === 1 && hourF.values.length === 1) { const h = hourF.values[0]; const m = minF.values[0]; timeDesc.push(`At ${formatTime12(h, m)} (${h.toString().padStart(2, "0")}:${m.toString().padStart(2, "0")})`); } else if (minF.values.length === 1 && hourF.isWildcard) { timeDesc.push(`At minute ${minF.values[0]} of every hour`); } else if (minRaw.startsWith("*/") && hourF.values.length === 1) { const step = minRaw.replace("*/", ""); timeDesc.push(`Every ${step} minutes, during hour ${hourF.values[0]}`); } else if (minRaw.startsWith("*/") && hourF.isWildcard) { const step = minRaw.replace("*/", ""); timeDesc.push(`Every ${step} minutes`); } else { timeDesc.push(`${minF.explanation}, ${hourF.explanation.toLowerCase()}`); } const dateDesc: string[] = []; // Day of Month & Day of Week if (!domF.isWildcard && dowF.isWildcard) { dateDesc.push(`on day ${domF.values.join(", ")} of the month`); } else if (domF.isWildcard && !dowF.isWildcard) { if (dowF.values.length === 5 && [1, 2, 3, 4, 5].every((v) => dowF.values.includes(v))) { dateDesc.push("Monday through Friday (weekdays)"); } else if (dowF.values.length === 2 && dowF.values.includes(0) && dowF.values.includes(6)) { dateDesc.push("on weekends (Saturday and Sunday)"); } else { const days = dowF.values.map((v) => DOW_NAMES_REV[v] || v.toString()); dateDesc.push(`every ${days.join(", ")}`); } } else if (!domF.isWildcard && !dowF.isWildcard) { const days = dowF.values.map((v) => DOW_NAMES_REV[v] || v.toString()); dateDesc.push(`on day ${domF.values.join(", ")} of the month and on ${days.join(", ")}`); } // Month if (!monthF.isWildcard) { const months = monthF.values.map((v) => MONTH_NAMES_REV[v] || `Month ${v}`); dateDesc.push(`in ${months.join(", ")}`); } if (dateDesc.length === 0) { return `${timeDesc.join(", ")}, every day`; } return `${timeDesc.join(", ")}, ${dateDesc.join(", ")}`; } // Calculate next scheduled run times (up to count) function computeNextRuns(fields: ParsedField[], count: number = 10, timezone: "local" | "utc" = "local"): Date[] { if (fields.some((f) => !f.valid)) return []; const [minF, hourF, domF, monthF, dowF] = fields; const minSet = new Set(minF.values); const hourSet = new Set(hourF.values); const domSet = new Set(domF.values); const monthSet = new Set(monthF.values); const dowSet = new Set(dowF.values); const results: Date[] = []; const start = new Date(); // Start from next minute const current = new Date(start.getTime()); current.setSeconds(0, 0); current.setMinutes(current.getMinutes() + 1); // Safety limit to avoid infinite loops (max 5 years or 500,000 steps) let steps = 0; const maxSteps = 500000; while (results.length < count && steps < maxSteps) { steps++; const getYear = timezone === "utc" ? current.getUTCFullYear() : current.getFullYear(); const getMonth = (timezone === "utc" ? current.getUTCMonth() : current.getMonth()) + 1; const getDate = timezone === "utc" ? current.getUTCDate() : current.getDate(); const getDay = timezone === "utc" ? current.getUTCDay() : current.getDay(); const getHour = timezone === "utc" ? current.getUTCHours() : current.getHours(); const getMin = timezone === "utc" ? current.getUTCMinutes() : current.getMinutes(); // Check Month if (!monthSet.has(getMonth)) { // Advance to start of next month if (timezone === "utc") { current.setUTCMonth(current.getUTCMonth() + 1, 1); current.setUTCHours(0, 0, 0, 0); } else { current.setMonth(current.getMonth() + 1, 1); current.setHours(0, 0, 0, 0); } continue; } // Check Day of Month & Day of Week const domMatch = domSet.has(getDate); const dowMatch = dowSet.has(getDay); let dayMatch = false; if (domF.isWildcard && dowF.isWildcard) { dayMatch = true; } else if (!domF.isWildcard && dowF.isWildcard) { dayMatch = domMatch; } else if (domF.isWildcard && !dowF.isWildcard) { dayMatch = dowMatch; } else { // Both specified: standard cron OR matching dayMatch = domMatch || dowMatch; } if (!dayMatch) { // Advance to next day if (timezone === "utc") { current.setUTCDate(current.getUTCDate() + 1); current.setUTCHours(0, 0, 0, 0); } else { current.setDate(current.getDate() + 1); current.setHours(0, 0, 0, 0); } continue; } // Check Hour if (!hourSet.has(getHour)) { // Advance to next hour if (timezone === "utc") { current.setUTCHours(current.getUTCHours() + 1, 0, 0, 0); } else { current.setHours(current.getHours() + 1, 0, 0, 0); } continue; } // Check Minute if (minSet.has(getMin)) { results.push(new Date(current.getTime())); } // Advance 1 minute if (timezone === "utc") { current.setUTCMinutes(current.getUTCMinutes() + 1); } else { current.setMinutes(current.getMinutes() + 1); } } return results; } const PRESETS = [ { name: "Every minute", cron: "* * * * *", category: "Frequent" }, { name: "Every 5 minutes", cron: "*/5 * * * *", category: "Frequent" }, { name: "Every 15 minutes", cron: "*/15 * * * *", category: "Frequent" }, { name: "Every 30 minutes", cron: "*/30 * * * *", category: "Frequent" }, { name: "Every hour", cron: "0 * * * *", category: "Hourly" }, { name: "Every 2 hours", cron: "0 */2 * * *", category: "Hourly" }, { name: "Every 6 hours", cron: "0 */6 * * *", category: "Hourly" }, { name: "Every day at midnight", cron: "0 0 * * *", category: "Daily" }, { name: "Every day at 9:00 AM", cron: "0 9 * * *", category: "Daily" }, { name: "Every weekday at 9:00 AM", cron: "0 9 * * 1-5", category: "Weekly" }, { name: "Every Monday at midnight", cron: "0 0 * * 1", category: "Weekly" }, { name: "Every Sunday at midnight", cron: "0 0 * * 0", category: "Weekly" }, { name: "Twice daily (12am & 12pm)", cron: "0 0,12 * * *", category: "Daily" }, { name: "1st of every month (midnight)", cron: "0 0 1 * *", category: "Monthly" }, { name: "1st & 15th of every month", cron: "0 0 1,15 * *", category: "Monthly" }, { name: "Quarterly (Jan, Apr, Jul, Oct 1st)", cron: "0 0 1 1,4,7,10 *", category: "Monthly" }, ]; export default function CronParserPage() { const [cronExpression, setCronExpression] = useState("*/5 * * * *"); const [runCount, setRunCount] = useState(8); const [timezone, setTimezone] = useState<"local" | "utc">("local"); const [copiedCron, setCopiedCron] = useState(false); const [copiedSchedule, setCopiedSchedule] = useState(false); // Parse 5-part cron expression const parsedCron = useMemo(() => { const rawTrimmed = cronExpression.trim(); if (!rawTrimmed) { return { valid: false, error: "Please enter a 5-part cron expression", fields: [], humanExplanation: "", }; } const tokens = rawTrimmed.split(/\s+/); if (tokens.length !== 5) { return { valid: false, error: `Expected 5 space-separated parts (Minute Hour Day-of-Month Month Day-of-Week), found ${tokens.length}`, fields: [], humanExplanation: "", }; } const fields = tokens.map((token, index) => { return parseCronField(token, FIELDS[index]); }); const invalidField = fields.find((f) => !f.valid); if (invalidField) { return { valid: false, error: invalidField.error || "Invalid cron field", fields, humanExplanation: "", }; } const humanExplanation = generateCronExplanation(fields); return { valid: true, fields, humanExplanation, }; }, [cronExpression]); // Compute next runs const nextRuns = useMemo(() => { if (!parsedCron.valid || parsedCron.fields.length !== 5) return []; return computeNextRuns(parsedCron.fields, runCount, timezone); }, [parsedCron, runCount, timezone]); // Copy Cron String const handleCopyCron = async () => { try { await navigator.clipboard.writeText(cronExpression.trim()); setCopiedCron(true); setTimeout(() => setCopiedCron(false), 2000); } catch { // Fallback } }; // Copy Next Run Times const handleCopySchedule = async () => { if (nextRuns.length === 0) return; const text = nextRuns .map((d, i) => `${i + 1}. ${timezone === "utc" ? d.toUTCString() : d.toString()}`) .join("\n"); try { await navigator.clipboard.writeText(text); setCopiedSchedule(true); setTimeout(() => setCopiedSchedule(false), 2000); } catch { // Fallback } }; // Handle single field input update const handleFieldChange = useCallback( (index: number, val: string) => { const tokens = cronExpression.trim().split(/\s+/); while (tokens.length < 5) tokens.push("*"); tokens[index] = val || "*"; setCronExpression(tokens.join(" ")); }, [cronExpression] ); const now = useMemo(() => new Date(), []); return ( Cron Expression Parser Online — DevToolbox
{/* Top Preset Gallery */}
Common Cron Presets Click any preset to load
{PRESETS.map((p) => { const isSelected = cronExpression.trim() === p.cron; return ( ); })}
{/* Main Cron Input Bar */}
Minute Hour Day Month Weekday
setCronExpression(e.target.value)} placeholder="* * * * *" className="w-full p-4 font-mono text-xl sm:text-2xl tracking-wider border border-[#1a1a1a] bg-[#0a0a0a] text-gray-200 focus:ring-1 focus:ring-blue-900 focus:outline-none shadow-inner" />
{/* Human Readable Explanation Banner */} {parsedCron.valid ? (
✓
Human-Readable Schedule

“{parsedCron.humanExplanation}”

) : (
!
Syntax Error

{parsedCron.error}

)}
{/* 5 Field Breakdown Interactive Cards */}

Cron Field Breakdown & Structure

Edit individual fields directly below
{FIELDS.map((fDef, idx) => { const parsedF = parsedCron.fields[idx]; const rawToken = parsedF ? parsedF.raw : ""; const isValid = parsedF ? parsedF.valid : true; return (
{fDef.name} {fDef.min}-{fDef.max}
handleFieldChange(idx, e.target.value)} placeholder="*" className="w-full p-2 font-mono text-base font-bold text-center border border-[#1a1a1a] bg-black text-gray-200 focus:ring-1 focus:ring-blue-900 focus:outline-none" />

{fDef.description}

{parsedF && parsedF.valid && (
{parsedF.explanation}
Matches {parsedF.values.length} {parsedF.values.length === 1 ? "value" : "values"}
)}
); })}
{/* Scheduled Runs Section */}
⏰

Upcoming Scheduled Runs (Next {runCount})

{/* Timezone Toggle */}
Timezone:
{/* Count selector */}
Count:
{nextRuns.length > 0 ? (
{nextRuns.map((date, idx) => { const relative = getRelativeTime(date, now); const isLocal = timezone === "local"; const dateStr = isLocal ? date.toLocaleDateString("en-US", { weekday: "short", year: "numeric", month: "short", day: "numeric", }) : date.toUTCString().slice(0, 16); const timeStr = isLocal ? date.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", second: "2-digit", hour12: true, }) : date.toUTCString().slice(17, 25) + " UTC"; return (
{idx + 1}
{dateStr} {timeStr}
{relative} {date.toISOString()}
); })}
) : (
No upcoming runs calculated. Please check your cron expression.
)}
{/* Cron Syntax Cheat Sheet */}

Cron Syntax Reference & Special Characters

* (Asterisk)

Wildcard (Any Value)

Matches every possible value in the field. E.g. * in hour means every hour.

, (Comma)

Value List

Specifies discrete multiple values. E.g. 1,15,30 in minute runs at minutes 1, 15, 30.

- (Hyphen)

Range of Values

Specifies an inclusive range. E.g. 1-5 in weekday means Monday through Friday.

/ (Slash)

Step Values

Specifies step increments. E.g. */15 in minute runs every 15 minutes.

); }