An explainable RFC 5545 recurrence rule engine written in MoonBit
moon check
moon test
moon run cmd/rrule_lab -- demomoon run cmd/rrule_lab -- normalize "RRULE:FREQ=WEEKLY;BYDAY=WE,MO,MO"
moon run cmd/rrule_lab -- explain "FREQ=MONTHLY;BYDAY=-1FR" zhmoon run cmd/rrule_lab -- expand \
20260801T090000 \
"FREQ=WEEKLY;COUNT=6;BYDAY=MO,WE,FR" \
20260801T000000 \
20260901T000000 \
20moon run cmd/rrule_lab -- audit 20260801T080000 \
"FREQ=DAILY;COUNT=5" 20260801T000000 20260810T000000
moon run cmd/rrule_lab -- diff 20260801T090000 \
"FREQ=DAILY;COUNT=6" \
"FREQ=DAILY;INTERVAL=2;COUNT=6" \
20260801T000000 20260820T000000moon run cmd/rrule_lab -- helplet start = @rrule_lab.parse_datetime("20260803T093000")
let rule = @rrule_lab.parse_rrule("FREQ=WEEKLY;COUNT=8;BYDAY=MO,WE,FR")
let options = @rrule_lab.ExpansionOptions::new(
@rrule_lab.parse_datetime("20260801T000000"),
@rrule_lab.parse_datetime("20260901T000000"),
limit=20,
)
let occurrences = @rrule_lab.expand_rrule(start, rule, options)
let audit = @rrule_lab.audit_occurrences(occurrences)
println(audit.to_text())moon add cjkzD/rrule_labpub suberror ExpansionError {
InvalidRule(Array[Diagnostic])
InvalidWindow
InvalidLimit(Int)
IterationLimitExceeded(Int)
} derive(Debug)pub struct AuditFinding {
severity : AuditSeverity
code : String
message : String
evidence : Array[DateTime]
suggestion : String
} derive(Eq, Debug)fn BusinessCalendar::add_business_days(self : BusinessCalendar, date : Date, amount : Int) -> Date raise BusinessCalendarErrorfn BusinessCalendar::adjust(self : BusinessCalendar, date : Date, convention : BusinessDayConvention) -> Date raise BusinessCalendarErrorfn BusinessCalendar::adjust_datetime(self : BusinessCalendar, value : DateTime, convention : BusinessDayConvention) -> BusinessDayAdjustment raise BusinessCalendarErrorfn BusinessCalendar::adjust_occurrences(self : BusinessCalendar, values : Array[DateTime], convention : BusinessDayConvention, deduplicate? : Bool) -> Array[BusinessDayAdjustment] raise BusinessCalendarErrorfn BusinessCalendar::count_business_days(self : BusinessCalendar, start : Date, finish : Date) -> Intfn BusinessCalendar::new(name : String, working_weekdays? : Array[Weekday], holidays? : Array[Date]) -> BusinessCalendar raise BusinessCalendarErrorfn BusinessCalendar::next_working_date(self : BusinessCalendar, date : Date, include_current? : Bool, search_limit? : Int) -> Date raise BusinessCalendarErrorfn BusinessCalendar::previous_working_date(self : BusinessCalendar, date : Date, include_current? : Bool, search_limit? : Int) -> Date raise BusinessCalendarErrorfn BusinessCalendar::with_working_weekdays(self : BusinessCalendar, working_weekdays : Array[Weekday]) -> BusinessCalendar raise BusinessCalendarErrorfn BusinessCalendar::working_dates_between(self : BusinessCalendar, start : Date, finish : Date) -> Array[Date]pub struct BusinessDayAdjustment {
original : DateTime
adjusted : DateTime
changed : Bool
convention : BusinessDayConvention
} derive(Eq, Debug)fn DateTime::new(year : Int, month : Int, day : Int, hour : Int, minute : Int, second : Int) -> DateTime raise DateTimeErrorfn ExpansionOptions::new(window_start : DateTime, window_end : DateTime, limit? : Int, iteration_limit? : Int) -> ExpansionOptionspub struct RRule {
frequency : Frequency
interval : Int
count : Int?
until : DateTime?
week_start : Weekday
by_second : Array[Int]
by_minute : Array[Int]
by_hour : Array[Int]
by_day : Array[ByDay]
by_month_day : Array[Int]
by_year_day : Array[Int]
by_week_number : Array[Int]
by_month : Array[Int]
by_set_position : Array[Int]
} derive(Eq, Debug)fn RecurrenceSet::expand(self : RecurrenceSet, options : ExpansionOptions) -> Array[DateTime] raise ExpansionErrorpub struct ScheduleAudit {
occurrence_count : Int
unique_count : Int
duplicate_count : Int
weekend_count : Int
outside_business_hours_count : Int
first_occurrence : DateTime?
last_occurrence : DateTime?
minimum_gap_seconds : Int64?
maximum_gap_seconds : Int64?
average_gap_seconds : Int64?
longest_daily_streak : Int
weekday_histogram : WeekdayHistogram
findings : Array[AuditFinding]
} derive(Eq, Debug)fn analyze_rule_change(start : DateTime, baseline : RRule, candidate : RRule, options : ExpansionOptions) -> ChangeImpact raise ExpansionErrorfn audit_occurrences(values : Array[DateTime], business_start_hour? : Int, business_end_hour? : Int, expected_minimum? : Int, dense_gap_threshold_seconds? : Int64) -> ScheduleAuditfn audit_rule(start : DateTime, rule : RRule, options : ExpansionOptions, business_start_hour? : Int, business_end_hour? : Int) -> ScheduleAudit raise ExpansionErrorfn compare_rules(start : DateTime, left : RRule, right : RRule, options : ExpansionOptions) -> RuleDifference raise ExpansionErrorfn expand_rrule(start : DateTime, rule : RRule, options : ExpansionOptions) -> Array[DateTime] raise ExpansionErrorAn explainable RFC 5545 recurrence rule engine written in MoonBit