One crontab line, two different sets of dates
An idle machine I took over carried this line, 0 0 13 * 5, with a comment that read "run demo-report.csv once a month on the Friday sitting on day 13". Fed into the parser page, its first predicted time is 2024-09-13, yet the run log holds one record for every single Friday. Same expression, two answers: one side insists there are only 2 runs in 2024, the other logged 62 of them. The missing 60 are not luck, they are two different semantics.
The page takes one line of text as its whole input. I pulled nextRun and parseField out of the Crontab Parser template word for word, pinned the starting point at 2024-01-01 00:00:00 and walked forward minute by minute, and came back with two semantic holes: what happens the moment day and weekday are restricted together, intersection versus union, and what happens on short months and on dates that can never exist, where the walk ends empty.
Hole one: the five fields chained with &&
The hit test sits on lines 240 to 244 of the template, and the condition f.minute && f.hour && f.day && f.month && f.week is a full intersection over all five fields, while real vixie-cron switches to or as soon as neither day nor weekday is *. Do the bookkeeping for 2024: day 13 appears 12 times, Friday appears 52 times, a day that is both 13 and a Friday appears 2 times, so the union is 12 + 52 - 2 = 62 while the intersection keeps only those 2 days. In other words the page squeezes 62 runs down to 2, a gap of 60.
The description text stands on the intersection side as well: the natural language it prints reads "at minute 0 of hour 0 on day 13 of every month on Friday", which is and, self consistent with nextRun, and drifting away from cron itself at the same time. What the five fields mean and how the interface demonstrates them is settled inside the site, in Crontab expressions explained, with predicted run times; this note only measures the branches that article never touched.
Figure 1: how many times each reading hits inside the same one year window.
With a single restricted field the two semantics merge
| Expression | The first runs this page gives (Shanghai time) | Ambiguity |
|---|---|---|
0 0 * * 1 | 2024-01-08, 01-15, 01-22, 01-29, 02-05, all Mondays at 00:00 | none, only the weekday is restricted |
0 0 1 * * | 2024-02-01, 03-01, 04-01, 05-01, 06-01 | none, only the day is restricted |
0 0 13 * 5 | 2024-09-13, 2024-12-13, 2025-06-13, 2026-02-13, 2026-03-13, 2026-11-13, 2027-08-13 | yes, and the 8th lookup already returns empty |
The first two rows restrict one field each, so intersection and union fall together and the chain behaves. Once day and weekday narrow at the same time, the very first hit jumps all the way to 2024-09-13, while production keeps running on every Friday.
Hole two: the calendar never hands over that day
0 0 31 * * chains 12 times from 2024-01-01 and hits only 7 months, the ones long enough to carry a thirty first day: 1, 3, 5, 7, 8, 10 and 12, while February and 4, 6, 9 and 11 are skipped outright. Ask for the next one after February and it answers 2024-03-31, without an error and without an explanation; the leap year 2024 still has just 7 months carrying a day 31.
The harsher case is February 30. In 0 0 30 2 * every field value and the field count are legal, parsing passes all the way through, and nextRun honestly scans the whole 366 × 24 × 60 = 527040 minutes before returning empty. One measured call costs about 58 milliseconds, there is no endless loop and no thrown error, and the interface is left with a single sentence: No next run time found (within one year). 0 0 31 2 * walks the same road.
That ceiling also punishes schedules that are very much alive: after the 7th hit of 0 0 13 * 5, which is 2027-08-13, the next day that is both a Friday and a 13 falls on 2028-10-13, more than 366 days past the previous one, so the 8th lookup shows the same not found message. Sparse is not the same as impossible, yet on this page the two cases print exactly the same words. The other silent path is the reversed range 8-1: the field resolves into an empty set with no complaint at all, and from then on every question returns empty.
Error text: a weekday name is never a name
| Input | The text the page throws |
|---|---|
SUN/sun/Sat/FRI | Invalid value: SUN (case makes no difference, there is no name table) |
weekday field written as 8 | Value 8 out of range [0-7] |
61 * * * * | Value 61 out of range [0-59] |
0 0 0 * * | Value 0 out of range [1-31] (the lower bound of the day field is 1) |
*/0 * * * * | Invalid step: */0 |
| four or six segments | The expression takes 5 fields (minute hour day month weekday) |
Inside the weekday field 7 and 0 are equivalent, because line 145 of parseCron folds 7 into 0, so 0 0 * * 7 and 0 0 * * 0 resolve the same way. An abbreviation real cron understands, FRI for instance, is nothing here but a NaN falling out of parseInt. The field count check runs after the split, which is why 0 0 1, * * *, one extra space behind the comma, is cut into six segments and still answered by that message about 5 fields.
The prediction drifts with its environment too
nextRun reads the local clock through getHours, getDate and getDay, line 229 zeroes the seconds first so any seconds inside the input are thrown away, and the starting instant stays exclusive: when after lands exactly on a hit, that hit is never reported twice. I fixed the start at 2024-01-01 00:00 in Shanghai time, which in milliseconds is 1704038400000 taken from Timestamp Converter, so the answer does not shift from one run to the next.
Daylight saving is the plainest cut of all. The zone this machine resolves is Asia/Shanghai, which has no daylight saving, so TZ=America/New_York has to be forced before anything triggers. In that setting 0 2 * * * starting from 2024-03-08 hits 2024-03-08, 2024-03-09, 2024-03-11 and 2024-03-12, because the spring forward erases 02:00 on March 10 completely. A predicted time was never decided by the expression alone. Two machines that agree on every character of the line can still disagree on the clock behind it, so the same input opened on a laptop and on a server parked in another zone will not print the same list, and the list is only trustworthy once the zone is written down next to it.
The weight of the page itself
Measured on the side: the template is 337 lines and 13855 bytes, with 7 ids, 14 button elements and 1 input, and all of the logic lives on the frontend. The parse button runs a fixed 10 passes to build the list of the next 10 run times, and every pass carries that same 527040 minute ceiling; a pass that returns empty ends the whole run on the spot, which is why the sparse 0 0 13 * 5 is listed with only 7 entries.
Bringing the two semantics back to one line
For "day 13 of every month or every Friday", stop hoping one expression serves both: split it into 0 0 13 * * plus 0 0 * * 5 and hand the union to the scheduler. For "day 13 only when it happens to be a Friday", let the timer fire and have the script test the date itself, and then those 2 predictions match exactly. The verification is dumb but it works: read the weekday and the day of each of the first hits out loud, one by one. The two routes are not interchangeable either: the first one changes when the work happens and leaves the wording of the schedule plain, the second keeps the calendar untouched and pushes the decision into the script, where it can be logged and read back the next morning.
Frequently asked questions
Q: Can the next run time from this page be copied straight into production?
Yes when only the day or only the weekday is restricted, because the two readings coincide there. No once both narrow at the same time, because this page intersects while the server unions.
Q: Does "No next run time found (within one year)" mean the job never runs?
Not necessarily. A date that can never exist, such as 0 0 30 2 *, and a legal but sparse schedule both end on that sentence. In the first case the calendar simply has no such day; in the second the next hit is only further than the 366 day scan limit.
Q: Should Sunday be written as 7 or as 0?
They are equivalent on this page, since line 145 rewrites 7 as 0. But SUN and sat are both rejected as invalid values, so do not bring the abbreviation habits of real cron in here.