Bodu.Globalization.Recurrence
Bodu.Globalization.Recurrence evaluates recurring schedules expressed in the industry-standard textual grammars - RFC 5545 (iCalendar) recurrence rules, Vixie-style cron expressions, and instant-anchored intervals in the RFC 5545 duration grammar. Every form parses from text, renders back to canonical text, compares by value, and answers the two questions a scheduler asks: when is the next occurrence? and when was the previous one? It is an independent sibling of the notable-date engine in the Globalization & Calendars topic.
The package is published as Preview: the API surface is stable enough to evaluate and build on, but may still change between releases without a major-version bump. It depends only on Bodu.Core and carries no calendar data.
The four schedule forms
| Form | Type | Canonical text | Shape |
|---|---|---|---|
| RFC 5545 recurrence rule | RecurrenceRule | FREQ=WEEKLY;INTERVAL=2;BYDAY=MO,WE,FR |
Calendar-aligned. A base frequency (DAILY … YEARLY) refined by INTERVAL, bounded by COUNT or UNTIL, and filtered by the BY* parts. The rule carries no start of its own - the series start (DTSTART) is passed to every query. |
| Composed rule set | RecurrenceSet | An iCalendar property block: DTSTART, one or more RRULE lines, optional RDATE / EXDATE |
One or more rules anchored at a common start, merged with explicit recurrence dates and with exception dates removed. |
| Cron expression | CronExpression | 0 2 * * MON-FRI, */15 * * * *, @daily |
Calendar-aligned. The five-field Vixie layout (CronFormat.Standard) or the six-field layout with a leading seconds field (CronFormat.WithSeconds), plus the @yearly / @annually, @monthly, @weekly, @daily / @midnight, and @hourly macros. |
| Anchored interval | AnchoredInterval | PT4H, P1DT2H30M, P2W |
Instant-aligned. Occurrences at anchor + k·interval for k ≥ 1; the anchor ("the last completed run", "contract start") is supplied to every query and is not itself an occurrence. |
The common contract
Whatever the form, the query surface is the same, so a host can put all four behind one adapter:
GetNextOccurrence(after, inclusive)returns the first occurrence strictly afterafter- or atafterwheninclusiveistrue- as a nullable value;nullmeans the schedule produces nothing further (aCOUNTexhausted, anUNTILpassed, or a cron expression that can never match).GetPreviousOccurrence(before, inclusive)returns the last occurrence strictly beforebefore- or atbeforewheninclusiveistrue. Due-ness is a one-line comparison over this answer:lastCompleted < schedule.GetPreviousOccurrence(now, inclusive: true).- Both queries have
DateTimeandDateTimeOffsetoverloads. TheDateTimeoverloads preserve the argument'sKind; theDateTimeOffsetoverloads evaluate on the wall-clock time and reattach the argument's offset. The rule-based forms additionally expose windowed and open-endedGetOccurrencesenumerators. Parse/TryParsefollow the BCLIParsable<T>shape, and every form adds aTryParse(s, out result, out failureMessage)overload whose message names the specific defect (The cron field '25' is not valid.,The COUNT and UNTIL rule parts cannot both appear in the same rule.) rather than a generic "invalid format".ToString()renders the canonical text, which re-parses to an equal value;Equalscompares by value, so a host can detect a changed schedule by comparing the parsed objects rather than the raw strings.
Purity
Every answer is a pure function of its arguments. No member reads the wall clock (DateTime.Now, DateTimeOffset.UtcNow) or consults the machine time zone (TimeZoneInfo.Local); the library's own test suite enforces this with a metadata scan. Consequences:
- Tests are deterministic - supply the "now" you want to test against.
- The library never owns a timer, stores a last-run, or marks an occurrence consumed. Missed occurrences coalesce structurally: an evaluation long after five missed occurrences answers exactly as an evaluation just after the first one, because the answer is an instant, not a backlog.
- Daylight-saving transitions are the caller's concern. The rule-based forms carry no time zone; a host that wants a local-time schedule across a transition re-derives the offset on each evaluation and passes it in. See Core concepts.
Independence from the calendar engine
Bodu.Globalization.Recurrence and Bodu.Globalization.Calendar share no dependency in either direction. The calendar engine's own <Recurrence> sources (<DailyInterval>, <Weekly>, <MonthlyDay>, <MonthlyWeekday>) are calendar-internal strategies for resolving notable dates; this package is the standalone, data-free scheduling grammar. Calendar-aware filtering of a recurrence stream - "the next weekday occurrence that is not a public holiday" - is composition from outside: enumerate here, filter with a NotableDateService there. The recurrence guide shows the pattern.
Which form do I need?
| Schedule | Reach for | Why |
|---|---|---|
| "Daily at 02:00", "weekdays at 09:00", "every 15 minutes" | CronExpression |
The operational shapes cron was designed for; the six-field layout adds seconds. |
| "The last Friday of every month", "the second Tuesday", "every other week on Monday and Thursday" | RecurrenceRule |
BYDAY ordinals (-1FR, 2TU), BYSETPOS, INTERVAL, and the COUNT / UNTIL bounds. |
Schedules that must interoperate with iCalendar data (RRULE text from a calendar feed) |
RecurrenceRule / RecurrenceSet |
The text is the RFC 5545 grammar; a set parses the DTSTART / RRULE / RDATE / EXDATE block directly. |
| "The Monday rule, plus this extra Thursday, minus the two dates we cancelled" | RecurrenceSet |
Rules, explicit additions, and exclusions composed into one ascending, duplicate-free stream. |
| "Every 4 hours after the previous run completed" | AnchoredInterval |
Instant-aligned rather than calendar-aligned - the spacing is measured from an anchor the caller supplies. |
| A schedule bounded to n occurrences or an end date | RecurrenceRule (COUNT / UNTIL) |
Cron and anchored intervals are unbounded by construction. |
Main types
| Type | Purpose |
|---|---|
| RecurrenceRule | Immutable RFC 5545 RRULE: Frequency, Interval, Count, Until, WeekStart, and the BySecond … BySetPos part lists; Parse / TryParse, ToString, and the anchored GetOccurrences / GetNextOccurrence / GetPreviousOccurrence queries. |
| RecurrenceRuleBuilder | Fluent construction of a rule (WithInterval, WithCount / WithUntil, WithWeekStart, ByDay, ByMonthDay, ByMonth, BySetPos, …) terminated by Build(). |
| RecurrenceSet | A start plus rules, explicit dates, and exception dates; parses and formats the iCalendar property block. |
| CronExpression | Parsed Vixie cron with next / previous queries; Format reports the layout it was parsed as. |
| CronFormat | Standard (five fields) or WithSeconds (six fields). |
| AnchoredInterval | A positive whole-second Interval whose occurrences are anchor + k·interval; canonical text is the RFC 5545 duration grammar. |
| RecurrenceFrequency | Secondly … Yearly - the FREQ scale. Sub-daily frequencies parse and round-trip, but enumerating them is not yet supported. |
| WeekDayNum | One BYDAY entry: an Ordinal (0 for every occurrence, 1 for the first, -1 for the last) and a Day. |
Where to go next
- Core concepts - occurrence vs. due-ness, anchor /
DTSTART, inclusive boundaries,WKST, theBY*parts andBYSETPOS, the cron day-field rule and search horizon, the duration grammar, offsets and daylight saving, value equality. - Getting started - install plus a parse → next / previous → enumerate sample for each form, the builder, a set round trip, and defect-naming
TryParse. - Recurrence and scheduling guide - choosing a form, the due-ness recipe, calendar-aware filtering by composition, bounded searches, and conformance.
- Runnable samples - one console project per form plus a scheduling host that puts all four behind one adapter.
- Bodu.Globalization.Recurrence API reference - full type-by-type docs.
- Globalization & Calendars topic - how this package sits beside the calendar engine and its companions.