RRule for Go
RFC 5545 recurrence rules — the calendar logic cron cannot express.
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
$ go get github.com/malcolmston/rruleQuick start
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— withINTERVAL,COUNTandUNTIL - Every RFC 5545 BY-part —
BYMONTH,BYWEEKNO,BYYEARDAY,BYMONTHDAY,BYDAY,BYHOUR,BYMINUTE,BYSECOND,BYSETPOS, with negative values throughout (BYMONTHDAY=-1,BYSETPOS=-2) - Nth-weekday
BYDAY—1FR,-1SU,20MO, written in Go asFR.Nth(1),SU.Nth(-1) WKSTthreaded through both weekly interval boundaries and ISO week numbering, where it genuinely changes resultsSet—RRULE+RDATE+EXRULE+EXDATEmerged into one ordered, de-duplicated stream, withStrToSetto parse a whole block- Querying —
All,Between,After,Beforeand a pull-basedIteratorwith no occurrence cap - iCalendar —
ParseCalendarhandles RFC 5545 line unfolding and property parameters;Calendar.Encodewrites CRLF with 75-octet folding that never splits a UTF-8 sequence - Round-trippable text —
Parse,StrToRRule,String()(DTSTART + RRULE together, matching dateutil'sstr(rrule)) andRuleString()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=2terminate in milliseconds instead of spinning forever - Zero dependencies — pure Go standard library, no cgo