RRule for Go

github.com/malcolmston/rrule

RFC 5545 recurrence rules — the calendar logic cron cannot express.

 GitHubports dateutil/dateutil

A port of <code>dateutil.rrule</code> and rrule.js: RFC 5545 recurrence rules, recurrence sets, and a minimal iCalendar object model, built entirely on the Go standard library — no third-party modules, no cgo, no require directives. It complements this family's <b>quartz</b> port, which speaks cron; cron cannot say "the second-to-last weekday of the month" or "every other week counting from Sunday", and RRULE can. The engine follows dateutil's structure — per-interval candidate sets, BY-part masks, then BYSETPOS over the survivors — because that structure is what makes RFC 5545 §3.3.10's expand/limit table come out right. Verified against dateutil's own test suite and the RFC's worked examples. The import path is github.com/malcolmston/rrule.

Install

shell
$ go get github.com/malcolmston/rrule

Quick start

main.go
import "github.com/malcolmston/rrule"

r, _ := rrule.New(rrule.Options{
    Freq:      rrule.Monthly,
    DTStart:   time.Date(2026, 1, 1, 9, 0, 0, 0, time.UTC),
    Count:     4,
    ByWeekday: []rrule.Weekday{rrule.FR.Nth(-1)}, <span class="tok-c">// last Friday
})
for _, t := range r.All() {
    fmt.Println(t.Format(time.RFC3339))
}

set := rrule.NewSet()
set.RRule(r)
set.ExDate(time.Date(2026, 3, 27, 9, 0, 0, 0, time.UTC))

Features

  • All seven frequencies — Yearly, Monthly, Weekly, Daily, Hourly, Minutely, Secondly — with INTERVAL, COUNT and UNTIL
  • Every RFC 5545 BY-part — BYMONTH, BYWEEKNO, BYYEARDAY, BYMONTHDAY, BYDAY, BYHOUR, BYMINUTE, BYSECOND, BYSETPOS, with negative values throughout (BYMONTHDAY=-1, BYSETPOS=-2)
  • Nth-weekday BYDAY1FR, -1SU, 20MO, written in Go as FR.Nth(1), SU.Nth(-1)
  • WKST threaded through both weekly interval boundaries and ISO week numbering, where it genuinely changes results
  • SetRRULE + RDATE + EXRULE + EXDATE merged into one ordered, de-duplicated stream, with StrToSet to parse a whole block
  • Querying — All, Between, After, Before and a pull-based Iterator with no occurrence cap
  • iCalendar — ParseCalendar handles RFC 5545 line unfolding and property parameters; Calendar.Encode writes CRLF with 75-octet folding that never splits a UTF-8 sequence
  • Round-trippable text — Parse, StrToRRule, String() (DTSTART + RRULE together, matching dateutil's str(rrule)) and RuleString() for the bare property body
  • DST-correct — wall-clock arithmetic in DTSTART's location, spring-forward gaps resolved per RFC 5545 §3.3.5, and repeat instants suppressed so an hourly rule crossing the gap neither duplicates nor skips
  • Bounded by construction — impossible rules like FREQ=MONTHLY;BYMONTHDAY=31;BYMONTH=2 terminate in milliseconds instead of spinning forever
  • Zero dependencies — pure Go standard library, no cgo
RRule