Projects/Build a Cron Expression Parser and Scheduler

Build a Cron Expression Parser and Scheduler

Parse a five-field cron line into exact value sets so `*/15` on minutes is provably {0, 15, 30, 45}, then match fixed timestamps and compute next fire times you can assert to the minute. Each lesson pins one concrete behavior: a stepped range, JAN meaning 1, both 0 and 7 meaning Sunday, the day-of-month OR day-of-week rule when both are restricted, a next fire that rolls across a month boundary, and a Feb 29 that skips ahead to the next leap year.

22 lessonsSmall~20 min / lessonCron expressionsSchedulingCalendar math
The project

What you'll build over the next 22 lessons

Over 22 lessons you build a working cron expression parser and scheduler as an importable library, driven entirely by explicit fixed timestamps so every result is deterministic and assertable to the minute. There is no wall clock and no hidden state: you parse an expression once into exact sets of allowed values, then ask two questions of it - does this timestamp match, and when does it next fire.

You start by splitting a cron line into its five fields and validating their bounds, then compile each field to the exact set of values it allows: a wildcard, a single number, a list, a range, and a step like `*/15`. You decode three-letter month and day names, handle the quirk that both 0 and 7 mean Sunday, and then match a timestamp - including the classic rule that when both day-of-month and day-of-week are restricted, either one matching is enough. Finally you compute the next fire time by scanning forward a minute at a time, correctly rolling across hour, day, and month boundaries, skipping months with no 31st, and landing on Feb 29 only in a leap year. Shortcut macros like `@daily` round it out.

This is a teaching-grade scheduler built around the standard five-field cron grammar (minute, hour, day-of-month, month, day-of-week) at minute resolution. It is honest about what it stops short of: no seconds field, no time zones or daylight-saving handling (timestamps are treated as naive), and none of the later Vixie and Quartz extensions like `L`, `W`, or `#`. What you finish with is the exact core that libraries like Vixie cron, croniter, and Quartz build on - a parser that compiles a cron line to value sets, a matcher, and a next-fire scanner - demonstrated by a small command-line tool that prints an expression's next fire times.

build-a-cron-parser / lesson-01.md
Lesson 01 · The five fields

Splitting a cron line into five fields

A cron expression is five fields separated by whitespace - minute, hour, day-of-month, month, day-of-week. Before parsing anything you have to split the line into exactly those five parts, and reject a line that has the wrong number.

The goal

Split an expression on whitespace into five fields, or return an error if there are not exactly five.

Start here - the target
TO DO
Scenario: A cron line splits into exactly five fields
Giventhe expression '*/15 0 1,15 * 1-5'
WhenSplitFields is called on it
Thenit returns the five fields '*/15', '0', '1,15', '*', '1-5' in order
AndSplitFields('1 2 3') returns an error, and SplitFields('1 2 3 4 5 6') returns an error
Background

A standard cron expression is a single line of five fields separated by spaces or tabs: minute, hour, day-of-month, month, and day-of-week, always in that order. Everything else in this project is about interpreting one field at a time, so the very first job is to chop the line into exactly five pieces. Splitting on any run of whitespace (not a single space) means extra spacing between fields does not matter, which is how real crontabs are written.

The one rule to enforce today is the count: a line with four or six fields is not a valid five-field cron expression, so reject it with a clear error rather than guessing what the writer meant. Pin both the good split and the wrong-count failure now - every later lesson receives exactly five field strings and never has to worry about the shape of the line again.

Make it work
func SplitFields(expr string) ([]string, error) {
f := strings.Fields(expr) // splits on any run of whitespace
if len(f) != 5 {
return nil, fmt.Errorf("expected 5 fields, got %d", len(f))
}
return f, nil
}
CheckpointDONE
You can split a cron line into its five fields and reject the wrong count. Commit and stop here.
Scope & extensions

Where this project stops - and where to go next

A genuinely working teaching-grade scheduler: the full standard five-field grammar (wildcards, single values, lists, ranges, and steps over both the wildcard and explicit ranges), three-letter month and day names, the quirk that both 0 and 7 mean Sunday, the day-of-month OR day-of-week matching rule, a timestamp matcher, a next-fire scanner that rolls correctly across day, month, and year boundaries and resolves leap-day expressions, next-N fire times, and the @hourly / @daily / @weekly / @monthly / @yearly macros (plus the @midnight and @annually aliases) - driven by a small asset-free command-line demo - but it stops at the standard core: no seconds field, no time zones or daylight-saving handling (timestamps are treated as naive), and none of the Vixie or Quartz L, W, or # extensions.

Extend it next
  • Time-zone-aware scheduling with an explicit location, instead of treating every timestamp as naive local time
  • An optional seconds field - the six-field cron form that some variants accept
  • The L, W, and # day extensions (last day of the month, nearest weekday, the nth weekday) from Vixie and Quartz cron
  • A faster next-fire search that rolls each field forward directly instead of scanning a minute at a time
  • A real scheduler loop that sleeps until each fire time and runs a job, rather than only answering next-fire queries
Recommended reading

Books & references that go deeper

  • The standard definition of the five-field cron grammar - minute, hour, day-of-month, month, day-of-week, their ranges, and the rule that a restricted day-of-month and day-of-week are combined with OR. The baseline every cron implementation starts from.

  • The Vixie-cron field reference: lists, ranges, steps, three-letter month and day names, the fact that both 0 and 7 mean Sunday, and the @hourly / @daily / @weekly / @monthly / @yearly shortcut macros this project implements.

  • Paul Vixie's own crontab documentation, the source of the step syntax (`*/n` and `a-b/n`) and the named macros. A precise, readable statement of the syntax you compile field by field.

  • A focused explainer of the single most surprising rule in cron: when both the day-of-month and day-of-week fields are restricted, the schedule fires when EITHER matches, not both. The exact behavior lesson 16 pins down.

  • An interactive reference that translates any cron expression into plain English and shows its upcoming fire times. Use it to sanity-check the value sets and next-fire timestamps you compute by hand.